Программирование состояний в UIControl

от автора

Основная проблема, с которой сталкивается программист при реализации какого-то управляющего элемента — выстраивание правильной логики работы этого элемента.

Исследование проблемы

Как определено в документации UIControl — это класс, реализующий общее поведение для визуальных элементов, которые способны реагировать определенным способом на действия пользователя. А значит, менять визуальное представление, поведение, инициировать процессы и т.д. Что же для этого нужно иметь и как это реализовать? На первый вопрос есть очевидный ответ — состояния, и логика переходов между ними. Со вторым вопросом немного посложнее…

Решение проблемы

Многие простодушные разработчики, заканчивают тем, что создают метод, который обычно называется update() и пишут в нем эпопею в сослагательном наклонении, проще говоря:

if ... {     element1.property = value1     ... } else if ... {     element1.property = value2     ... } ...

Это еще куда не шло, код последовательный и читаемый. Но, если обезьяне попадается какая-то модная граната, все заканчивается еще плачевней:

   RAC(element1, hidden) = [RACSignal combineLatest:@[                                                     self.textField1.rac_textSignal                                                     ] reduce:^(NSString *password) {                                                         return @((!password.length >= 1));                                                     }];     RAC(element2, hidden) = [RACSignal combineLatest:@[                                                     self.textField1.rac_textSignal                                                     ] reduce:^(NSString *password) {                                                         return @(!(password.length >= 2));                                                     }];     RAC(element3, hidden) = [RACSignal combineLatest:@[                                                     self.textField1.rac_textSignal                                                     ] reduce:^(NSString *password) {                                                         return @(!(password.length >= 3));                                                     }];     RAC(element4, hidden) = [RACSignal combineLatest:@[                                                     self.textField1.rac_textSignal                                                     ] reduce:^(NSString *password) {                                                         if(password.length == PIN_LENGTH) {                                                             [self activateNextField];                                                             return @(NO);                                                         }                                                         else return @(YES);                                                     }]; 

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

Решение из коробки

UIControl и соответственно его наследники, используют следующий механизм обновления состояния:

Первое, что делает метод обновления — считывает текущее состояние, а конкретнее свойство UIControlState state. Оно является битовой маской из единиц состояний, описанных в enum UIControlState. Важно заметить, и как многие ошибочно делают, что это свойство должно быть рассчитываемым, а не хранимым. Т.е. реальное состояние объекта должно формировать описание этого состояния, а не наоборот.

Далее, из контейнера выдергиваются значения, ассоциированные с полученным состоянием и применяются.

Процесс актуализации состояния инициируется после изменения фактора состояния. Например, в boolean переменной:

open var isEnabled: Bool {    didSet {        if oldValue != isEnabled {            // вызов метода обновления        }    } }

UIControlState имеет зарезервированную часть битовой маски для создания дополнительных состояний — UIControlStateApplication. Если вы хотите добавить состояние для любого системного control`а, то вы можете выбрать любое значение из этого интервала.

extension UIControlState {     static let custom = UIControlState(rawValue: 1 << 16) }  let button = UIButton(type: .custom) let title = "Title for custom state" button.setTitle(title, for: .custom)      button.title(for: .custom) == title // true

Но почему-то разработчики Apple, предоставив нам возможность создавать свои состояния, не предоставили API, чтобы ими управлять.

Мой случай

Моя задача состояла в том, чтобы реализовать control для ввода пин-кода. Задача достаточно тривиальная, поэтому пытаешься её усложнить.

Учитывая вышеописанную проблему, я и решил написать то, что Apple не задекларировала в публичный интерфейс, и может быть немного больше)

Так я создал класс надстройку над UIControl — QUIckControl. Он предоставляет возможность устанавливать значения для определенного состояния (или множества состояний) для конкретного объекта.

func setValue(_ value: Any?, forTarget: NSObject, forKeyPath: String, for: QUICState)

Как видно из семантики метода, в основе лежит KVC. Проблема валидации ключей в swift 3 уже решена, а в ObjC легко решается добавлением define macros.

Перед установкой значения для пользовательского состояния, это состояние нужно зарегистрировать используя метод:

func register(_ state: UIControlState, forBoolKeyPath keyPath: String, inverted: Bool)

Если ваш control вошел в состояние для которого вы не устанавливали значений, то будет применено дефолтное значение. Дефолтное значение определяется в момент первой установки значения для конкретного ключа. Формально вы можете его переопределить используя состояние .normal в режиме частичного соответствия(cм. ниже), т.к. .normal содержится абсолютно в любом состоянии.

Для того, чтобы упростить настройку состояний и не дублировать значения, была создана структура-описание состояния QUICState. Сейчас она содержит 6 режимов оценки соответствия текущему состоянию:

  • режим полного соответствия
  • режим частичного соответствия
  • режим несоответствия
  • режим соответствия хотя бы одной единицы состояния
  • режим полного несоответствия
  • режим определенный пользователем

Каждый режим имеет свой приоритет, для определения первостепенного значения в случае множественного соответствия.

Так как актуализация состояния происходит сразу после изменения фактора состояния(boolean переменной), создана возможность осуществления множественных переходов, без моментального применения изменений:

func beginTransition() // запуск процесса перехода func endTransition() // закрытие процесса перехода без применения func commitTransition() // закрытие процесса перехода с применением func performTransition(withCommit commit: Bool = default, transition: () -> Void) // блок обертка для методов выше

Вывод

Данное API позволяет достаточно быстро настраивать состояния и создавать зависимости между control`ами и не только:

control.setValue(true, forTarget: otherControl, forKeyPath: "enabled", forAllStatesContained: [.filled, .valid])

Что например, является частым use case`ом для форм ввода.

В результате я получил, то что хотел, но еще с элементами реактивщины. Автоматное программирование достаточно неудобное, но при грамотном подходе достаточно надежное. Не зря этот стиль программирования находит применение от игр и контроллеров до всевозможных анализаторов и ИИ.

→ Реализацию PinCodeControl и весь код можно посмотреть здесь.
ссылка на оригинал статьи https://habrahabr.ru/post/316646/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *