處理鍵盤通知

發表於2015-10-08

近日在 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 不會在之後的正式版中出現。因此,我們還需要更好的辦法。

鍵盤通知除了我們常見的四個:

之外,還有兩個 iOS 5 才引入的:

經我測試,在UIKeyboardWillShowNotification傳送次數不正確時,UIKeyboardWillChangeFrameNotificationUIKeyboardDidChangeFrameNotification都能正確傳送。這自然會成為解決問題的關鍵。

因為我們要做的是鍵盤跟隨動畫,因此不考慮UIKeyboardDidChangeFrameNotification,因為Did表明它“滯後”了。那麼UIKeyboardWillChangeFrameNotification就成為了我們唯一的希望。

通過監聽它,我們可以觀察到它會在UIKeyboardWillShowNotification之前或者在UIKeyboardDidHideNotification之後發出。因為鍵盤隱藏的通知並沒有不正常,所以我們不需要關心其在UIKeyboardDidHideNotification的傳送。也就是說,我們要把UIKeyboardWillChangeFrameNotification當作UIKeyboardWillShowNotification來用,以保證獲取到正確的鍵盤高度。但這樣以來,鍵盤出現通知的“次數”就多了,我們還要想辦法縮減到正確的次數。

我們先把鍵盤通知分成兩類:Show 和 Hide。因為UIKeyboardWillChangeFrameNotification會被當作 Show 來來使用,需要避免它在 Hide 時生效。

於是我們定義一個結構 KeyboardInfo:

並定義一個變數:

每次收到鍵盤通知時,我們就更新此變數,其中action能表示當前是 Show 還是 Hide,而isSameAction需要計算,表示當前的action是否與之前的一樣,可用於區別鍵盤通知型別的轉換。

那麼我們的通知處理邏輯如下:

其中,私有函式handleKeyboard(_, _) 將通知裡的資訊取出生成 KeyboardInfo 並賦值給keyboardInfo

然後注意keyboardWillChangeFrame函式,它處理UIKeyboardWillChangeFrameNotification。因為此通知會在UIKeyboardWillShowNotification之前傳送,要將它當作UIKeyboardWillShowNotification來用的前提是:

  1. keyboardInfo 不存在,表示鍵盤還未彈出過,(因為 UIKeyboardWillShowNotification 至少會傳送一次,故不處理UIKeyboardWillChangeFrameNotification
  2. keyboardInfo已存在,只要保證前一次是 Show 再處理即可。

最後,鍵盤通知的次數處理,在設定 keyboardInfo 時,我們增加一個 willSet

可以看出,我們只會在鍵盤Action改變時,或鍵盤高度增量不等於 0 時才進行真正的處理。由此,就可以避免因為將UIKeyboardWillChangeFrameNotification當作UIKeyboardWillShowNotification用而導致“次數”反而增加了。

不過還有一個新情況:當鍵盤出現後,若使用者按下 Home 進入後臺,然後回到本應用,那麼 iOS 還會再傳送UIKeyboardWillShowNotificationUIKeyboardWillChangeFrameNotification,而我們並不需要它們。好在這樣的情況很好處理,只需在 willSet 的頂部先判斷一下應用的狀態即可:

此外,iOS 的鍵盤在某些裝置上還可以拆分(Split)和浮動(Undock),這時系統會傳送兩個 Hide 通知,若之後再 dismiss 時,系統會傳送 UIKeyboardWillChangeFrameNotification,不過這個時候就不能將其當做 Show 來處理了。好在上面的keyboardWillChangeFrame函式已經避免了這樣的情況。

有了這些程式碼和考量後,我們就可以暴露“閉包”給外部,閉包的執行就放在上面程式碼 TODO 的位置。

出於方便的考慮,KeyboardMan 共暴露三個閉包:

其中前兩個閉包比較方便,放在其中的程式碼會被自動“動畫”,易於使用。第三個將每次重新整理的 KeyboardInfo 傳送出去,使用的邏輯就交給程式設計師了。另外,稍微注意一下 animateWhenKeyboardAppear 閉包的appearPostIndex引數,它表示“本次”鍵盤出現時,通知傳送到第幾次了(每次都從0開始,有可能你的程式碼裡用得到)。如果你用 postKeyboardInfo 閉包那麼可用keyboardMan引數取到它。

還有一些細節,包括通知監聽開啟或關閉的實現(注意 deinit 裡設定屬性並不會觸發對應的 willSet 或 didSet),通知內容解析的實現,具體請看 KeyboardMan 的程式碼。另外,Demo 裡有三個閉包的基本用法。

專案地址:https://github.com/nixzhu/KeyboardMan

最後是個預告,我最近在寫一本關於演算法的書(程式碼用 Swift 2),不會是系統的演算法講解,而是從具體例子實現一些“綜合性”的演算法,重點在於分析的過程。但只剛開了個頭,希望能在 Swift 2 正式版釋出前完成,似乎時間不多了,不敢保證。

相關文章