近日在 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 正式版釋出前完成,似乎時間不多了,不敢保證。