16 дек. 2025 г.·6 мин

Нативная валидация форм в SwiftUI: фокус и ошибки

Нативная валидация форм в SwiftUI: управляйте фокусом, показывайте встроенные ошибки вовремя и отображайте серверные сообщения понятно, не раздражая пользователей.

Нативная валидация форм в SwiftUI: фокус и ошибки

Как выглядит «нативная» валидация в SwiftUI

Нативная форма iOS спокойна. Она не спорит с пользователем во время ввода. Она даёт понятную обратную связь в нужный момент и не заставляет искать, что пошло не так.

Главное ожидаемое свойство — предсказуемость. Одни и те же действия должны давать одинаковый тип отклика каждый раз. Если поле неверно, форма должна показать это в консистентном месте, с одинаковым тоном и с понятным следующим шагом.

Большинству форм нужны три типа правил:

  • Правила поля: корректно ли само значение (пусто, формат, длина)?
  • Кросс‑проверки: зависят ли значения друг от друга (Password и Confirm Password)?
  • Правила сервера: принимает ли это бэкенд (email уже используется, требуется приглашение)?

Тайминг важнее хитрых формулировок. Хорошая валидация ждёт подходящего момента и говорит один раз, ясно. Практичный ритм выглядит так:

  • Молчать, пока пользователь печатает, особенно для правил формата.
  • Показать отклик после ухода из поля или после нажатия Submit.
  • Держать ошибки видимыми, пока их не исправят, и сразу убирать после исправления.

Валидация должна оставаться тихой, пока пользователь ещё формирует ответ, например вводит email или пароль. Показывать ошибку на первом символе ощущается как придирка, даже если технически это верно.

Валидация должна становиться видимой, когда пользователь сигнализирует, что закончил: фокус уходит или он пытается отправить форму. В этот момент они хотят подсказки — и вы можете помочь им попасть ровно в то поле, которое требует внимания.

Правильно поставленный тайминг упрощает всё остальное. Встроенные сообщения остаются короткими, движения фокуса помогают, а серверные ошибки воспринимаются как обычная обратная связь, а не наказание.

Постройте простую модель состояния валидации

Нативная форма начинается с чёткого разделения: текст, который ввёл пользователь, — не то же самое, что мнение приложения об этом тексте. Если смешивать их, вы либо будете показывать ошибки слишком рано, либо терять серверные сообщения при обновлении UI.

Простой подход — дать каждому полю собственное состояние из четырёх частей: текущее значение, взаимодействовал ли пользователь, локальная (на устройстве) ошибка и серверная ошибка (если есть). Тогда UI решает, что показывать, исходя из «touched» и «submitted», а не реагируя на каждый символ.

struct FieldState {
    var value: String = ""
    var touched: Bool = false
    var localError: String? = nil
    var serverError: String? = nil

    // One source of truth for what the UI displays
    func displayedError(submitted: Bool) -> String? {
        guard touched || submitted else { return nil }
        return localError ?? serverError
    }
}

struct FormState {
    var submitted: Bool = false
    var email = FieldState()
    var password = FieldState()
}

Несколько простых правил сохраняют предсказуемость:

  • Разделяйте локальные и серверные ошибки. Локальные правила (например «обязательно» или «некорректный email») не должны перезаписывать серверное сообщение вроде «email уже занят».
  • Очищайте serverError, когда пользователь снова редактирует поле, чтобы он не застрял, уставившись на старое сообщение.
  • Устанавливайте touched = true только когда пользователь уходит из поля (или когда вы считаете, что он пытался взаимодействовать), а не при вводе первого символа.

С этой моделью представление может свободно привязываться к value. Валидация обновляет localError, а слой API — serverError, не мешая друг другу.

Обработка фокуса, которая направляет, а не пилит

Хорошая валидация в SwiftUI должна создавать ощущение, будто системная клавиатура помогает пользователю завершить задачу, а не ругает его. Фокус — большая часть этого.

Простой паттерн — использовать фокус как единственный источник правды через @FocusState. Определите enum для полей, привяжите к нему каждое поле и двигайтесь вперёд, когда пользователь нажимает кнопку на клавиатуре.

enum Field: Hashable { case email, password, confirm }

@FocusState private var focused: Field?

TextField("Email", text: $email)
  .textContentType(.emailAddress)
  .keyboardType(.emailAddress)
  .textInputAutocapitalization(.never)
  .submitLabel(.next)
  .focused($focused, equals: .email)
  .onSubmit { focused = .password }

SecureField("Password", text: $password)
  .submitLabel(.next)
  .focused($focused, equals: .password)
  .onSubmit { focused = .confirm }

Что делает это нативным — умеренность. Перемещайте фокус только по явным действиям пользователя: Next, Done или основная кнопка. При отправке устанавливайте фокус на первое неверное поле (и прокручивайте к нему при необходимости). Не «воруйте» фокус, пока пользователь ещё печатает, даже если текущее значение сейчас неверно. Также соблюдайте последовательность в метках клавиш: Next для промежуточных полей, Done для последнего.

Частый сценарий — регистрация. Пользователь нажимает Create Account. Вы валидируете один раз, показываете ошибки, затем ставите фокус на первое невалидное поле (обычно Email). Если он находится в поле Password и всё ещё печатает, не возвращайте его в Email посреди набора. Эта мелочь часто отличает «полированную iOS‑форму» от «раздражающей формы».

Встроенные ошибки, которые появляются в нужный момент

Встроенные ошибки должны ощущаться как тихая подсказка, а не нравоучение. Главное отличие между «нативно» и «раздражающе» — когда показывается сообщение.

Правила тайминга

Если ошибка показывается в момент, когда кто‑то только начинает печатать, это прерывает. Лучшее правило: подождать, пока у пользователя будет справедливый шанс закончить поле.

Хорошие моменты для показа встроенной ошибки:

  • После ухода из поля
  • После нажатия Submit
  • После короткой паузы при вводе (только для очевидных проверок, вроде формата email)

Надёжный подход — показывать сообщение только когда поле отмечено как touched или была попытка отправки. Свежая форма остаётся спокойной, но пользователь всё равно получит понятное руководство после взаимодействия.

Компоновка и стиль

Ничто не выглядит менее iOS‑подобным, чем изменение раскладки при появлении ошибки. Резервируйте место для сообщения или анимируйте его появление, чтобы оно не отодвинуло следующее поле резко вниз.

Держите текст ошибки коротким и конкретным, с одним действием на сообщение. «Пароль должен быть не короче 8 символов» — это полезно. «Неверный ввод» — нет.

По стилю стремитесь к сдержанности и консистентности. Небольшой шрифт под полем (например, footnote), один постоянный цвет для ошибок и мягкое выделение поля читаются лучше, чем тяжёлые фоны. Очищайте сообщение, как только значение становится корректным.

Реалистичный пример: на форме регистрации не показывайте «Email неверен», пока пользователь всё ещё вводит name@. Покажите это после ухода из поля или после небольшой паузы, и уберите сразу, как адрес станет корректным.

Локальный поток валидации: печать, уход из поля, отправка

Держать правила в одном месте
Используйте визуальные инструменты для определения правил и затем экспортируйте их в веб и мобильные UI.
Создать приложение

Хороший локальный поток имеет три скорости: лёгкие подсказки при вводе, строгие проверки при уходе из поля и полные правила при отправке. Именно этот ритм делает валидацию нативной.

Пока пользователь печатает, поддерживайте валидацию лёгкой и тихой. Думайте «это явно невозможно?» а не «это идеально?». Для поля email вы можете проверять только наличие @ и отсутствие пробелов. Для пароля можно показывать небольшой хелпер вроде «8+ символов» после начала ввода, но избегать красных ошибок на первом символе.

Когда пользователь уходит из поля, запускайте более строгие однополные правила и показывайте встроенные ошибки при необходимости. Здесь уместны «Обязательно» и «Неверный формат». Это также хорошая точка для обрезки пробелов и нормализации ввода (например, приведение email к нижнему регистру), чтобы пользователь видел то, что будет отправлено.

При отправке валидируйте всё заново, включая кросс‑проверки, которые нельзя было решить раньше. Классический пример — совпадение Password и Confirm Password. Если это не проходит, установите фокус на поле, требующее исправления, и покажите одно чёткое сообщение рядом с ним.

Используйте кнопку отправки осторожно. Держите её активной, пока пользователь ещё заполняет форму. Отключайте только тогда, когда нажатие ничего не даст (например, во время фактической отправки). Если вы отключаете её из‑за неверного ввода, всё равно показывайте, что нужно исправить рядом с ней.

Во время отправки показывайте понятный индикатор загрузки. Меняйте текст кнопки на ProgressView, предотвращайте двойные нажатия и держите форму видимой, чтобы пользователь понимал, что происходит. Если запрос длится дольше секунды, короткая метка вроде «Создание аккаунта...» снизит тревогу, не добавляя лишнего шума.

Серверная валидация без фрустрации

Серверные проверки — окончательный источник истины, даже если локальные проверки строгие. Пароль может пройти локальную проверку, но быть отклонён как слишком простой, или email может уже использоваться.

Крупнейший UX‑выигрыш — отделить «ваш ввод недопустим» от «не удалось связаться с сервером». Если запрос таймаутится или пользователь офлайн, не помечайте поля как неверные. Показать спокойный баннер или алерт вроде «Не удалось подключиться. Попробуйте ещё раз.» и оставить форму как есть.

Когда сервер говорит, что валидация провалилась, сохраняйте ввод пользователя и указывайте на конкретные поля. Стирание формы, очистка пароля или перенос фокуса делают пользователя ощущающим наказание за попытку.

Простой паттерн — распарсить структурированный ответ об ошибке в два ведра: field errors и form‑level errors. Затем обновить состояние UI, не меняя привязки текста.

struct ServerValidation: Decodable {
  var fieldErrors: [String: String]
  var formError: String?
}
// Map keys like "email" or "password" to your local field IDs.

Что обычно ощущается как нативное:

  • Помещайте сообщения полей inline, под полем, используя формулировки сервера, если они понятны.
  • Перемещайте фокус к первому полю с ошибкой только после попытки отправки, а не во время ввода.
  • Если сервер возвращает несколько проблем, показывайте по одному сообщению на поле, чтобы было читаемо.
  • Если есть подробности по полю, не сваливайтесь на «Что‑то пошло не так.»

Пример: пользователь отправляет форму регистрации, и сервер возвращает «email уже используется». Сохраните введённый email, покажите сообщение под полем Email и установите фокус на это поле. Если сервер недоступен, покажите единое сообщение с предложением повторить и оставьте все поля нетронутыми.

Как показывать серверные сообщения в нужном месте

Снизить доработки при изменениях
Поддерживайте согласованность валидации клиента и сервера по мере изменения требований и регенерации приложения.
Начать

Серверные ошибки выглядят «несправедливо», когда они появляются в случайном баннере. Размещайте каждое сообщение как можно ближе к полю, которое его вызвало. Общие сообщения используйте только когда их действительно нельзя привязать к конкретному полю.

Начните с преобразования полезной нагрузки ошибок сервера в идентификаторы полей SwiftUI. Бэкенд может вернуть ключи вроде email, password или profile.phone, в то время как ваш UI использует enum Field.email и Field.password. Сделайте маппинг один раз, сразу после ответа, чтобы остальная часть представления оставалась согласованной.

Гибкий способ моделирования — хранить serverFieldErrors: [Field: [String]] и serverFormErrors: [String]. Храните массивы, даже если обычно показываете одно сообщение. При показе inline‑ошибки выбирайте самое полезное сообщение первым. Например, «Email already in use» полезнее, чем «Invalid email», если оба есть.

Несколько ошибок на поле — обычное дело, но показывать их все шумно. Чаще показывайте только первое сообщение inline и храните остальные для детального просмотра, если это действительно нужно.

Для ошибок, не привязанных к полю (истёкшая сессия, лимит запросов, «Попробуйте позже»), размещайте их рядом с кнопкой отправки, чтобы пользователь видел их в момент действия. Также обязательно очищайте старые ошибки при успешной отправке, чтобы UI не выглядел «застрявшим».

Наконец, очищайте серверные ошибки, когда пользователь меняет связанное поле. На практике onChange для email должен удалять serverFieldErrors[.email], чтобы UI сразу показал: «Ок, вы это исправляете.»

Доступность и тон: маленькие решения, которые ощущаются нативными

Сгенерировать приложение и бэкенд
Смоделируйте пользователей и учётные данные в PostgreSQL, затем сгенерируйте приложение и бэкенд вместе.
Начать проект

Хорошая валидация — это не только логика. Это ещё и то, как она читается, звучит и ведёт себя с Dynamic Type, VoiceOver и разными языками.

Делайте ошибки легко читаемыми (и не только цветом)

Предположите, что текст может стать крупным. Используйте стили, поддерживающие Dynamic Type (например, .font(.footnote) или .font(.caption) без фиксированных размеров), и позволяйте меткам ошибок переноситься. Сохраняйте консистентные отступы, чтобы раскладка не прыгала слишком сильно при появлении ошибки.

Не полагайтесь только на красный цвет. Добавьте понятную иконку, префикс «Ошибка:» или оба варианта. Это помогает людям с нарушением цветового восприятия и ускоряет сканирование.

Короткий набор проверок, который обычно выдерживает испытание:

  • Используйте читаемый стиль текста, который масштабируется с Dynamic Type.
  • Разрешайте перенос и избегайте усечения сообщений об ошибках.
  • Добавьте иконку или метку вроде «Ошибка:» вместе с цветом.
  • Держите высокий контраст и в светлой, и в тёмной теме.

Заставьте VoiceOver читать правильное

Когда поле неверно, VoiceOver должен озвучивать метку, текущее значение и ошибку вместе. Если ошибка — отдельный Text под полем, её могут пропустить или прочесть вне контекста.

Два подхода помогают:

  • Объединяйте поле и его ошибку в один accessibility‑элемент, чтобы ошибка объявлялась при фокусе на поле.
  • Устанавливайте accessibility hint или value, включающую сообщение об ошибке (например, «Password, обязательно, минимум 8 символов»).

Тон тоже важен. Пишите сообщения, которые легко локализовать. Избегайте сленга, шуток и расплывчатых фраз вроде «Упс». Предпочитайте конкретные указания: «Email отсутствует» или «Пароль должен содержать цифру».

Пример: форма регистрации с локальными и серверными правилами

Представьте форму регистрации с тремя полями: Email, Password и Confirm Password. Цель — форма, которая молчит во время ввода, а затем становится полезной, когда пользователь пытается двигаться дальше.

Порядок фокуса (что делает Return)

С SwiftUI FocusState каждое нажатие Return должно ощущаться как естественный шаг.

  • Email Return: переводит фокус в Password.
  • Password Return: переводит фокус в Confirm Password.
  • Confirm Password Return: скрывает клавиатуру и пытается отправить форму.
  • Если Submit неудачен: переводите фокус на первое поле, требующее внимания.

Этот последний шаг важен. Если email неверен, фокус возвращается к Email, а не просто к красному сообщению где‑то ещё.

Когда появляются ошибки

Простое правило сохраняет интерфейс спокойным: показывайте сообщения после того, как поле было отмечено (пользователь ушёл из него), или после попытки отправки.

  • Email: показывайте «Введите корректный email» после ухода из поля или при Submit.
  • Password: показывайте правила (минимальная длина и т.д.) после ухода или при Submit.
  • Confirm Password: показывайте «Пароли не совпадают» после ухода или при Submit.

Теперь про сервер. Допустим, пользователь отправляет форму и API возвращает:

{
  "errors": {
    "email": "That email is already in use.",
    "password": "Password is too weak. Try 10+ characters."
  }
}

Что видит пользователь: сообщение сервера под Email и под Password в соответствующих местах. Confirm Password остаётся спокойным, если он прошёл локальные проверки.

Что он делает дальше: фокус ставится на Email (первая серверная ошибка). Он меняет email, нажимает Return чтобы перейти к Password, правит пароль и снова отправляет. Поскольку сообщения inline и фокус двигается осмысленно, форма ощущается сотрудничающей, а не ругающейся.

Частые ошибки, которые делают валидацию «не‑iOS»

Добавить кросс‑проверку полей
Добавляйте проверки полей и кросс‑проверки перетаскиванием бизнес‑логики.
Создать форму

Форма может быть технически правильной и при этом выглядеть неправильно. Большинство «не‑iOS» проблем сводится к таймингу: когда вы показываете ошибку, когда перемещаете фокус и как реагируете на сервер.

Типичная ошибка — говорить слишком рано. Если показывать ошибку на первом символе, люди чувствуют придирку во время ввода. Ожидание, пока поле будет отмечено (уход из него или попытка отправки), обычно решает это.

Асинхронные ответы сервера тоже могут ломать поток. Если запрос возвращается и вы внезапно меняете фокус, это кажется случайным. Держите фокус там, где последний раз был пользователь, и двигайте его только когда он нажал что‑то вроде Next или когда вы обрабатываете попытку отправки.

Ещё одна ловушка — очищать всё при каждом изменении. Удаление всех ошибок при вводе одного символа может скрыть реальную проблему, особенно с серверными сообщениями. Очищайте только ошибку для редактируемого поля и сохраняйте остальные до тех пор, пока их реально не исправят.

Избегайте «молчаливого» заблокированного Submit. Отключение кнопки отправки навсегда без объяснения, что исправить, заставляет пользователя гадать. Если вы отключаете её, сопоставьте это с конкретными подсказками или разрешите отправку, а затем направьте к первой проблеме.

Медленные запросы и дублирующие нажатия легко упустить. Если вы не показываете прогресс и не блокируете двойные нажатия, пользователь нажмёт дважды, получит два ответа и увидит запутанные ошибки.

Быстрая проверка здравого смысла:

  • Откладывайте ошибки до blur или submit, а не до первого символа.
  • Не меняйте фокус после ответа сервера, если пользователь этого не просил.
  • Очищайте ошибки по полю, а не всё сразу.
  • Объясняйте, почему Submit заблокирован (или разрешайте отправку с подсказками).
  • Показывайте загрузку и игнорируйте дополнительные нажатия во время ожидания.

Пример: если сервер говорит «email уже занят» (возможно, от бэкенда, который вы собрали в AppMaster), держите сообщение под Email, не трогайте Password и позвольте пользователю править Email без перезапуска всей формы.

Короткий чек‑лист и следующие шаги

Нативный опыт валидации в основном про тайминг и умеренность. Вы можете иметь строгие правила и при этом сделать экран спокойным.

Перед релизом проверьте:

  • Валидируйте в нужное время. Не показывайте ошибки на первом символе, если это не явно полезно.
  • Перемещайте фокус целенаправленно. При отправке прыгайте к первому невалидному полю и делайте очевидным, что именно там не так.
  • Делайте формулировки короткими и конкретными. Говорите, что делать дальше, а не просто указывайте, что пользователь «ошибся».
  • Уважайте загрузку и повторные попытки. Блокируйте кнопку во время отправки и сохраняйте введённые значения, если запрос неудачен.
  • Рассматривайте серверные ошибки как обратную связь по полю, когда это возможно. Маппьте коды сервера на поле и используйте глобальное сообщение только для реально общих проблем.

Потом протестируйте как реальный человек. Держите небольшой телефон в одной руке и попробуйте заполнить форму большим пальцем. Затем включите VoiceOver и убедитесь, что порядок фокуса, объявления ошибок и метки кнопок по‑прежнему понятны.

Для отладки и поддержки полезно логировать коды серверной валидации (не сырые сообщения) вместе с экраном и именем поля. Когда пользователь говорит «не получается зарегистрироваться», вы быстро поймёте, был ли это email_taken, weak_password или сетевой таймаут.

Чтобы сохранить согласованность в приложении, стандартизируйте модель поля (value, touched, local error, server error), расположение ошибок и правила фокуса. Если вы хотите быстрее строить нативные iOS‑формы без ручного кодирования каждого экрана, AppMaster (appmaster.io) может генерировать SwiftUI‑приложения вместе с бэкенд‑сервисами, что облегчает синхронизацию правил валидации клиента и сервера.

Легко начать
Создай что-то невероятное

Экспериментируйте с AppMaster с бесплатной подпиской.
Как только вы будете готовы, вы сможете выбрать подходящий платный план.

Попробовать AppMaster
Нативная валидация форм в SwiftUI: фокус и ошибки | AppMaster