在雲端 Mac 測試 iOS 輸入法組字與表單驗證

在雲端 Mac 測試 iOS 輸入法組字與表單驗證

使用者在雲端 Mac 的 iOS 模擬器中輸入中文姓名:拼音還沒選字,表單就顯示「只能填寫中文」;才剛選定候選字詞,送出按鈕又短暫變灰。問題通常不在正規表示式,而是程式把輸入法組字中的文字當成最終送出的值。要重現並修正這個問題,必須區分欄位的「組字中」與「已確認」狀態,不能只看 text 屬性。

先釐清驗證的是哪個狀態

中文拼音、日文假名轉換與韓文組字,都會經過候選字詞或組字階段。對遵循 UITextInput 的 UITextField 而言,markedTextRange 不為空值,表示目前有標記文字。此時介面可以顯示使用者正在輸入的內容,但不應據此回報格式錯誤、開放送出,或將其寫入最終狀態。

收到輸入事件,不代表輸入已完成。組字中的文字仍屬於編輯過程;業務驗證應針對確認後的值。

先為欄位訂好規則:組字期間清除先前的錯誤並暫停格式驗證;組字結束後,依目前的 text 重新驗證;使用者按下送出時,再確認組字已結束並複核一次。最後一步不能省,否則候選字詞尚未確認,使用者仍可能透過其他操作觸發送出。是否允許空值、姓名是否必須為漢字,都是業務規則,應與組字狀態的判斷分開處理。

在欄位事件中延後判斷

以下物件只負責決定何時呼叫驗證函式。範例規則要求欄位非空,且只包含漢字;實際專案應替換 isValid,不要將姓名規則直接套用到電子郵件或地址欄位。

import UIKit

@MainActor
final class NameFieldValidator {
    private(set) var message: String?
    private(set) var canSubmit = false

    func editingDidChange(_ field: UITextField) {
        guard field.markedTextRange == nil else {
            message = nil
            canSubmit = false
            return
        }
        evaluate(field.text ?? "")
    }

    func submit(_ field: UITextField) -> Bool {
        guard field.markedTextRange == nil else {
            canSubmit = false
            return false
        }
        evaluate(field.text ?? "")
        return canSubmit
    }

    private func evaluate(_ value: String) {
        canSubmit = !value.isEmpty &&
            value.unicodeScalars.allSatisfy {
                (0x3400...0x9FFF).contains(Int($0.value))
            }
        message = canSubmit ? nil : "请填写中文姓名"
    }
}

在檢視控制器中,將 .editingChanged 接到 editingDidChange(_:),再依 message 與 canSubmit 更新提示及按鈕。標記文字變動時會不會觸發事件,取決於實際的輸入路徑;不要假設「選字完成」一定剛好對應一次 .editingChanged。編輯結束及進入送出流程時,也應重新讀取欄位狀態。程式修改 text 後,同樣要主動更新介面,因為直接賦值不等於使用者鍵入。範例中的碼點範圍僅用來示範流程,並未涵蓋所有姓名用字、擴充漢字或間隔符號。

用單元測試固定狀態轉換

在應用程式的單元測試目標中建立真正的 UITextField,測試組字期間不能送出、確認後才依最終文字判斷。UIKit 物件應在主執行緒使用;這段測試不依賴螢幕鍵盤。

import XCTest
import UIKit

@MainActor
final class NameFieldValidatorTests: XCTestCase {
    func testMarkedTextIsNotSubmitted() {
        let field = UITextField()
        let validator = NameFieldValidator()

        field.setMarkedText("zhong", selectedRange: NSRange(location: 5, length: 0))
        validator.editingDidChange(field)

        XCTAssertNotNil(field.markedTextRange)
        XCTAssertFalse(validator.canSubmit)
        XCTAssertFalse(validator.submit(field))

        field.setMarkedText("中文", selectedRange: NSRange(location: 2, length: 0))
        field.unmarkText()
        validator.editingDidChange(field)

        XCTAssertNil(field.markedTextRange)
        XCTAssertEqual(field.text, "中文")
        XCTAssertTrue(validator.submit(field))
    }
}

讓測試目標包含受測型別,接著在雲端 Mac 上執行 Xcode 的 Test 動作。接入 CI 時,先在 Xcode 中共享 scheme,再用 xcodebuild -showdestinations -scheme <方案名> -project <工程名>.xcodeproj 找出可用的模擬器;將選定的 platform=iOS Simulator,id=<设备ID> 傳給 xcodebuild test。裝置 ID 應從目前使用的機器讀取,不要把另一台機器的識別碼寫進腳本。

這項測試驗證的是驗證器對欄位狀態的反應,不是系統鍵盤會以什麼順序送出事件。如果 markedTextRange 的斷言失敗,先確認測試欄位支援標記文字,再檢查呼叫是否在主執行緒執行;不要為了讓測試通過,就刪掉組字狀態的斷言。

在模擬器核對實際輸入流程

單元測試通過後,透過雲端 Mac 的圖形介面開啟目標 iOS 模擬器,在系統設定中啟用目標輸入法,並確認目前鍵盤確實已切換至該輸入法。關閉會把輸入直接當作實體鍵盤文字傳送的設定,再點入姓名欄位操作:

  1. 輸入拼音但不選候選字詞,觀察錯誤提示與送出按鈕;兩者都不應把尚未確認的文字視為有效姓名。
  2. 選取漢字候選字詞,檢查提示、按鈕與欄位值是否一致;再刪掉一個字,確認驗證結果會隨最終值更新。
  3. 在組字期間透過鍵盤返回鍵或其他送出入口嘗試送出,確認送出層也會拒絕,而不只是按鈕看起來無法點擊。
  4. 切換目標系統版本及輸入法,重複檢查。如果自動化腳本只貼上現成的漢字,就會跳過最重要的組字過程。

可以暫時記錄 .editingChanged、編輯結束及進入送出流程時的狀態,排查「已經選字卻沒有重新驗證」是否因事件遺漏所致。除錯輸出只用於本機排查,不要將使用者輸入內容寫入持久化日誌。

讓各層負責各自的邊界

實際表單還可能透過自動填寫、貼上、語音輸入或程式回填取得內容。這些路徑不一定會產生標記文字,卻同樣可能改變欄位值,因此最終送出時必須獨立複核。反過來說,也不要因 markedTextRange 不為空值,就丟棄正在編輯的文字:只需暫停可能造成誤判的業務驗證,保留正常的輸入顯示。

驗收時要分別確認三件事:候選字詞出現前有沒有誤報、確認後有沒有及時更新,以及繞過按鈕直接送出時能否通過檢查。分開測試這三項,才不會讓雲端 Mac 上重現的輸入問題,被一張「能打出漢字」的截圖掩蓋。

常見問題

為何輸入法組字時不應立即驗證欄位?

markedTextRange 存在時,選字仍可能改變欄位內容。應待組字結束後驗證確認的值,送出時再驗證一次。

XCTest 能取代模擬器的鍵盤驗收嗎?

不能。XCTest 可固定驗證規則;候選字選取及鍵盤事件順序仍須在目標 iOS 版本的模擬器確認。

在專屬實體節點上開始作業

為下一項任務設定雲端 Mac

依照模型所需記憶體或建置負載選擇 Mac mini。三種機型支援按日、週、月或季租用;節點供應狀態以控制台即時顯示為準。

立即租用雲端 Mac