Програмування станів у 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 в режимі часткового відповідності(див. нижче), т. к. .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 і весь код можна подивитися тут.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.