跳到主要內容

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

留言

這個網誌中的熱門文章

勝券在握

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

10/17部會心得

其實部會一開完,我就想寫了,只是最近沉迷於小說,直到今天才有開始動工的心情。 業務的報告部份就不要講了,一點都不感興趣,講的好像會大賣的樣子,薪水有增加再說啦! 技術方面, DQ 做的功能好像挺有意思的,不過感覺上沒人看出他的價值,不過我覺得最佳的 UI 還是 vs 2005 為主,如果要研發基底的 UI 我覺得要以類 VS 為目標。金融網的東西也令人興奮,我本來就覺得網頁的東西潛力無窮,雖然是抄的,不過覺得還不錯,網頁互抄比 AP 互抄容易多了,很多 AP 的功能要模擬出來,真的比想像中難太多了。雖然金融網聽到最後我睡著了,但我非常期待他下一版能帶來什麼。好吧!說一下我們這邊技術心得,看起來炫,但是我一點都不心動,因為我想不到這東西能增加我們產品的價值嗎? 這場部會最棒的是最後副總的報告,這是第一次瞭解整個產業環境與我們的應對方法,這樣我才清楚為什麼要成立那麼多非資訊本業部門,金融網成立的原因何在,產業鏈垂直整合的價值在哪裡。但是會像如規劃中的成功嗎?其實從旁觀察規劃與現在實際的運作,我抱的期望沒那麼大,不過這是有意思的夢,就看能不能實現囉! 不過在會中,我就想到一個我覺得可以配合上我們的產業架構,而且還不錯的商業模式,這也是我想寫這篇心得的原因。 在副總的報告中,提到金融網是為了提供一個入門的金融資訊,接觸最底層不付費的又想看金融資訊的民眾,其實我想到更進階的是接觸所有上網的人。而這功能結合 43thing 與財務規劃(參考 到底要賺多少錢才能退休呢? )你要作這些你想要做的事情(如遊學、旅遊)或退休,你到底要賺多少錢,然後開始洗腦光靠上班的死薪水是達不成這些目的的,所以要開始投資,投資如何規劃,就是要買我們的金融產品,一步一步引君入彀。這功能主要的對象就不只是想投資的人,而是所有的人,只要你心中有任何的夢想,而這夢想需要錢來達成,就需要這個工具。這就是把基底做的更大,個體經濟學常用的互補效應。所以這功能包含紀錄你要達成的夢想,財務管理包含薪資增加目標、稅務、相關的投資規劃與風險等。目標就是吸引所有人來理財、投資。 另一方面,我想倒也有一些人就是他沒有時間也沒有能力去學如何投資或操作股票,他們只想跟老師聽名牌,那這個就可以結合 blog 的功能來作,吸引一些老師來 blog 建立個人投資說明,開發一個跟單工具,這跟單工具可以比較所...

Slim Diray 3/6

飲食:早餐/火腿蛋吐司三明治+350g優酪乳;午餐/三杯雞飯;晚餐/鴨肉飯+500g巧克力牛奶。晚餐7點半前吃。 運動:無 體重: -10.6 公斤 (與前日輕0.4kg) 71.4 公斤 就寢時間:2:00 檢討:今天沒做運動,ㄟ到輕了0.4kg,吃的量跟昨天差不多,今天反而輕了,經驗談便是盤整不需理由,靜待突破反壓。 原本想低調,不想寫聯誼的。不過這場聯誼出乎意料的好玩,所以記錄一下。因為身為活動組組長的我,本來沒有啥動力安排什麼遊戲,剛開始覺得奇怪,Jim幹嘛安排在大魯閣,這樣聯誼要怎樣進行??果然自我介紹完,就換代幣去玩棒壘球了,而棒壘球,是一個人專心再打,大家就無聊的等位置,又Victor手傷沒辦法打,在一旁也不知道幹嘛。後來大家幾乎各自散開找位置打球或完投準,第一次提領的20枚代幣用完後,Jim就提議上去二樓玩,原來二樓也是用代幣玩的,終於感覺代幣沒有換太多,幾個小遊戲還蠻好玩的,中間我覺得最適合的聯誼遊戲是打桌球,裡面是投兩枚代幣,可以玩30分鐘,重點是兩個人在打,其他人就可以聊天,聯誼到那時刻終於有聊天的機會了,其實遊戲玩開後,聊天就比較自然一點,玩了兩輪桌球,也就是一小時,該聊的也都聊了吧,最後是打撞球,分隊打,大家技術都很爛,最後幾乎全靠自由球清檯。 而這場聯誼Jim找他的發光點了,對嘛!桌球系隊,不秀桌球的絕技,就浪費了,打球中間還教了靜郁幾手。對了!欣怡也是系隊的,我想他們很適合私底下切磋球技,哈!我有馥菁化的趨勢喔!相對今天我就比較低調了,多天的訓練似乎沒啥用,講話沒啥笑點,沒啥梗可以用,對話也不容易接下去,一整個遜掉了,而我自己對話,有很大的問題,眼神會亂飄,都是對話完畢,我才驚覺自己這個毛病又犯了,這樣感覺好像沒誠意。今天被講年記看起比較小,可是我每次照鏡子都覺得自己很糙老的,還說我娃娃臉!既然被這樣稱讚了,當然要回說妳看起來也比我小啊!嗯!沒玩配對遊戲,幾個之前預訂的聯誼獎勵,就不知道怎麼送了,我還是自肥好了。看大家有沒有機會發展下去,好像大家都無疾而終,只有我會主動再連絡,但是每個女的都連絡,又被講花心,嚇!我又沒對人家怎樣!奇怪!