問題描述
在開發 iOS 應用時,遇到一個棘手的 bug:當 TextField 位於畫面下方時,使用者快速連續點擊多次,會在鍵盤還未完全升起時就誤觸鍵盤按鍵,導致不預期的文字被輸入到 TextField 中。
為什麼這個 Bug 如此難以重現?
起初,這個問題讓人非常困擾,因為:
- QA 回報:Bug 描述為「誤擊」,但一般測試方法(點一下 → 等鍵盤跳出 → 再點)很難模擬
- 關鍵發現:需要「快速連擊」+ 「鍵盤升起速度較慢」的條件才會觸發
- 設備差異:
- ✅ iPhone 13 (iOS 22):可穩定重現
- ❌ iPhone 15 (iOS 18):無法重現
這說明了舊款手機因為效能較低,鍵盤動畫較慢,更容易出現這個問題。
嘗試過的方案(全部失敗)
在尋找解決方案時,我嘗試了三種常見的做法:
❌ 方案 1:使用 isUserInteractionEnabled
控制
@objc func keyboardWillShow(_ notification: Notification) {
textField.isUserInteractionEnabled = false
}
@objc func keyboardDidShow(_ notification: Notification) {
textField.isUserInteractionEnabled = true
}
❌ 方案 2:使用遮罩層防止誤觸
@objc func keyboardWillShow(_ notification: Notification) {
overlayView.isHidden = false
}
@objc func keyboardDidShow(_ notification: Notification) {
overlayView.isHidden = true
}
❌ 方案 3:使用 TextField Delegate 控制
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
return canBeginEditing
}
失敗原因分析
這三種方案都有一個致命的缺陷:
所有的阻擋邏輯都在
keyboardWillShow
之後才執行,但誤觸輸入已經在keyboardWillShow
之前發生了!
當使用者快速連點時,文字已經被輸入進 TextField,再去阻擋已經來不及了。
✅ 最終解決方案
關鍵突破點是:不要等鍵盤通知,直接在輸入發生的瞬間進行攔截!
核心思路
- 監聽
.editingChanged
事件:直接監聽 TextField 的編輯變化 - 即時攔截:在輸入的瞬間檢查鍵盤狀態,不符合條件就立即清空
- 鍵盤狀態追蹤:維護一個
isKeyboardVisible
標記
實作程式碼
class ViewController: UIViewController {
@IBOutlet weak var textField: UITextField!
private var isKeyboardVisible = false
override func viewDidLoad() {
super.viewDidLoad()
// 監聽 TextField 的編輯變化
textField.publisher(for: .editingChanged)
.sink { [weak self] _ in
self?.handleTextFieldChanged()
}
.store(in: &cancellables)
// 註冊鍵盤通知
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardWillShow),
name: UIResponder.keyboardWillShowNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(keyboardDidShow),
name: UIResponder.keyboardDidShowNotification,
object: nil
)
}
private func handleTextFieldChanged() {
// 關鍵:如果鍵盤還沒完全升起,立即清空輸入
if !isKeyboardVisible {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.textField.text = ""
// 手動觸發事件,保持與其他 publisher 同步
self.textField.sendActions(for: .editingChanged)
}
}
}
@objc func keyboardWillShow(_ notification: Notification) {
isKeyboardVisible = false
}
@objc func keyboardDidShow(_ notification: Notification) {
isKeyboardVisible = true
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}
工作流程
- 使用者在鍵盤未升起時快速點擊輸入框
- 任何輸入都會觸發
publisher(for: .editingChanged)
- 檢查
!isKeyboardVisible
→ 立即清空textField.text
- 手動觸發
sendActions(for: .editingChanged)
→ 通知其他 publisher - 直到
keyboardDidShow
被調用,isKeyboardVisible
才變為true
- 之後的輸入可以正常保留
方案優勢
✅ 即時過濾:在鍵盤升起前的任何輸入都會被立即清空
✅ 配合底層:完全使用 TextField 的 publisher 機制
✅ 保持同步:確保所有相關的 publisher 都能收到正確的事件
✅ 防誤觸鍵盤:有效防止快速點擊時的鍵盤誤觸問題
✅ 架構兼容:與現有的 Combine 或 RxSwift 架構完全兼容
關鍵技術要點
1. 為什麼用 DispatchQueue.main.async
?
DispatchQueue.main.async { [weak self] in
self.textField.text = ""
self.textField.sendActions(for: .editingChanged)
}
- 避免在
.editingChanged
回調中直接修改 text 造成遞迴調用 - 確保事件傳播順序正確
2. 為什麼要手動呼叫 sendActions(for: .editingChanged)
?
當我們程式化清空 textField.text
時,不會自動觸發 .editingChanged
事件,需要手動通知其他監聽者(如 Combine publisher、RxSwift observer)。
總結
這個問題的解決過程告訴我們:
- 深入理解事件順序:鍵盤通知並不是解決所有輸入問題的萬能鑰匙
- 從源頭攔截:直接監聽
.editingChanged
比依賴系統通知更可靠 - 舊設備測試的重要性:效能較差的設備更容易暴露時序問題
- 快速連擊測試:模擬真實使用者的操作習慣,而不是理想狀態下的單次點擊
希望這個解決方案能幫助到遇到類似問題的開發者!如果你有更好的做法,歡迎在下方留言討論。
標籤:iOS開發 / UITextField / 鍵盤處理 / Bug修復 / Swift
留言