跳到主要內容

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

留言

這個網誌中的熱門文章

解決 CI Trust Issue:Target Must Be Enabled Before It Can Be Used

📱 iOS開發 | 🔧 CI/CD | 💻 Xcode | 🐛 除錯筆記 🔴 問題描述 這兩天在跑 CI 時突然出現錯誤訊息: Package@swift-6.0.swift:PACKAGE-TARGET:CasePathsMacros: error: Target 'CasePathsMacros' must be enabled before it can be used 🤔 嘗試過的解法 💬 Claude 的建議 首先詢問了 Claude,得到以下步驟: 先更新 swift-case-paths 到最新版本 確保使用 "Up to Next Major Version" 執行 File → Packages → Reset Package Caches Clean Build Folder (Cmd + Shift + K) 重新 Build 結果: 一看就知道沒用 😅 🤖 ChatGPT 的建議 接著試了 ChatGPT 的解法,主要是降低引用到的 package 版本。繞了一圈,還是沒用。 ✅ 最終解決方案 最後還是回到 Google,找到了真正有效的解法。針對這個 macro fingerprint validation 問題,有三種解決方式: 📌 方法一:本機開發用(Terminal 指令) defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES 📌 方法二:xcodebuild 參數 在執行 xcodebuild 指令時,加上 -skipMacroValidation 參數 📚 參考連結: https://vocus.cc/article/690779ebfd89780001859b14 📌 方法三:CI 正統做法 ⭐️(推薦) 步驟 1: 在專案根目錄建立資料夾 ci_scripts 步驟 2: 在此資料夾中建立腳本 ci_post_clone.sh ,內容如下: #!/bin/zsh mkdir -p ~/Library/org.swift.swiftpm/security/ cp macros.js...

用 AI Debug 的迷思:當建議越改越糟時

現在許多開發者習慣用 AI 來協助 debug,但在實務上常遇到一種情況: 依照 AI 建議改了兩三輪後,錯誤仍然存在,甚至越改越複雜。 這種狀況其實有幾個常見的盲點,值得特別注意。 1. 先回到「上一個正常版本」 當你已經按照 AI 的方向修了好幾次但問題仍未解決時,最有效的第一步是: 回到上一個正常工作的版本,縮小問題來源。 許多 bug 並不是你正在看的那段程式碼造成的,而可能是: 同事剛好修改了某個底層模組 某個 shared component 產生 side effect Auto Layout 層級重新 layout 時觸發 crash 如果只是盯著眼前的 function 修,反而容易被誤導。 2. AI 沒有看到你的整個專案 AI 通常只能根據你貼出的片段判斷問題,這代表它不知道: 你的 view hierarchy 裡是否有其他 constraint 影響 layout 某些 model 是否被 extension 修改過 父層或子層邏輯是否干擾目前的行為 整個專案採用的 concurrency 模型是什麼 因此,AI 可能會朝著完全錯誤的方向修,導致反覆修改卻無法解決。 3. Swift 6 例子:錯誤真正原因常不在你修改的那一行 例如開發者常遇到的錯誤: passing closure as a 'sending' parameter risks causing data races 許多人(包含 AI)會開始從 function 內部調整,但這類錯誤真正的關鍵通常是: 傳進去的物件沒有實作 Sendable。 也就是說,你不是要改 function,而是要回頭檢查: 傳入的 model / struct / class 裡面是否有 non-Sendable 成員 是否需要標註 @unchecked Sendable 如果 AI 沒看到相關檔案,自然很難找到正確方向。 結語:AI 是工具,不是預言機 AI 很適合用來: 解釋概念 協助產生測試程式 提供重構建議 釐清你已懷疑的方向 但在 debug 狀況下,以下三件事更重要: 回到上一版,縮小差異範圍。 釐清真正問題來...

勝券在握

其實這本書,感覺上寫的有點雜,比上一本講巴非特的書更難懂,兩個講的東西其實是一致的。投資原則便是先選產業,再選公司,慎選時機進場。只買了解的企業是價值投資一貫的原則。價值投資的書大概就先看到這裡了,彼得林區不知道是屬於那一類的,接下來大概會看這部份的書。暫時的目標是把杜金龍介紹的書單看完,真的還不少。接下來的投資會以巴菲特的方法來做,感覺上這比較適合我,練習把漲跌不當一回事,對我而言真的很重要。期權大概不會再玩了,買了以後一直在看漲跌,令人受不了。工作時都不能專心。 就價值投資人而言,真的不需要我們的產品,因為第一點就把我們程式特性打死,不理會股票市場的漲跌,這樣報價功能就沒什麼意義了,價值投資根本不需要技術分析,除非我們能提供相關價值投資的資訊,但我們基本分析真的很爛,看不到什麼資料。有機會我來思考一下價值投資到底要什麼資料,能不能把他寫成一個可運用的程式。 以下是我認為重要的書摘,其實這也只包含最後一章,我認為也只有這章值得做書摘。 巴非特相信使用短期價格來判斷一家公司的成功與否是愚蠢的。取而代之的是,他要公司向他報告因經濟實力成長所獲得的價值,一年一次,他固定檢查幾個變數: 初始的股東權益報酬率。 營運毛利、負債水準與資本支出需求的變化。 該公司的現金產生能力。 如果這些經濟指標正在進展,他知道長期下來,結果會反應在股價上。短期之內,股價所發生的是是不合常理的。 投資策略 不理會股票市場每日的漲跌 不擔心經濟情勢。 買下一家公司,而不是股票 管理企業的投資組合 巴非特原則 企業原則 這家企業是簡單且可以了解的 了解一家企業如何產生利潤的相關經濟活動。 這家企業的營運歷史是否穩定 他必須經得起時間的考驗。 這家企業的長期發展前景是否看好 市場特許權,五力分析 經營原則 經營者是否理性 理性的經營者將只會把多餘的現金,投資在那些產生較資本成本報酬率為高的計畫裡。 經營者對他的股東是誠實坦白的 報告時能知道營業部門如何營業,坦承失敗,了解公司的目的是使股東權益報酬率達到最大。 經營者是會盲從其他法人機構的行為 當心『其他公司也這麼做,一定沒問題』為自己行為辯護的經營者。衡量經營者競爭力的一個方法是,看他們如何運用自己的思考能力以避免依附群眾心理。 財務原則 把重點集中...