Site icon AppTractor

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

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

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

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

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

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

Конечно, такие фреймворки, как Combine или RxSwift, предлагают встроенные дебаунсеры. Но создание собственного дает вам:

Это также отличный вопрос для собеседований про 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)
    }
}

Что здесь происходит?

Давайте увеличим масштаб:

Это легкая, элегантная и уважающая очередь, которую вы предоставляете (по умолчанию это 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)
    }
}

Эти тесты гарантируют:

Пример реального использования

Давайте используем дебаунсер для ввода пользователя в 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 и производительности.

Если вы создадите свой собственный дебаунсер, то это:

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

Источник

Exit mobile version