Connect with us

Программирование

Пишем свой дебаунсер на Swift

Дебаунсинг — это небольшой паттерн с огромными преимуществами в плане UX и производительности.

Опубликовано

/

     
     

Представьте, что вы набираете текст в строке поиска, и с каждым нажатием клавиши ваше приложение отправляет API-запрос. Это пять обращений — и ваш бэкэнд просто умрет.

Дебаунсинг — это распространенная техника, используемая для отсрочки выполнения функции до тех пор, пока не пройдет определенное время, в течение которого она не будет вызвана снова.

Это особенно полезно в следующих случаях:

  1. Поиске: избегайте запуска сетевого запроса при каждом нажатии клавишь
  2. Автосохранение формы: дождитесь, пока пользователь перестанет печатать, прежде чем сохранять форму
  3. Действия в пользовательском интерфейсе: предотвращение случайных многократных нажатий.

Зачем создавать свой собственный дебаунсинг

Конечно, такие фреймворки, как 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 для любого проекта

Если вы нашли эту статью полезной, не стесняйтесь поделиться ею с другими. Счастливого кодинга!

Источник

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.
Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: