Валидация полей ввода — едва ли не самая распространенная задача в мобильных приложениях. Каждое приложение которое имеет форму авторизации и регистрации, также имеет некоторое количество средств ввода информации со стороны пользователя, которые порождают вполне ожидаемый садистско — изощренный ажиотаж тестировщиков. Продвинутая, технически-грамотная общность разработчиков научилась эффективно с ними с ним бороться путем встраивания регулярных выражений в исходный код.
Как правило, когда нужно ограничится регистрацией и авторизацией, не требуется прилагать особые усилия для достижения поставленной цели — код переносится из одного проекта в другой, практически не претерпевая изменений. И, что особо важно, без увеличения человеко-часов на дальнейшее сопровождение этого кода. Но мир не был бы столь прост, если бы не творческие порывы UI/UX дизайнеров, которые склонны вопреки устоявшейся традиции, логике и здравому смыслу изобретать новые способы взаимодействия с конечным пользователем, размещая на форме несколько, на их взгляд, необходимых элементов управления, доступность которых зависит от декартового множества условий проверки валидности большого количества полей ввода и других управляющих контролов. К сожалению, и эту ситуацию, вряд ли можно назвать редкой.
Раздражение разработчика нарастает пропорционально тому, как часто ему приходится нарушать DRY принципы: с одной стороны профессиональная гордость не позволяет идти на компромиссы, а с другой — копипаст кода — наиболее эффективный способ избежать длительного цикла тестирования — отладки. Однообразный скопипасщенный код значительно проще поддерживать, чем, идеологически выверенный уникальный «велосипед». Поиск же альтернативы не только растрачивает время творчества разработчика, но и добавляет зависимости в проект.
Вместе с тем, iOS SDK представляет некоторые крайне недооцененные возможности, которые легко масштабируются на множество задач связанных не только с валидацией — декларативное программирование сильно облегчает жизнь разработчика. Конечно же, автору известен непримиримый станлюбителей друзей использования «неразбавленного» кода, но, поскольку профессиональная деятельность по верстке интерфейсов у автора статьи начиналась с разработки графического UI еще для MS DOS, то, большого желания тратить время на создание очередного совершенного класса — не возникает, и если при равной ценности можно использовать мышь — предпочтение будет отдано мыши. Соответственно, здесь изложено как минимизировать количество кода, чтоб ускорить и упростить процесс разработки.
Типичная минимальная задача выглядит следующим образом:
Имеется поля ввода логина и пароля, и кнопка авторизации. Необходимо чтоб кнопка авторизации меняла состояние (isEnable) в зависимости от того, что содержится в полях ввода.
Несколько более расширенная версия этой задачи выглядит так:
Имеем поля ввода email, password и phone number, а так же две кнопки — регистрация и запрос SMS кода на введенный телефон. Кнопка регистрации должна быть доступна только когда введены правильные данные в каждое из полей. А кнопка запроса кода — когда валидно поле номера телефона.
Типичное решение — создание взаимозависимых флагов путем комбинации операторов «if» и «switch» в одном вью-контроллере. Сложность будет нарастать с увеличением количества контролов вовлеченных в процесс. Значительно более продвинутым решением будет создание машины состояний. Решение отличное — но трудоемкое, к тому же, имеющее высокий порог вхождение — а это, отнюдь не то, что хочется реализовать ленивому (АКА «истинному») разработчику.
Общая идея предлагаемого состоит в следующем:
Существует класс для поля ввода и класс менеджера валидации. Поле ввода унаследован, как и ожидается, от UITextField. Менеджер валидации — наследуется от UIControl, содержит в себе коллекцию валидируемых элементов (совершенно не обязательно это должны быть потомки UITextField) и реализует паттерн «наблюдатель». Кроме того, он выступает менеджером для других элементов управления, которые должны менять свой статус доступности при изменении состояния валидируемых элементов. Другими словами, если поле содержит невалидный email, кнопка регистрации должна быть недоступна.
Поля ввода самовалидируемые — они могут ответить на вопрос валидные ли содержащиеся в них данные, и изменить свое состояние. Некоторые разработчики предпочитают использовать отдельный класс валидатора (и его потомки) для валидации полей ввода, но такая практика лишает возможности в полной мере насладится декларативностью и масштабируемостью переносимого кода. Для нас важно, чтоб поле ввода или любой другой валидируемый контрол поддерживало простой интерфейс вида:
Для добавления валидации в Ваш проект необходимо добавить 4 файла из примера размещенного в репозитории GitHub.
Строго говоря, если Вы захотите реализовать аналогичным образом валидацию для любого другого элемента управления, то, скорее всего, Вам потребуется только DSValidationManager. Остальное используется только ради Вашего удобства.
Процесс валидации показан на видео (gif).
Как видно, перед началом ввода поле находится в дефолтном состоянии, а валидация происходит по мере ввода данных. Если покинуть поле до завершения валидации, над полем появляется надпись, уведомляющая об ошибке. Кнопка регистрации будет недоступна до тех пор, пока оба поля не станут валидными.
Аналогичная ситуация складывается и на втором экране (который становится доступным только после активации регистрации). Но на втором экране валидация полей и кнопок происходит независимо — кнопка регистрации становится доступной только если все поля провалидизировались. В то же время кнопка отправки SMS доступна уже тогда, когда валидно поле с номером телефона.
А теперь фокус: Во всем проекте содержится только один класс для вью-контроллера, тот самый, который был создан XCode при создании проекта из шаблона. Он совершенно пустой. Но даже он не используется. В этом не сложно убедиться при помощи диспетчера.
Внимательный читатель заметил, что справа от респондера находится Объект из стандартной палитры компонентов XCode. Для него параметр Class переопределен классом DSValidationManager. Такой же трюк проделан и для полей ввода, с той лишь разницей, что там используется класс DSTextField.
Теперь все наше программирование сводится к следующим простым действиям:
Код DSValidationManager до безобразия примитивен.
Как видно, он получает уведомление от одной коллекции, и воздействует на другую.
Наблюдателем его делает свойство, которое мы даже не рассматривали в этом примере. Вы можете перетащить связь с объекта ValidationManager прямо в контроллер, добавить там событие, аналогичное нажатию на кнопку, и обрабатывать состояние менеджера непосредственно во-вью контроллере. Если очень этого хотите. Это бывает полезно, в том случае, когда следует не просто заблокировать элемент управления, а еще выполнить какую-нибудь полезную работу (к примеру, сыграть марш или показать AlertView).
Из всего кода TextEdit следует отметить только то, что поле должно позаботится о том, чтоб правильно выставить свойство «isValid», и дернуть метода «verified()» у своего делегата. Однако, обратите внимание, что в поле ввода используется не ссылка на объект менеджера, а коллекция. Это позволяет привязать поле ввода к нескольким менеджерам.
Соответственно, метод нужно будет дернуть у каждого из делегатов.
Теперь, если Вам потребуется сверстать новую форму с полями ввода, Вам даже не нужно будет выставлять кастомные значения свойств — достаточно будет скопировать нужные Runtime аттрибуты скопом и применить их к новому полю (не забудьте только заменить название класса).
Вообще говоря, трюк с использованием «объекта» в заголовке формы можно использовать значительно шире, чем просто для валидации полей — он c реактивной скоростью превращает MVC в MVVM без какого-либо Rx. Наиболее частое его применение — реализации сетевого обмена и уведомлений об изменениях в модели CoreData.
Как правило, когда нужно ограничится регистрацией и авторизацией, не требуется прилагать особые усилия для достижения поставленной цели — код переносится из одного проекта в другой, практически не претерпевая изменений. И, что особо важно, без увеличения человеко-часов на дальнейшее сопровождение этого кода. Но мир не был бы столь прост, если бы не творческие порывы UI/UX дизайнеров, которые склонны вопреки устоявшейся традиции, логике и здравому смыслу изобретать новые способы взаимодействия с конечным пользователем, размещая на форме несколько, на их взгляд, необходимых элементов управления, доступность которых зависит от декартового множества условий проверки валидности большого количества полей ввода и других управляющих контролов. К сожалению, и эту ситуацию, вряд ли можно назвать редкой.
Раздражение разработчика нарастает пропорционально тому, как часто ему приходится нарушать DRY принципы: с одной стороны профессиональная гордость не позволяет идти на компромиссы, а с другой — копипаст кода — наиболее эффективный способ избежать длительного цикла тестирования — отладки. Однообразный скопипасщенный код значительно проще поддерживать, чем, идеологически выверенный уникальный «велосипед». Поиск же альтернативы не только растрачивает время творчества разработчика, но и добавляет зависимости в проект.
Вместе с тем, iOS SDK представляет некоторые крайне недооцененные возможности, которые легко масштабируются на множество задач связанных не только с валидацией — декларативное программирование сильно облегчает жизнь разработчика. Конечно же, автору известен непримиримый стан
Дисклеймер:
Чтоб проиллюстрировать излагаемый механизм, будет создан минималистический проект. В реальном проекте используемые классы несколько более насыщенные и покрывают больший функционал. Доработка их для Ваших приложений не составит труда, зато уменьшение количество кода — повысит ясность излагаемой идеи.
Типичная минимальная задача выглядит следующим образом:
Имеется поля ввода логина и пароля, и кнопка авторизации. Необходимо чтоб кнопка авторизации меняла состояние (isEnable) в зависимости от того, что содержится в полях ввода.
Несколько более расширенная версия этой задачи выглядит так:
Имеем поля ввода email, password и phone number, а так же две кнопки — регистрация и запрос SMS кода на введенный телефон. Кнопка регистрации должна быть доступна только когда введены правильные данные в каждое из полей. А кнопка запроса кода — когда валидно поле номера телефона.
Типичное решение — создание взаимозависимых флагов путем комбинации операторов «if» и «switch» в одном вью-контроллере. Сложность будет нарастать с увеличением количества контролов вовлеченных в процесс. Значительно более продвинутым решением будет создание машины состояний. Решение отличное — но трудоемкое, к тому же, имеющее высокий порог вхождение — а это, отнюдь не то, что хочется реализовать ленивому (АКА «истинному») разработчику.
Лема о ленивом разработчике
Мы знаем, что разработчик должен быть ленив, потому что всю трудоемкую и повторяющуюся работу должен выполнять компьютер. Если разработчик любит выполнять много работы («по кругу» ), то это уже спорт или билдинг, но не девелопмент.
Общая идея предлагаемого состоит в следующем:
Существует класс для поля ввода и класс менеджера валидации. Поле ввода унаследован, как и ожидается, от UITextField. Менеджер валидации — наследуется от UIControl, содержит в себе коллекцию валидируемых элементов (совершенно не обязательно это должны быть потомки UITextField) и реализует паттерн «наблюдатель». Кроме того, он выступает менеджером для других элементов управления, которые должны менять свой статус доступности при изменении состояния валидируемых элементов. Другими словами, если поле содержит невалидный email, кнопка регистрации должна быть недоступна.
Поля ввода самовалидируемые — они могут ответить на вопрос валидные ли содержащиеся в них данные, и изменить свое состояние. Некоторые разработчики предпочитают использовать отдельный класс валидатора (и его потомки) для валидации полей ввода, но такая практика лишает возможности в полной мере насладится декларативностью и масштабируемостью переносимого кода. Для нас важно, чтоб поле ввода или любой другой валидируемый контрол поддерживало простой интерфейс вида:
@objc protocol IValidatable : class {
varisValid: Bool { get }
}
Для добавления валидации в Ваш проект необходимо добавить 4 файла из примера размещенного в репозитории GitHub.
- Класс DSTextField реализует поле ввода с индикацией процесса валидации.
- Класс DSValidationManager представляет собой наблюдателя, который и обеспечивает нужную нам функциональность.
- Расширение StringOptional+Utils — содержит всего один метод, который использует регулярное выражение для проверки того, является ли текст текущей строки валидным.
- UIView+Layer — просто удобный способ добавить рамочку заданной ширины к любому потомку UIView.
Строго говоря, если Вы захотите реализовать аналогичным образом валидацию для любого другого элемента управления, то, скорее всего, Вам потребуется только DSValidationManager. Остальное используется только ради Вашего удобства.
Процесс валидации показан на видео (gif).
Как видно, перед началом ввода поле находится в дефолтном состоянии, а валидация происходит по мере ввода данных. Если покинуть поле до завершения валидации, над полем появляется надпись, уведомляющая об ошибке. Кнопка регистрации будет недоступна до тех пор, пока оба поля не станут валидными.
Аналогичная ситуация складывается и на втором экране (который становится доступным только после активации регистрации). Но на втором экране валидация полей и кнопок происходит независимо — кнопка регистрации становится доступной только если все поля провалидизировались. В то же время кнопка отправки SMS доступна уже тогда, когда валидно поле с номером телефона.
А теперь фокус: Во всем проекте содержится только один класс для вью-контроллера, тот самый, который был создан XCode при создании проекта из шаблона. Он совершенно пустой. Но даже он не используется. В этом не сложно убедиться при помощи диспетчера.
Внимательный читатель заметил, что справа от респондера находится Объект из стандартной палитры компонентов XCode. Для него параметр Class переопределен классом DSValidationManager. Такой же трюк проделан и для полей ввода, с той лишь разницей, что там используется класс DSTextField.
Теперь все наше программирование сводится к следующим простым действиям:
- Связать коллекцию verifiedControls из ValidationManager с полями ввода.
- Связать коллекцию managedControls из ValidationManager с кнопкой, которой нужно управлять.
- Связать поля ввода в обратную сторону с ValidationManager, сделав его делегатом.
- В поле ввода задать регулярное выражение для валидации и сообщение об ошибке, а так же, другие кастомные и стандартные свойства через стандартный диспетчер XCode. В принципе, все кроме регулярного выражения можно оставить «как есть». Оно единственное требует задействовать клавиатуру, и то, только для того, чтоб скопипастить формулу из тырнета. Если сообщение об ошибке отсутствует, то оно просто не будет показано на экране.
Код DSValidationManager до безобразия примитивен.
import UIKit
@objc protocol IValidationManager : class {
func verificated()
}
@objc protocol IValidatable : class {
var isValid: Bool { get }
}
class DSValidationManager : UIControl, IValidationManager
{
@IBOutlet var verifiedControls: [UIControl]?
@IBOutlet var managedControls: [UIControl]?
private (set) var valid : Bool = false {
didSet {
self.sendActions(for: .valueChanged)
}
}
overrideinit(frame: CGRect) {
super.init(frame: frame)
self.verificated()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.verificated()
}
func verificated() {
self.checkVerifiedControls()
self.controlsAction()
}
private func checkVerifiedControls() {
guard let list:[IValidatable] = self.verifiedControls?.filter({$0 isIValidatable}) as? [IValidatable]
else { return }
self.valid = list.filter({!$0.isValid}).count==0
}
private func controls Action() {
guard let list = self.managedControls else { return }
for item in list {
item.isEnabled = self.valid
}
}
}
Как видно, он получает уведомление от одной коллекции, и воздействует на другую.
Наблюдателем его делает свойство, которое мы даже не рассматривали в этом примере. Вы можете перетащить связь с объекта ValidationManager прямо в контроллер, добавить там событие, аналогичное нажатию на кнопку, и обрабатывать состояние менеджера непосредственно во-вью контроллере. Если очень этого хотите. Это бывает полезно, в том случае, когда следует не просто заблокировать элемент управления, а еще выполнить какую-нибудь полезную работу (к примеру, сыграть марш или показать AlertView).
Из всего кода TextEdit следует отметить только то, что поле должно позаботится о том, чтоб правильно выставить свойство «isValid», и дернуть метода «verified()» у своего делегата. Однако, обратите внимание, что в поле ввода используется не ссылка на объект менеджера, а коллекция. Это позволяет привязать поле ввода к нескольким менеджерам.
Соответственно, метод нужно будет дернуть у каждого из делегатов.
var isValid: Bool {
return self.text.verification(self.expression, required:self.valueRequired)
}
@IBOutlet var validateDelegates: [IValidationManager]?
private func handleDelegateAction(_ sender: UITextField) {
guard let list = self.validateDelegates else { return }
for validateDelegate in list {
validateDelegate.verificated()
}
}
Теперь, если Вам потребуется сверстать новую форму с полями ввода, Вам даже не нужно будет выставлять кастомные значения свойств — достаточно будет скопировать нужные Runtime аттрибуты скопом и применить их к новому полю (не забудьте только заменить название класса).
Вообще говоря, трюк с использованием «объекта» в заголовке формы можно использовать значительно шире, чем просто для валидации полей — он c реактивной скоростью превращает MVC в MVVM без какого-либо Rx. Наиболее частое его применение — реализации сетевого обмена и уведомлений об изменениях в модели CoreData.