Пользователь вводит китайское имя в iOS Simulator на облачном Mac. Пока он набирает пиньинь и ещё не выбрал иероглифы, форма уже сообщает: «Можно вводить только китайские иероглифы». После выбора кандидата кнопка отправки на мгновение становится неактивной. Обычно дело не в ошибке регулярного выражения: приложение принимает текст в процессе композиции IME за окончательное значение. Чтобы воспроизвести и исправить проблему, нужно различать два состояния поля — «ввод ещё составляется» и «ввод подтверждён», а не ориентироваться только на свойство text.
Сначала определите, какое значение проверять
Ввод китайского пиньинь, преобразование японской каны и набор корейских слогов проходят через стадию композиции или выбора кандидата. У UITextField, работающего через UITextInput, непустой 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))
}
}
Убедитесь, что тестовая цель включает проверяемый тип, затем запустите действие Test в Xcode на облачном Mac. Для CI сначала сделайте схему общей в Xcode, затем найдите доступный симулятор командой xcodebuild -showdestinations -scheme <方案名> -project <工程名>.xcodeproj. Передайте выбранный platform=iOS Simulator,id=<设备ID> команде xcodebuild test. Считывайте ID устройства на текущей машине, а не записывайте в скрипт идентификатор с другого компьютера.
Этот тест проверяет реакцию валидатора на состояние поля, а не порядок событий системной клавиатуры. Если проверка markedTextRange не проходит, сначала убедитесь, что тестовое поле поддерживает помеченный текст, а затем проверьте, выполняется ли вызов в главном потоке. Не удаляйте проверку состояния композиции только ради того, чтобы тест стал успешным.
Проверьте реальный ввод в симуляторе
После успешного модульного теста откройте нужный iOS Simulator через графический интерфейс облачного Mac. В системных настройках включите целевой метод ввода и убедитесь, что выбрана именно его клавиатура. Отключите настройку, при которой ввод напрямую передаётся как текст с физической клавиатуры, затем откройте поле имени и выполните следующие действия:
- Введите пиньинь, но не выбирайте кандидата. Проверьте сообщение об ошибке и кнопку отправки: неподтверждённый текст не должен считаться корректным именем.
- Выберите кандидата с иероглифами и проверьте, согласованы ли подсказка, состояние кнопки и значение поля. Затем удалите один иероглиф и убедитесь, что проверка обновляется по окончательному значению.
- Во время композиции попробуйте отправить форму клавишей возврата или другим способом. Отправка должна отклоняться на уровне обработки действия, а не только выглядеть недоступной из-за состояния кнопки.
- Повторите проверку с нужными версиями системы и методами ввода. Если автоматизированный сценарий только вставляет готовые иероглифы, он пропускает самую важную стадию — композицию.
Для поиска пропущенного события, из-за которого после выбора кандидата проверка не запускается повторно, можно временно записывать состояние при .editingChanged, завершении редактирования и попытке отправки. Используйте такой отладочный вывод только локально; не сохраняйте ввод пользователя в постоянных журналах.
Разделяйте ответственность правильно
В реальной форме значение поля может измениться из-за автозаполнения, вставки, голосового ввода или программного заполнения. Эти действия не обязательно создают помеченный текст, поэтому при окончательной отправке значение нужно проверять независимо от предыдущих событий. И наоборот, непустой markedTextRange — не повод отбрасывать вводимый текст. Приостанавливайте только бизнес-валидацию, которая могла бы дать ложный результат, сохраняя обычное отображение ввода.
При приёмке проверяйте три вещи по отдельности: не возникает ли ложная ошибка до выбора кандидата, своевременно ли обновляется состояние после подтверждения и проходит ли проверку отправка в обход кнопки. Тогда проблему ввода, воспроизведённую на облачном Mac, не скроет скриншот, на котором всего лишь удалось напечатать иероглифы.
Часто задаваемые вопросы
Зачем откладывать проверку поля при вводе IME?
Пока существует markedTextRange, текст может измениться после выбора кандидата. Проверяйте окончательное значение только после завершения композиции.
Заменяет ли XCTest ручную проверку клавиатуры?
Нет. XCTest проверяет логику поля, а порядок событий клавиатуры и выбор кандидата нужно отдельно проверить в симуляторе нужной версии iOS.
Настройте облачный Mac для следующей задачи
Выберите Mac mini по объёму памяти или нагрузке сборки. Доступны три конфигурации и аренда на день, неделю, месяц или квартал. Актуальный статус узла отображается в панели управления.