# \[Combine\] How to \`debounce\` specific events only?

**URL:** <https://forums.swift.org/t/combine-how-to-debounce-specific-events-only/66129>\
**Category:** Using Swift\
**Tags:** combine\
**Created:** [July 13, 2023, 4:28pm UTC](https://forums.swift.org/t/combine-how-to-debounce-specific-events-only/66129 "2023-07-13T16:28:46Z")\
**Posts on this page:** 7\
**Page:** 1

<div class="post-metadata">

**Author:** ![ylorn](https://avatars.discourse-cdn.com/v4/letter/y/13edae/32.png) [@ylorn](https://forums.swift.org/u/ylorn)\
**Post date:** [July 13, 2023, 4:28pm UTC](https://forums.swift.org/t/combine-how-to-debounce-specific-events-only/66129/1 "2023-07-13T16:28:46Z")

</div>

Say we have following events:

```swift
enum Events {
    case initial
    case ongoing
    case enabled
    case disabled
    case error(Error)
}

```

and all these events is published on an `ObservableObject`:

```swift
class Observer: ObservableObject {
    @Published var event: Events = .initial
    var cancellables: Set<AnyCancellable> = []
}

```

What I want to achieve is:

- When receiving an `.ongoing` event, wait for `1` second:
  - if a new event comes within that `1` second, skip `.ongoing` and publish the new event;
  - otherwise, publish the `.ongoing` event

- When receiving an `.error` event, wait for `3` second:
  - if a new event comes within that `3` second, skip `.error` and publish the new event;
  - otherwise, publish the `.error` event

I understand the `.debounce` operator can achieve the effects, but that is applied to event flow on all events:

```diff
    init() {
        self.$event
+ .debounce(for: .seconds(1), scheduler: DispatchQueue.main)
            .sink {
                print("Event:", $0)
            }
            .store(in: &self.cancellables)
    }

```

I am wondering if there is a way to make sure the debouncing logic only kicks in selectively on certain events?

---

<div class="post-metadata">

**Author:** ![ylorn](https://avatars.discourse-cdn.com/v4/letter/y/13edae/32.png) [@ylorn](https://forums.swift.org/u/ylorn)\
**Post date:** [July 13, 2023, 5:08pm UTC](https://forums.swift.org/t/combine-how-to-debounce-specific-events-only/66129/2 "2023-07-13T17:08:42Z")

</div>

I tried with following implementation, but it seems no events gets published at all:

```diff
    init() {
        self.$event
+ .flatMap {
+ switch $0 {
+ case .ongoing:
+ return Just($0)
+ .debounce(for: .seconds(1), scheduler: DispatchQueue.main)
+
+ case .error:
+ return Just($0)
+ .debounce(for: .seconds(3), scheduler: DispatchQueue.main)
+
+ default:
+ return Just($0)
+ .debounce(for: .seconds(0), scheduler: DispatchQueue.main)
+ }
+ }
            .sink {
                print("Event:", $0)
            }
            .store(in: &self.cancellables)
    }

```

---

<div class="post-metadata">

**Author:** ![ylorn](https://avatars.discourse-cdn.com/v4/letter/y/13edae/32.png) [@ylorn](https://forums.swift.org/u/ylorn)\
**Post date:** [July 14, 2023, 2:03am UTC](https://forums.swift.org/t/combine-how-to-debounce-specific-events-only/66129/3 "2023-07-14T02:03:50Z")

</div>

Figured it out.

---

<div class="post-metadata">

**Author:** ![cassianomonteiro](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/cassianomonteiro/32/29087_2.png) [@cassianomonteiro](https://forums.swift.org/u/cassianomonteiro)\
**Post date:** [November 29, 2023, 8:28pm UTC](https://forums.swift.org/t/combine-how-to-debounce-specific-events-only/66129/4 "2023-11-29T20:28:16Z")

</div>

Can you post your solution here? I'm facing the same issue.

---

<div class="post-metadata">

**Author:** ![Dmitriy\_Ignatyev](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/dmitriy_ignatyev/32/11340_2.png) [@Dmitriy\_Ignatyev](https://forums.swift.org/u/Dmitriy_Ignatyev)\
**Post date:** [November 29, 2023, 8:47pm UTC](https://forums.swift.org/t/combine-how-to-debounce-specific-events-only/66129/5 "2023-11-29T20:47:56Z")

</div>

Hi. I have no time to completely solve the task, but I can share similar operator that I implemented in RxSwift. You can implement your own using Combine.

```swift
extension ObservableType where Element: Hashable {
  public func debounceEachUniqueElement(_ dueTime: RxTimeInterval, scheduler: some SchedulerType) -> Observable<Element> {
    Observable<Element>.create { observer -> any Disposable in
      let debouncer = ElementDebouncer<Element>(observer: observer)
      
      let disposable = self.subscribe(onNext: { element in
        debouncer.lockAndDo {
          let token = UUID()
          debouncer._addPending(element: element, token: token)
          let schedulerDisp = scheduler.scheduleRelative((element, token), dueTime: dueTime, action: debouncer.propagate)
          debouncer._addDisposable(schedulerDisp, token: token)
        }
      }, onError: { error in
        debouncer.lockAndDo {
          debouncer._onTerminationCleanResources()
          
          observer.onError(error)
        }
      }, onCompleted: {
        debouncer.lockAndDo {
          debouncer._onCompletedEmitAllPendingValues()
          debouncer._onTerminationCleanResources()
          
          observer.onCompleted()
        }
      }, onDisposed: nil)
      
      return disposable
    } // .create end
  }
}

fileprivate final class ElementDebouncer<T: Hashable> {
  private let _lock = NSRecursiveLock()
  
  private var pendingElementsQueue: [T: UUID] = [:]
  private var disposables: [UUID: any Disposable] = [:]
  
  private let observer: AnyObserver<T>
  
  init(observer: AnyObserver<T>) {
    self.observer = observer
  }
  
  func lockAndDo(_ action: VoidClosure) {
    _lock.withLockVoid(action)
  }
  
  func _addPending(element: T, token: UUID) {
    pendingElementsQueue[element] = token
  }
  
  func _addDisposable(_ disposable: any Disposable, token: UUID) {
    disposables[token] = disposable
  }
  
  func _onTerminationCleanResources() {
    disposables.forEach { _, disposable in disposable.dispose() }
    disposables = [:]
    
    pendingElementsQueue = [:]
  }
  
  func _onCompletedEmitAllPendingValues() {
    pendingElementsQueue.forEach { element, _ in observer.onNext(element) }
  }
  
  func propagate(_ args: (element: T, scheduledToken: UUID)) -> any Disposable {
    _lock.lock(); defer { _lock.unlock() }
    
    let (element, scheduledToken) = args
    
    disposables[scheduledToken]?.dispose()
    disposables[scheduledToken] = nil
    
    if let currentToken = pendingElementsQueue[element], currentToken == scheduledToken {
      pendingElementsQueue[element] = nil
      observer.onNext(element)
    }
    
    assertError(pendingElementsQueue.count < 100, ConditionalError(code: .excessOfLimit))
    
    return Disposables.create()
  }
}

```

This operator debounces each element separately. As if each unique element had its own timer that is restarted when the element is emitted again while timer is not completed.  
You can also see standard debounce operator as a reference: [https://github.com/ReactiveX/RxSwift/blob/main/RxSwift/Observables/Debounce.swift](https://github.com/ReactiveX/RxSwift/blob/main/RxSwift/Observables/Debounce.swift)

In your case you need to add a closure for making a decision to debounce or not. Something like:  
`subject.debounce(dueTime: Duration, shouldDebounce: @escaping (Element) -> Bool)`  
or  
`subject.debounce(shouldDebounce: @escaping (Element) -> Duration?)` – there is no debounce when nil and debounce with provided time interval if not nil. This also gives the ability to provide different debounce intervals for different elements.

---

<div class="post-metadata">

**Author:** ![cassianomonteiro](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/cassianomonteiro/32/29087_2.png) [@cassianomonteiro](https://forums.swift.org/u/cassianomonteiro)\
**Post date:** [November 29, 2023, 11:04pm UTC](https://forums.swift.org/t/combine-how-to-debounce-specific-events-only/66129/6 "2023-11-29T23:04:46Z")

</div>

Thanks, with your insights I ended up with this. I don't need to track each elements separately, instead simply overriding the debounce with the condition:

```swift
public extension Publisher {
  func debounce<S>(
    for dueTime: S.SchedulerTimeType.Stride,
    scheduler: S,
    options: S.SchedulerOptions? = nil,
    shouldDebounce: @escaping (Output) -> Bool
  ) -> AnyPublisher<Output, Failure> where S: Scheduler {
    map { output in
      guard shouldDebounce(output) else {
        return Just(output)
          .eraseToAnyPublisher()
      }
      return Just(output)
        .delay(for: dueTime, scheduler: scheduler)
        .eraseToAnyPublisher()
    }
    .switchToLatest()
    .eraseToAnyPublisher()
  }
}

```

---

<div class="post-metadata">

**Author:** ![KiranJasvanee](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/kiranjasvanee/32/39859_2.png) [@KiranJasvanee](https://forums.swift.org/u/KiranJasvanee)\
**Post date:** [March 6, 2025, 5:07am UTC](https://forums.swift.org/t/combine-how-to-debounce-specific-events-only/66129/7 "2025-03-06T05:07:41Z")

</div>

This is working absolutely fine. Thanks for the solution @cassianomonteiro!
