A user enters a Chinese name in an iOS Simulator on a cloud Mac. Before they have selected characters from the pinyin candidates, the form says “Chinese characters only.” Just after they select a candidate, the submit button briefly becomes disabled. The problem usually is not a bad regular expression: the app is treating text still being composed by the IME as the final value. To reproduce and fix it, distinguish between text being composed and text that has been committed instead of watching only the text property.
Define What Validation Should Check
Chinese pinyin, Japanese kana conversion, and Korean syllable composition all involve a candidate or composition stage. For a UITextField that uses UITextInput, a non-nil markedTextRange means marked text is present. The UI can show what the user is typing, but it should not report a format error, enable submission, or store that text as the final value yet.
An input event does not mean input is complete. Text under composition is part of editing; business validation should run against the committed value.
Establish a contract for the field: during composition, clear any previous error and pause format validation; when composition ends, validate the current text again; on submission, check that composition has ended and validate once more. That last check matters because the user may trigger submission another way before confirming a candidate. Whether an empty value is allowed and whether a name must contain only Chinese characters are business rules separate from the composition check.
Defer Validation in Field Events
The object below decides only when to call the validation function. The example requires a nonempty value containing only Chinese characters. In a real project, replace isValid with the appropriate rule; do not apply a name rule unchanged to an email or address field.
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 : "请填写中文姓名"
}
}
In the view controller, connect .editingChanged to editingDidChange(_:), then update the message and button from message and canSubmit. Whether changes to marked text trigger an event depends on the input path. Do not assume that selecting a candidate always produces exactly one .editingChanged event. Read the field state again when editing ends and at the submission entry point. Also refresh the UI after changing text programmatically: direct assignment is not the same as user typing. The code point range in this example demonstrates the flow only; it does not cover every character used in names, extended Chinese characters, or separators.
Lock Down State Transitions with a Unit Test
Create a real UITextField in the app’s unit test target. Check that submission is blocked during composition and that, after confirmation, the decision uses the final text. UIKit objects should be used on the main thread; this test does not depend on the on-screen keyboard.
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))
}
}
Make sure the test target includes the type under test, then run Xcode’s Test action on the cloud Mac. For CI, first share the scheme in Xcode, then find an available simulator with xcodebuild -showdestinations -scheme <方案名> -project <工程名>.xcodeproj. Pass the selected platform=iOS Simulator,id=<设备ID> to xcodebuild test. Read the device ID from the current machine; do not hard-code an identifier from another machine into the script.
This test verifies how the validator responds to field state, not the order in which the system keyboard sends events. If the markedTextRange assertion fails, first confirm that the test field supports marked text, then check that the call runs on the main thread. Do not remove the composition-state assertion just to make the test pass.
Verify the Real Keyboard Path in the Simulator
After the unit test passes, open the target iOS Simulator through the cloud Mac’s graphical interface. Enable the target input method in system settings and confirm that the current keyboard has actually switched to it. Disable the setting that sends input directly as physical-keyboard text, then tap the name field:
- Type pinyin without selecting a candidate. Check the error message and submit button; neither should treat uncommitted text as a valid name.
- Select a Chinese-character candidate and check that the message, button, and field value agree. Delete one character and confirm that validation updates based on the final value.
- During composition, try submitting with the keyboard’s Return key or another submission path. Confirm that the submission layer rejects it, rather than relying only on a button that looks disabled.
- Repeat the checks with the target system versions and input methods. An automation script that only pastes ready-made Chinese characters skips the most important part: composition.
You can temporarily log state at .editingChanged, the end of editing, and the submission entry point to find missed events when validation does not rerun after candidate selection. Keep this debug output local to troubleshooting; do not write user input to persistent logs.
Keep the Boundaries in the Right Layer
A real form may also receive autofill, pasted text, dictation, and programmatic updates. These paths can change the field value without producing marked text, so final submission must perform its own check. Conversely, a non-nil markedTextRange is no reason to discard text being edited. Pause only the business validation that could produce a false result, and keep displaying the input normally.
For acceptance, check three things separately: whether a false error appears before candidate selection, whether the UI updates promptly after confirmation, and whether submission paths that bypass the button still enforce the check. Testing each one prevents a screenshot showing that Chinese characters can be typed on a cloud Mac from hiding the actual input bug.
Frequently asked questions
When should an iOS form validate text entered through an IME?
Wait while markedTextRange is non-nil. Validate the resulting field value after composition ends, and validate once more on submission.
Can an XCTest unit test replace a Simulator keyboard check?
No. A unit test covers the validation rule; candidate selection and keyboard event ordering still need a check in Simulator with the target iOS version.
Choose a cloud Mac for your next task
Choose a Mac mini based on memory or build workload. Three models are available to rent by the day, week, month, or quarter. Check real-time node availability in the console.