Keyboard Handler — Observer Pattern

春麗 S.T.E.M.
8 min readAug 24, 2023

--

目錄

⦿ 思考方向
⦿ textFiedldDidBeginEditing
⦿ Boxing
⦿ ViewController

思考方向

這篇文章要修改使用 TextField 時,鍵盤彈出時去提高整體 View 的高度的做法,之前除簡單使用 ScrollView 達成之外,在另篇文章中,也用了 NotificationCenter 監聽當鍵盤彈出時,鍵盤此時的高度,於是就可以計算鍵盤頂部的 Y,當你取得當下 TextField 的底部 Y,兩者相減來確定 View 要提高多少以免擋住鍵盤。

NotificationCenter 的做法中要加入兩個監聽,一個是 UIResponder.keyboardWillShowNotification,一個是 UIResponder.keyboardWillHideNotification,這是由於 CoreFoundation 已幫我們做好 post,我們就可專注在 keyboard show and hide 各自要做什麼事情上面,我們可以在公堂之上假設一下,keyboard show 的時候,會在 UITextFieldDelegate 中的 textFieldDidBeginEditing(_ textField: UITextField) 發出通知;hide 的時候,會在 textFieldDidEndEditing(_ textField: UITextField) 發出通知。

這邊我們新建一個 class 來 Handle 鍵盤擋住輸入框這件事,所以這個 class 就叫做 KeyboardHandler,如下:

class KeyboardHandler: NSObject, UITextFieldDelegate {

weak var activeTextField: UITextField?
weak var viewController: UIViewController?
var keyboardOffset: CGFloat = 0
let offsetY: CGFloat = 20

init(for viewController: UIViewController) {
self.viewController = viewController
}

func textFieldDidBeginEditing(_ textField: UITextField) {


}

func textFieldDidEndEditing(_ textField: UITextField) {
keyboardOffset.value = 0
}
}

不論如何,當你要使用 Observer Pattern 來做到 Handle Keyboard shown 時,可以參考 Notification Center 怎麼做。而我們仍然需要一個現在正在使用的 TextField,所以宣告 activeTextField,而本來應該在 ViewController 中遵循 UITextFieldDelegate 的事,我們希望給 KeyboardHandler 做,所以也須宣告 viewController,並且在檢查並計算高度時,會需要該 viewController 底 View 的高度。

接著 keyboardOffset 就是你要提高的高度,offsetY 是你另外想多提高的高度。

下面我們來看看 textFieldDidBeginEditing 怎麼實作。

繼續閱讀|回目錄

textFieldDidBeginEditing

    func textFieldDidBeginEditing(_ textField: UITextField) {
activeTextField = textField

guard let activeTextField = activeTextField else { return }

// 此刻 TextField 的底部 Y
let textFieldFrame = activeTextField.convert(activeTextField.bounds,
to: self.viewController?.view)
let textFieldY = textFieldFrame.maxY
// 假設鍵盤高度
let estimatedKeyboardHeight: CGFloat = 360

// 此刻鍵盤頂部 Y
let keyboardY = (self.viewController?.view.frame.height ?? 0) - estimatedKeyboardHeight
// 若大於零需提高
let targetY = textFieldY - keyboardY

if targetY > 0 {
keyboardOffset = -targetY - offsetY
}
}

同樣地,我們需要將 activeField 設為當前的 textField,接著去找到當前 textField 在 SuperView 的位置,找到底部 Y。

由於我們不想採用 NotificationCenter,所以沒辦法即時找到當前鍵盤的高度了,所以採用跟 ScrollView 解決方案一樣的做法,設定一個固定的鍵盤高,由於鍵盤是從底部彈出,一樣我們去找到它的頂部 Y,最後設定 keyboardOffset。

在 Observer Pattern 中,你還記得 Boxing 嗎?

繼續閱讀|回目錄

Boxing

在 Boxing 中,我們透過宣告一個數值,包括這個數值的型別,讓它與 UI 綁定,是謂 Data Binding。

如果在 ViewController 裡,我們想這樣使用它:

keyboardHandler.keyboardOffset.bind({ [weak self] offset in
UIView.animate(withDuration: 0.25) {
self?.view.frame = CGRect(x: 0,
y: offset,
width: self?.view.bounds.width ?? 0,
height: self?.view.bounds.height ?? 0)
}
})

當 keyboardHandler 中的 keyboardOffset 改變時,我們去提高 ViewController 底 View 的高度,這個高度就是改變的數值 offset,在 textFieldDidBeginEditing 時,offset 依照不同的 TextField 去計算出不同的高度,在 textFieldEndEditing 時,offset 回到 0。

太棒了!這個綁定的 Data 就是 keyboardOffset,所以如下宣告:

let keyboardOffset: Box<CGFloat> = Box(0)

它預設為 0,也就是說 ViewController 的底 View 此時也被初始化,我們再將 UITextFieldDelegate 實作的 function 改成如下:

func textFieldDidBeginEditing(_ textField: UITextField) {
if targetY > 0 {
keyboardOffset.value = -targetY - offsetY
}
}

func textFieldDidEndEditing(_ textField: UITextField) {
keyboardOffset.value = 0
}

接著,我們要回到 ViewController 裡去使用 KeyboardHandler 了。

繼續閱讀|回目錄

ViewController

    @IBOutlet weak var firstTextField: UITextField!
@IBOutlet weak var secondTextField: UITextField!
@IBOutlet weak var thirdTextField: UITextField!

private lazy var keyboardHandler = KeyboardHandler(for: self)

override func viewDidLoad() {
super.viewDidLoad()

firstTextField.delegate = self.keyboardHandler
secondTextField.delegate = self.keyboardHandler
thirdTextField.delegate = self.keyboardHandler

keyboardHandler.keyboardOffset.bind({ [weak self] offset in

UIView.animate(withDuration: 0.25) {
self?.view.frame = CGRect(x: 0,
y: offset,
width: self?.view.bounds.width ?? 0,
height: self?.view.bounds.height ?? 0)
}
})

}

Cool,完成了!

透過 Boxing,我們也可以將 View(UI)跟 KeyboardHandler 的 keyboardOffset 綁定,當 offset 變化時,View 跟著變化。

最終成果如下:

這次就分享到這,感謝您的閱讀。

繼續閱讀|回目錄

附上 GitHub:

--

--

春麗 S.T.E.M.
春麗 S.T.E.M.

Written by 春麗 S.T.E.M.

Do not go gentle into that good night, Old age should burn and rave at close of day; Rage, rage, against the dying of the light.

No responses yet