近日在 iOS 9 beta4 上,我更觀察到 UIKeyboardWillShowNotification 可能會少發,導致之前根據其發送次數做的算法不能正常工作,結果就是在使用中文拼音鍵盤時,本該處于鍵盤上方的輸入框會被鍵盤擋住大部分。
為了解決這個問題,同時也對鍵盤通知相關的代碼做整理并重構(畢竟這些代碼分散在 ViewController 里也不好維護,更難以重用),我想寫一個單獨的庫是最好的選擇。至于庫的名字,“鍵盤俠”就很不錯。雖然在中文里它不算個好詞匯,不過英文念著還不錯:KeyboardMan。
先說一下之前對鍵盤通知UIKeyboardWillShowNotification發送多次的處理。
在做鍵盤跟隨動畫時,我們需要根據鍵盤的高度來調整某些 View 的位置,或者要更新 UIScrollView(UITableView、UICollectionView)的 contentOffset 和 contentInset,以使某些內容不被鍵盤擋住。
既然是鍵盤跟隨動畫,那必然要監聽UIKeyboardWillShowNotification以獲取鍵盤高度以及動畫參數(時長和曲線類型)。因為當用戶使用某些鍵盤時,UIKeyboardWillShowNotification并不止發送一次,第一次的高度并不是最后完整鍵盤的高度。如果我們簡單地獨立對待每一次通知,但由于調整contentOffset應該用增量的方式,將導致我們要在處理鍵盤通知前紀錄當前的contentOffset并利用它實現增量的效果。很明顯,“鍵盤彈出前的contentOffset”需要我們的小心維護,自然,這并不有趣。
然后,上面的方式可能失效,例如當UIKeyboardWillShowNotification本該發送兩次時卻只發送了一次,那我們就不能獲取到正確的鍵盤高度,以此,即不能正確設置contentOffset,也會導致鍵盤上的輸入框會被鍵盤擋住(輸入框的位置調整不需要考慮增量的問題,只需要正確的鍵盤高度)。
雖說UIKeyboardWillShowNotification的發送次數不夠很可能是 iOS 9 beta4 的 bug,但我們很難保證這樣的 bug 不會在之后的正式版中出現。因此,我們還需要更好的辦法。
鍵盤通知除了我們常見的四個:
1 2 3 4 | let UIKeyboardWillShowNotification: String let UIKeyboardDidShowNotification: String let UIKeyboardWillHideNotification: String let UIKeyboardDidHideNotification: String |
之外,還有兩個 iOS 5 才引入的:
1 2 | let UIKeyboardWillChangeFrameNotification: String let UIKeyboardDidChangeFrameNotification: String |
經我測試,在UIKeyboardWillShowNotification發送次數不正確時,UIKeyboardWillChangeFrameNotification和UIKeyboardDidChangeFrameNotification都能正確發送。這自然會成為解決問題的關鍵。
因為我們要做的是鍵盤跟隨動畫,因此不考慮UIKeyboardDidChangeFrameNotification,因為Did表明它“滯后”了。那么UIKeyboardWillChangeFrameNotification就成為了我們唯一的希望。
通過監聽它,我們可以觀察到它會在UIKeyboardWillShowNotification之前或者在UIKeyboardDidHideNotification之后發出。因為鍵盤隱藏的通知并沒有不正常,所以我們不需要關心其在UIKeyboardDidHideNotification的發送。也就是說,我們要把UIKeyboardWillChangeFrameNotification當作UIKeyboardWillShowNotification來用,以保證獲取到正確的鍵盤高度。但這樣以來,鍵盤出現通知的“次數”就多了,我們還要想辦法縮減到正確的次數。
我們先把鍵盤通知分成兩類:Show 和 Hide。因為UIKeyboardWillChangeFrameNotification會被當作 Show 來來使用,需要避免它在 Hide 時生效。
于是我們定義一個結構 KeyboardInfo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public struct KeyboardInfo { public let animationDuration: NSTimeInterval public let animationCurve: UInt public let frameBegin: CGRect public let frameEnd: CGRect public var height: CGFloat { return frameEnd.height } public let heightIncrement: CGFloat public enum Action { case Show case Hide } public let action: Action let isSameAction: Bool } |
并定義一個變量:
1 | var keyboardInfo: KeyboardInfo? |
每次收到鍵盤通知時,我們就更新此變量,其中action能表示當前是 Show 還是 Hide,而isSameAction需要計算,表示當前的action是否與之前的一樣,可用于區別鍵盤通知類型的轉換。
那么我們的通知處理邏輯如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | func keyboardWillShow(notification: NSNotification) { handleKeyboard(notification, .Show) } func keyboardWillChangeFrame(notification: NSNotification) { if let keyboardInfo = keyboardInfo { if keyboardInfo.action == .Show { handleKeyboard(notification, .Show) } } } func keyboardWillHide(notification: NSNotification) { handleKeyboard(notification, .Hide) } func keyboardDidHide(notification: NSNotification) { keyboardInfo = nil } |
其中,私有函數handleKeyboard(_, _) 將通知里的信息取出生成 KeyboardInfo 并賦值給keyboardInfo。
然后注意keyboardWillChangeFrame函數,它處理UIKeyboardWillChangeFrameNotification。因為此通知會在UIKeyboardWillShowNotification之前發送,要將它當作UIKeyboardWillShowNotification來用的前提是:
keyboardInfo 不存在,表示鍵盤還未彈出過,(因為 UIKeyboardWillShowNotification 至少會發送一次,故不處理UIKeyboardWillChangeFrameNotification)keyboardInfo已存在,只要保證前一次是 Show 再處理即可。最后,鍵盤通知的次數處理,在設置 keyboardInfo 時,我們增加一個 willSet
1 2 3 4 5 6 7 8 9 | var keyboardInfo: KeyboardInfo? { willSet { if let info = newValue { if !info.isSameAction || info.heightIncrement != 0 { //TODO } } } } |
可以看出,我們只會在鍵盤Action改變時,或鍵盤高度增量不等于 0 時才進行真正的處理。由此,就可以避免因為將UIKeyboardWillChangeFrameNotification當作UIKeyboardWillShowNotification用而導致“次數”反而增加了。
不過還有一個新情況:當鍵盤出現后,若用戶按下 Home 進入后臺,然后回到本應用,那么 iOS 還會再發送UIKeyboardWillShowNotification和UIKeyboardWillChangeFrameNotification,而我們并不需要它們。好在這樣的情況很好處理,只需在 willSet 的頂部先判斷一下應用的狀態即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 | var keyboardInfo: KeyboardInfo? { willSet { if UIapplication.sharedApplication().applicationState != .Active { return } if let info = newValue { if !info.isSameAction || info.heightIncrement != 0 { //TODO } } } } |
此外,iOS 的鍵盤在某些設備上還可以拆分(Split)和浮動(Undock),這時系統會發送兩個 Hide 通知,若之后再 dismiss 時,系統會發送 UIKeyboardWillChangeFrameNotification,不過這個時候就不能將其當做 Show 來處理了。好在上面的keyboardWillChangeFrame函數已經避免了這樣的情況。
有了這些代碼和考量后,我們就可以暴露“閉包”給外部,閉包的執行就放在上面代碼 TODO 的位置。
出于方便的考慮,KeyboardMan 共暴露三個閉包:
1 2 3 4 5 | public var animateWhenKeyboardAppear: ((appearPostIndex: Int, keyboardHeight: CGFloat, keyboardHeightIncrement: CGFloat) -> Void)? public var animateWhenKeyboardDisappear: ((keyboardHeight: CGFloat) -> Void)? public var postKeyboardInfo: ((keyboardMan: KeyboardMan, keyboardInfo: KeyboardInfo) -> Void)? |
其中前兩個閉包比較方便,放在其中的代碼會被自動“動畫”,易于使用。第三個將每次刷新的 KeyboardInfo 發送出去,使用的邏輯就交給程序員了。另外,稍微注意一下 animateWhenKeyboardAppear 閉包的appearPostIndex參數,它表示“本次”鍵盤出現時,通知發送到第幾次了(每次都從0開始,有可能你的代碼里用得到)。如果你用 postKeyboardInfo 閉包那么可用keyboardMan參數取到它。
還有一些細節,包括通知監聽開啟或關閉的實現(注意 deinit 里設置屬性并不會觸發對應的 willSet 或 didSet),通知內容解析的實現,具體請看 KeyboardMan 的代碼。另外,Demo 里有三個閉包的基本用法。
項目地址:https://github.com/nixzhu/KeyboardMan
最后是個預告,我最近在寫一本關于算法的書(代碼用 Swift 2),不會是系統的算法講解,而是從具體例子實現一些“綜合性”的算法,重點在于分析的過程。但只剛開了個頭,希望能在 Swift 2 正式版發布前完成,似乎時間不多了,不敢保證。
全能程序員交流QQ群290551701,群內程序員都是來自,百度、阿里、京東、小米、去哪兒、餓了嗎、藍港等高級程序員 ,擁有豐富的經驗。加入我們,直線溝通技術大牛,最佳的學習環境,了解業內的一手的資訊。如果你想結實大牛,那 就加入進來,讓大牛帶你超神!
新聞熱點
疑難解答