在云端 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 能完全代替模拟器上的输入法验收吗?

不能。单元测试能固定组合态和提交态的校验契约;候选栏、键盘事件顺序与真实交互仍应在目标系统版本的模拟器上人工核对。

在独享物理节点上动手

为下一项任务配置云端 Mac

按模型内存或构建负载选择 Mac mini。三档机型支持按天、周、月、季租用;节点可用状态以控制台实时返回为准。

立即租用云端 Mac