Программирование
Пишем свой дебаунсер на 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 для любого проекта
Если вы нашли эту статью полезной, не стесняйтесь поделиться ею с другими. Счастливого кодинга!
-
Аналитика магазинов2 недели назад
Мобильный рынок Ближнего Востока: исследование Bidease и Sensor Tower выявляет драйверы роста
-
Интегрированные среды разработки3 недели назад
Chad: The Brainrot IDE — дикая среда разработки с играми и развлечениями
-
Новости4 недели назад
Видео и подкасты о мобильной разработке 2025.45
-
Новости3 недели назад
Видео и подкасты о мобильной разработке 2025.46

