分析一個有趣的 Swift 項目:LTBouncyPlaceholder

jopen 10年前發布 | 7K 次閱讀 LTBouncyPlaceholder

本文嘗試分析 LTBouncyPlaceholder 項目的實現

項目作者:lexrus

分析作者:@nixzhu

我希望你已經下載了 LTBouncyPlaceholder 的 Demo ,用 Xcode 6 打開并編譯、運行,然后在界面中顯示的幾個 UITextField 里輸入一些文字來體驗這個擴展。看到 Placeholder 的動畫了嗎?PS:iOS 8 的鍵盤也挺帶感。

Demo

開始

我首先觀察到擴展里重載了 willMoveToSuperview:,對于這個方法,Xcode 的文檔里寫有:

The default implementation of this method does nothing. Subclasses can override it to perform additional actions whenever the superview changes.

也就是說,這個方法默認不做事情,但子類可以重載它以便在 superview 改變時執行額外的操作。那么當 UITextField 被加載時,這個方法就會自動調用,所以我們先來看看它做了什么:

override func willMoveToSuperview(newSuperview: UIView!) {
    if newSuperview {
        // 1. 首先要求 lt_placeholderLabel 顯示它自己
        lt_placeholderLabel.setNeedsDisplay()

        // 2. 然后替換了 drawPlaceholderInRect 方法
        struct TokenHolder {
            static var token: dispatch_once_t = 0;
        }

        dispatch_once(&TokenHolder.token) {
            var originMethod: Method = class_getInstanceMethod(object_getClass(UITextField()),
                Selector.convertFromStringLiteral("drawPlaceholderInRect:".bridgeToObjectiveC().UTF8String))
            var swizzledMethod: Method = class_getInstanceMethod(object_getClass(UITextField()),
                Selector.convertFromStringLiteral("_drawPlaceholderInRect:".bridgeToObjectiveC().UTF8String))
            method_exchangeImplementations(originMethod, swizzledMethod)

        }

        // 3. 最后監聽通知,這樣用戶輸入文字或刪除文字時,_didChange: 也會執行了
        NSNotificationCenter.defaultCenter().addObserver(self,
            selector: Selector.convertFromStringLiteral("_didChange:"),
            name: UITextFieldTextDidChangeNotification,
            object: nil)
    } else {
        NSNotificationCenter.defaultCenter().removeObserver(self,
            name: UITextFieldTextDidChangeNotification,
            object: nil)
    }
}

閱讀代碼并觀察我添加的注釋,我們知道了,這個擴展為原類添加了一個新的屬性 lt_placeholderLabel(因為它是被直接使用的,就像原類的屬性一樣不需要寫 self.),而從其名字可以得知它應該是一個用于顯示占位符的 UILabel;之后 lexrus 利用 dispatch_oncemethod_exchangeImplementations 將系統的 drawPlaceholderInRect: 實現替換為 _drawPlaceholderInRect:,我們稍后會分析它的實現;最后,lexrus 在擴展里監聽 UITextFieldTextDidChangeNotification 通知,這個通知大家應該比較熟悉,即當 UITextField 里的文字發生改變時,這個通知就會被發出。作者希望在這個通知出現時做一些事情,因而要執行 _didChange,我們之后也會分析其實現。

“虛擬”屬性

我們首先看看 lt_placeholderLabel ,既然 lexrus 對其調用了 setNeedsDisplay() ,那么它肯定要先生成。在 UITextField+LTBouncyPlaceholder.swift 里搜索 lt_placeholderLabel ,我們就會看到:

var lt_placeholderLabel: UILabel {
get {
    var _placeholderLabelObject: AnyObject? = objc_getAssociatedObject(self, kPlaceholderLabelPointer)
    if let _placeholderLabel : AnyObject = _placeholderLabelObject {
        return _placeholderLabel as UILabel
    }
    var _placeholderLabel = UILabel(frame: self.placeholderRectForBounds(self.bounds))
    _placeholderLabel.font = self.font
    _placeholderLabel.text = placeholder
    _placeholderLabel.textColor = UIColor.lightGrayColor()
    self.addSubview(_placeholderLabel)
    objc_setAssociatedObject(self,
        kPlaceholderLabelPointer,
        _placeholderLabel,
        objc_AssociationPolicy(OBJC_ASSOCIATION_RETAIN_NONATOMIC))
    return _placeholderLabel
}
}

這是一個實例變量,但要知道我們現在在一個擴展中,并不能直接擴展類的屬性。而為了擴展原類的屬性,lexrus 使用了被稱為“關聯對象(Associated Objects)”的技術(請參考mattt編寫的文章,中文翻譯英文原文),利用 objc_setAssociatedObjectobjc_getAssociatedObject “虛擬”出一個屬性。而這個屬性使用起來的感覺和在原類中定義的屬性一樣。

上面的代碼并不復雜,get 類似 Objective-C 里的 getter,當我們訪問這個屬性的時候,它就會自動執行。它首先看看是否已有這個“虛擬屬性”,有就直接返回。若沒有,就利用原類的 bounds 和自帶方法 placeholderRectForBounds 計算一個 frame 以便生成了一個新的 UILabel,再設置好字體等就作為 subview 被添加到 self(即 UITextField) 上了。最后設置好“虛擬屬性”。這樣下次再訪問此屬性時,這個 UILabel 就可以直接返回而不會被重復創建了。怎樣?一樣有 Lazyload 的感覺吧?

另外,稍微注意一下 kPlaceholderLabelPointer 的使用,它定義在 UITextField+LTBouncyPlaceholderKeys.swift 文件里,其實是一個 CConstVoidPointer ,相當于 C 的 const void *,具體請參考 Apple 提供的文檔:Interacting with C APIs 一節。

方法替換

接下來,我們看看 _drawPlaceholderInRect: 的實現:

func _drawPlaceholderInRect(rect: CGRect) {

}

似乎什么都沒做,而這正是方法替換的神奇之處。根據 drawPlaceholderInRect: 的文檔說明:

You should not call this method directly. If you want to customize the drawing behavior for the placeholder text, you can override this method to do your drawing.

By the time this method is called, the current graphics context is already configured with the default environment and text color for drawing. In your overridden method, you can configure the current context further and then invoke super to do the actual drawing or do the drawing yourself. If you do render the text yourself, you should not invoke super.

這個方法是用于繪制原生的 Placeholder 的,而我們現在使用了自定義的 Placeholder ,因此原生的對我們來說沒有用處了,所以不需要將其繪制出來。

處理通知

最后我們再來看看 _didChange 做了什么:

func _didChange (notification: NSNotification) {
    if notification.object === self {
        if self.text.lengthOfBytesUsingEncoding(NSUTF8StringEncoding) > 0 {
            if alwaysBouncePlaceholder {
                self._animatePlaceholder(toRight: true)
            } else {
                lt_placeholderLabel.hidden = true
            }
        } else {
            if alwaysBouncePlaceholder {
                self._animatePlaceholder(toRight: false)
            } else {
                lt_placeholderLabel.hidden = false
            }
        }
    }
}

先確認通知的發送者是自己,然后在 UITextField 里輸入有文字時,若 alwaysBouncePlaceholder 屬性的狀態為 true,就執行 self._animatePlaceholder(toRight: true) ,我想大概是將我們剛才討論的作為 Placeholder 的 UILabel 以動畫的方式移動到右邊。

注意:如之前提到,UITextField 本身的 Placeholder 因為方法替換,不會被繪制出來,而 lt_placeholderLabeltext 就是設置為 UITextField 自身的 placeholder 的,在沒有輸入文字時,它看起來就是原生的 Placeholder。

動畫

事實上,只要讀者稍微閱讀一下 _animatePlaceholder 就可以分析出來,lexrus 還使用了一個名為 lt_rightPlaceholderLabel 的“虛擬屬性”,用于在 UITextField 里有字符時,在其最右邊顯示另外一個 Placeholder,大家運行 Demo 時應該有所體會。

這里的 Core Animation 將 lt_placeholderLabel 移動到右邊并隱去,與此同時, lt_rightPlaceholderLabel 也被移動到右邊,但它是漸顯,這樣就得到我們所體驗到的效果。

作者注:不知道只用一個自定義的 Placeholder 能否實現這個效果,但既然 lexrus 這樣寫,可能有他的道理。

補記:lexrus 給出了解釋:

用兩個 UILabel 是因為我之前在 QuartzComposer 里試過只用一個的話,文字變化的動畫就會比較難做,兩個 UILabel 分別淡入、淡出比較簡單。

在 IB 里設置運行時屬性

最后,在擴展文件的開頭,我們還看到 alwaysBouncePlaceholderabbreviatedPlaceholder 這兩個“虛擬屬性”,它們被創建所用的技術和 lt_placeholderLabel 一致,不再贅述。只需要觀察到,這兩個屬性在 Demo 中的 IB 中對應 UITextField 的 Identity Inspector 里有被使用,這樣就等于直接初始化了它們。

另外,abbreviatedPlaceholder 意思是“簡短的占位符”,它順便設置了 lt_rightPlaceholderLabeltext 屬性,非常合理。注意 newValue 應該是 set 的默認參數。

總結

到此,我們差不多就分析完了這個相當出色的 UITextField 的擴展,它為我們帶來了新鮮的使用體驗。

而我們學習了一些新技術,特別是以 Swift 語言寫成這一點值得大家研究。這些技術是:

  • 屬性的 set 和 get 的使用
  • 在擴展里“創建”屬性的方法,即“關聯對象(Associated Objects)”
  • UITextField 的工作特點,加載、通知等,這些對于自定義控件來說很有用
  • 一些 Core Animation 的組合,彈性動畫+漸隱漸顯
  • GCD 的使用,注意到 dispatch_once 了嗎?它一般用于生成單例。
  • 方法替換,作者替換了 drawPlaceholderInRect: 以取消原生 Placeholder 的繪制。

挑戰

  1. 如我提到的,是否只用一個自定義的 Placeholder 也能實現這個效果?
  2. 用本文分析的這些技術,為系統的其它原生控件編寫擴展。你需要的可能僅僅只是一點品味和想像力。
  3. 能直接在擴展里使用 IBDesignable 和 IBInspectable 嗎?如果不能,嘗試使用子類化的方式來實現這個擴展并加上對 IBDesignable 和 IBInspectable 的支持,你可以參考我翻譯的這篇文章

若本文的分析有任何不當之處,請讀者指出,提 Issue 或者直接發送 PR 都可以!

作者注:歡迎非商業轉載,但請一定注明出處:https://github.com/nixzhu/dev-blog

歡迎轉發此條微博 http://weibo.com/2076580237/B8CSosnAz 以分享給更多人!

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!