跳到主要內容

iOS TextField 快速點擊導致誤觸鍵盤輸入的解決方案


問題描述

在開發 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,再去阻擋已經來不及了。


✅ 最終解決方案

關鍵突破點是:不要等鍵盤通知,直接在輸入發生的瞬間進行攔截!

核心思路

  1. 監聽 .editingChanged 事件:直接監聽 TextField 的編輯變化
  2. 即時攔截:在輸入的瞬間檢查鍵盤狀態,不符合條件就立即清空
  3. 鍵盤狀態追蹤:維護一個 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)
    }
}

工作流程

  1. 使用者在鍵盤未升起時快速點擊輸入框
  2. 任何輸入都會觸發 publisher(for: .editingChanged)
  3. 檢查 !isKeyboardVisible → 立即清空 textField.text
  4. 手動觸發 sendActions(for: .editingChanged) → 通知其他 publisher
  5. 直到 keyboardDidShow 被調用,isKeyboardVisible 才變為 true
  6. 之後的輸入可以正常保留

方案優勢

即時過濾:在鍵盤升起前的任何輸入都會被立即清空
配合底層:完全使用 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)。


總結

這個問題的解決過程告訴我們:

  1. 深入理解事件順序:鍵盤通知並不是解決所有輸入問題的萬能鑰匙
  2. 從源頭攔截:直接監聽 .editingChanged 比依賴系統通知更可靠
  3. 舊設備測試的重要性:效能較差的設備更容易暴露時序問題
  4. 快速連擊測試:模擬真實使用者的操作習慣,而不是理想狀態下的單次點擊

希望這個解決方案能幫助到遇到類似問題的開發者!如果你有更好的做法,歡迎在下方留言討論。


標籤:iOS開發 / UITextField / 鍵盤處理 / Bug修復 / Swift

留言