Ваш QA любит нажимать кнопки? Вот решение.
Мы все знакомы с ситуацией - мы реализовали величайшую новую функцию, которую когда-либо видели в нашем приложении. Он доходит до QA, и первое, что они делают, - это быстро нажимают кнопку, которая нажимает следующий UIViewController пять раз подряд.

Механические переключатели обычно работают с использованием двух металлических контактов, которые при нажатии переключателя входят в контакт друг с другом, замыкая цепь. Эти контакты обычно изготавливаются из упругого металла, который может отскакивать друг от друга один или несколько раз, прежде чем вступить в окончательный контакт друг с другом. Этот эффект может привести к возникновению нескольких переходов от состояния низкого к высокому уровню мощности, которые в логической схеме потенциально могут регистрироваться как множественные нажатия кнопки. Чтобы предотвратить это, состояние сигнала может быть дискретизировано с низкой частотой до тех пор, пока не будет достоверно сказано, что изменение состояния произошло. Этот процесс известен как устранение ошибок.
Этот термин используется, в частности, в сообществах JavaScript и реактивного программирования для обозначения процесса наблюдения за потоком последовательных событий до тех пор, пока не будет достигнуто устойчивое состояние, в который отправляется событие, представляющее это устойчивое состояние (то есть конечное состояние). Если бы эту идею применили к потоку нажатий кнопок, то событие нажатия кнопки было бы отправлено только в какой-то момент после того, как произошло последнее нажатие кнопки. Это обычно может быть реализовано путем введения задержки в выдаче события касания кнопки, так что событие генерируется только в том случае, если в течение периода задержки не было произведено никаких последующих нажатий кнопки.
На него часто ссылаются в связи с регулировкой, потому что эти две операции в чем-то похожи по своей природе. Оба включают выборку потока событий - с дросселированием нас интересует выдача выборочной версии (частота выборки или задержка обычно передается как параметр функции дросселирования) исходного потока событий. Это может быть реализовано путем выдачи первого или последнего события касания кнопки каждый период задержки, например каждые 0,5 секунды. С debouncing нас интересует выборка исходного потока событий и выдача события только после достижения устойчивого состояния, то есть после того, как значение no кажется изменяющимся.
Примером того, где это может быть полезно, является реализация поисковых предложений. Мы могли бы генерировать чрезмерный трафик в API, если бы мы должны были отправлять запрос в API поисковых предложений каждый раз, когда пользователь вводит новую букву в текстовое поле, однако, если бы мы должны были регулировать события потока, мы могли бы только отправить запрос в API каждые пару секунд, а не при каждом нажатии на букву. Если бы мы хотели реализовать предварительную загрузку результатов поиска, чтобы результаты появлялись без необходимости нажимать кнопку «Готово» или ее эквивалент, мы могли бы отклонить поток входных событий таким образом, чтобы мы отправляли запрос в API поиска через некоторое время после того, как перестанем получать новый ввод.
Что касается противодействия потоку нажатий кнопок, не имеет смысла ждать, пока не будет выполнено последнее нажатие кнопки, чтобы отправить событие нажатия кнопки, поскольку это приведет к тому, что пользовательский интерфейс не будет отвечать на запросы. В этом случае принятие первого нажатия кнопки и затем игнорирование последующих нажатий кнопки позволяет пользовательскому интерфейсу оставаться отзывчивым, не вызывая потенциально неправильного поведения программы из-за многократного выполнения исходного кода обработчика нажатия кнопки.
Мы можем довольно просто реализовать такое поведение в UIButton в Swift с помощью расширения UIControl следующим образом:
public extension UIControl {
@objc static var debounceDelay: Double = 0.5
@objc func debounce(delay: Double = UIControl.debounceDelay, siblings: [UIControl] = []) {
let buttons = [self] + siblings
buttons.forEach { $0.isEnabled = false }
let deadline = DispatchTime.now() + delay
DispatchQueue.main.asyncAfter(deadline: deadline) {
buttons.forEach { $0.isEnabled = true }
}
}
}
В приведенном выше примере кода мы просто устанавливаем свойство кнопки isEnabled на false на период 0,5 секунды после первого нажатия, а затем снова устанавливаем его на true по истечении периода задержки. Мы помещаем расширение в UIControl, а не в UIButton, поскольку свойство isEnabled фактически определено в UIControl, а не в UIButton, и позволяет использовать код в более широком смысле. Мы также устанавливаем глобальную задержку 0,5 с, так что если эта функция вызывается в функции обработчика кнопки, то все последующие нажатия кнопки будут игнорироваться в течение этого периода по умолчанию - однако, если мы хотим переопределить эту задержку для каждой кнопки, тогда мы можем передать переопределенное значение задержки в функцию debounce в качестве параметра.
И последнее, на что следует обратить внимание: мы также разрешаем передавать массив одноуровневых UIControls в качестве параметра функции на тот случай, если мы хотим временно заблокировать другие элементы управления одновременно с исходной кнопкой. Например, предположим, что у нас есть две кнопки A и B, при этом нажатие кнопки A приводит к нажатию UIViewController A, а нажатие кнопки B приводит к нажатию UIViewController B. Если бы мы быстро нажали кнопку A, а затем кнопку B (что могло бы произойти, если задержка в нажатии UIViewController A произошла из-за задержки, вызванной плохим уровнем сигнала и логической ошибкой), при этом нажимая только кнопку A, то в лучшем случае мы бы оказались в ситуация, при которой будет отправлен UIViewController A, а затем UIViewController B. В худшем случае такой сценарий может привести к сбою приложения. Поэтому мы разрешаем одновременное устранение проблем с родственными элементами управления, чтобы избежать сценариев, в которых обработчики кнопок для отдельных кнопок, вызываемые одновременно, могут привести к непреднамеренному взаимодействию.
Фреймворки функционального реактивного программирования (FRP), такие как RxSwift, часто предоставляют реализации функций устранения неполадок и дросселирования, включая структуру Apple Объединение, представленную в iOS 13. Объединенная реализация устранения отклонений генерирует событие после того, как входной поток достиг стабильного уровня. состояние, которое было бы именно тем, что нам нужно в приведенном выше примере, включающем выполнение запроса API для результатов поиска после ввода данных в текстовое поле.
func debounce<S>(for dueTime: S.SchedulerTimeType.Stride, scheduler: S, options: S.SchedulerOptions? = nil) -> Publishers.Debounce<AnyPublisher<Output, Failure>, S> where S : Scheduler
Однако с точки зрения противодействия нажатию кнопки использование этой функции не было бы идеально подходящим, поскольку, как упоминалось выше, будет задержка между нажатием кнопки и реакцией пользовательского интерфейса на нажатие.
Вышеприведенное расширение Swift гораздо больше соответствует реализации дроссельной заслонки комбайна, которая может генерировать либо первое, либо последнее событие в пределах указанного временного интервала. Он принимает параметр latest, который, если установлен в false, вызовет генерацию первого события в пределах временного интервала, что кажется более соответствующим нашему желаемому поведению, позволяя кнопке и пользовательскому интерфейсу оставаться отзывчивыми, при этом игнорируя последующие нажатия кнопок.
func throttle<S>(for interval: S.SchedulerTimeType.Stride, scheduler: S, latest: Bool) -> Publishers.Throttle<PassthroughSubject<Output, Failure>, S> where S : Scheduler
Резюме
Применяя концепцию, преобладающую в веб-разработке (в частности, JavaScript) и реактивном программировании, используя простое расширение Swift или функцию библиотеки FRP, мы можем защититься от непредвиденных последствий в нашем приложении и, надеюсь, в результате увидим меньше заявок, возвращаемых от QA. 😎
Расширение для UIControl можно найти в открытом доступе на GitHub под лицензией MIT вместе с образцом приложения, чтобы вы могли его опробовать. Обратите внимание, что каждое нажатие кнопки в примере приложения не приводит к печати сообщения Кнопка нажата!. Вместо этого из-за того, что нажатие кнопок не дребезжит, текст печатается не чаще одного раза в 0,5 секунды.