Программирование
Пишем свой дебаунсер на Swift
Дебаунсинг — это небольшой паттерн с огромными преимуществами в плане UX и производительности.
Представьте, что вы набираете текст в строке поиска, и с каждым нажатием клавиши ваше приложение отправляет API-запрос. Это пять обращений — и ваш бэкэнд просто умрет.
Дебаунсинг — это распространенная техника, используемая для отсрочки выполнения функции до тех пор, пока не пройдет определенное время, в течение которого она не будет вызвана снова.
Это особенно полезно в следующих случаях:
- Поиске: избегайте запуска сетевого запроса при каждом нажатии клавишь
- Автосохранение формы: дождитесь, пока пользователь перестанет печатать, прежде чем сохранять форму
- Действия в пользовательском интерфейсе: предотвращение случайных многократных нажатий.
Зачем создавать свой собственный дебаунсинг
Конечно, такие фреймворки, как Combine или RxSwift, предлагают встроенные дебаунсеры. Но создание собственного дает вам:
- Полный контроль (привет кастомизация!)
- Совместимость с UIKit/нереактивными кодовыми базами
- Более глубокое понимание параллелизма, очередей и памяти
Это также отличный вопрос для собеседований про Swift.
Основная реализация
Вот чистая, потокобезопасная реализация дебаунсера в Swift:
class Debouncer { let delay: TimeInterval let queue: DispatchQueue var workItem: DispatchWorkItem? let syncQueue = DispatchQueue(label: "com.debouncer.sync") init(delay: TimeInterval, queue: DispatchQueue = DispatchQueue.main) { self.delay = delay self.queue = queue } func cancelTask() { workItem?.cancel() workItem = nil } func debounce(_ action: @escaping () -> Void) { let newWorkItem = DispatchWorkItem { [weak self] in action() self?.cancelTask() } syncQueue.sync { cancelTask() workItem = newWorkItem } queue.asyncAfter(deadline: .now() + delay, execute: newWorkItem) } }
Что здесь происходит?
Давайте увеличим масштаб:
debounce(_:)
: ключевой метод. Каждый раз, когда вы его вызываете, предыдущая задача отменяется, и назначается новая.DispatchWorkItem
: думайте об этом как об «отменяемом замыкании». Мы планируем его выполнение после некоторой задержки.syncQueue
: последовательная очередь, которая обеспечивает одновременное изменениеWorkItem
только одним потоком — это позволяет избежать состояния гонки в многопоточных средах.[weak self]
: избегает retain циклов. Всегда хорошая идея в отложенных замыканиях.
Это легкая, элегантная и уважающая очередь, которую вы предоставляете (по умолчанию это main), реализация
Проверено и надежно
Хорошие инженеры пишут тесты. Отличные инженеры еще тестируют и параллелизм.
import XCTest class DebouncerTests: XCTestCase { let debouncer = Debouncer(delay: 0.1) func test_task_completion() { let expectation = XCTestExpectation(description: "Task should complete") var message = "Task started" debouncer.debounce { message = "Task completed" expectation.fulfill() } XCTAssertEqual(message, "Task started") wait(for: [expectation], timeout: 1) XCTAssertEqual(message, "Task completed") } func test_repeated_task() { let expectation = XCTestExpectation(description: "Only the last task should complete") expectation.expectedFulfillmentCount = 1 var message = "Task started" for i in 0..<5 { debouncer.debounce { message = "Task \(i+1) completed" expectation.fulfill() } } XCTAssertEqual(message, "Task started") wait(for: [expectation], timeout: 1) XCTAssertEqual(message, "Task 5 completed") } func test_cancelled_task() { let expectation = XCTestExpectation(description: "Task cancelled") var message = "Task started" debouncer.debounce { message = "Task Completed" } debouncer.cancelTask() DispatchQueue.main.asyncAfter(deadline: .now() + 2) { message = "Task Cancelled" expectation.fulfill() } wait(for: [expectation], timeout: 3) XCTAssertEqual(message, "Task Cancelled") XCTAssertNotEqual(message, "Task Completed") } func test_thread_safety() { let expectation = XCTestExpectation(description: "Test thread safety") let debouncer = Debouncer(delay: 0.1) DispatchQueue.global().async { for _ in 0..<10 { debouncer.debounce { expectation.fulfill() } } } wait(for: [expectation], timeout: 2) } }
Эти тесты гарантируют:
- Что задача выполняется только после задержки
- Что выполняется только последний вызов дебаунсера
- Что отмена предотвращает выполнение
- Безопасность потоков под нагрузкой (протестировано с помощью очереди .
global()
)
Пример реального использования
Давайте используем дебаунсер для ввода пользователя в UITextField:
let debouncer = Debouncer(delay: 0.5) @objc func textFieldDidChange(_ textField: UITextField) { debouncer.debounce { self.performSearch(with: textField.text) } }
Пользователь набирает «S», «Sw», «Swi», «Swif», «Swift» — после паузы в 0.5 секунды запускается только один поиск.
Заключительные мысли
Дебаунсинг — это небольшой паттерн с огромными преимуществами в плане UX и производительности.
Если вы создадите свой собственный дебаунсер, то это:
- Углубит ваше понимание GCD, замыканий и безопасности памяти
- Даст вам многоразовую утилиту на Swift для любого проекта
Если вы нашли эту статью полезной, не стесняйтесь поделиться ею с другими. Счастливого кодинга!