August 9, 2024

79. Пример работы с debounce в Combine

Кто еще застал RxSwift вполне мог работать с debounce для реализации отложенного поиска, чтобы начинать поиск с небольшой задержкой после ввода текста. В этой статье посмотрим как можно это реализовать с помощью Combine.

Демонстрация

Демо дебаунса в 1 секунду, большие паузы между новыми символами

На гифке выше пример дебаунса в 1 секунду - после ввода нового символа мы ждем 1 секунду, чтобы выполнить действие. Тут и далее в качестве примера действия будем отображать введенный текст под текстовым полем.

Демо дебаунса в 1 секунду, вводим много текста сразу

На гифке выше мы вводим текст почти без пауз, т.е. между каждым введенным символом меньше 1 секунды, и только когда мы останавливаемся дольше чем на 1 секунду, выполняется действие после дебаунса.

Реализация

Есть разные варианты реализации такой фичи. Для простоты покажу вариант с меньшим количеством кода на Combine, а в самой вьюхе пока что будем использовать обычное текстовое поле.

Последовательность наших действий:

  1. Подписаться на изменения текста в первом свойстве
  2. Добавить дебаунс для подписки
  3. Выводить значение после дебаунса в другое свойство
  4. Подключить к вьюхе класс в подпиской и двумя вышеупомянутыми свойствами

Вьюмодель

final class DebounceViewModel : ObservableObject {
  @Published var searchQuery = ""
  @Published private(set) var debouncedText = ""
   
  init(delay: DispatchQueue.SchedulerTimeType.Stride) {
    $searchQuery
      .debounce(for: delay, scheduler: DispatchQueue.main)
      .assign(to: &$debouncedText)
  }
}

Вьюха

struct DebounceExampleView: View {
  @StateObject private var viewModel: DebounceViewModel
   
  init(debounceDelay: DispatchQueue.SchedulerTimeType.Stride) {
    self._viewModel = .init(wrappedValue: .init(delay: debounceDelay))
  }
   
  var body: some View {
    VStack(spacing: 20) {
      TextField("Что ищем?", text: $viewModel.searchQuery)
        .textFieldStyle(.roundedBorder)
      if !viewModel.debouncedText.isEmpty {
        Text("Пользователь ввел запрос для поиска:")
        Text(viewModel.debouncedText)
      }
    }
    .padding()
    Spacer()
  }
}

Превью

#Preview("1 секунда") {
  DebounceExampleView(debounceDelay: .seconds(1))
}

#Preview("0.5 секунды") {
  DebounceExampleView(debounceDelay: .seconds(0.5))
}

Что дальше

Теперь мы отслеживаем нужную нам паузу при вводе текста и можем реализовать функционал поиска - это можно сделать разными способами, например:

  1. Добавить во вьюху модификатор onChange/onReceive, в котором стартовать поиск
  2. Добавить во вьюмодель подписку на debouncedText, и при ее срабатывании стартовать поиск
  3. Можно сразу без лишнего действия с обновлением debouncedText вызывать поиск после дебаунса во вьюмодели

В любом случае нужно продумать логику поиска и обработать разные важные кейсы, например:

  • поиск уже стартовали, а пользователь продолжает вводить текст (и предыдущий поиск возвращает результаты/ошибку)
  • пользователь вводит пробелы или несколько слов - нужно дропнуть лишние пробелы и корректно закодировать оставшиеся для отправки на сервер (если есть интеграция с сервером)

Заключение

В данном случае я знал, что для решения рабочей задачи мне нужен debounce, и было несложно загуглить решение. Если знать, что нужно гуглить, то разобраться в Combine может быть проще, чем в RxSwift. Как минимум из-за наличия меньшего объема фичей, ведь RxSwift годами развивался за счет сообщества, а Combine сильно моложе.

Код для этой статьи можно посмотреть тут, другие статьи по разработке - тут, а про инвестиции - тут.