Every time I need a single-value debounce, after reviewing the existing patterns in modern Swift, I end up using the old good NSObject.performSelector and its cancel... counterpart. Although with a major drawback that these methods only work on MainActor but this is generally fine since debounce is typically used in UI.
Fundamentally, what makes the newer methods uncomfortable to use is the inability to cancel the previous task in a “clean” way, as in, remove it from the queue completely.
var scheduledTask: Task<Void, Never>?
func timerProc() {
scheduledTask?.cancel()
// ... perform some action
scheduledTask = Task {
try await Task.sleep(for: 5)
if !Task.isCancelled {
await timerProc()
}
}
}
so, what if timerProc() is “bombarded” with a lot of calls? Each call leaves a task in the queue that will only find out about cancellation after its time is out.
This seems like an omission in Swift 6, unless I’m missing something of course and this is not a big problem, or unless it’s too complex to implement within the new concurrency paradigm.
I am a beginner, so please take my views with a pinch of salt.
I feel adding a delay is not equivalent to debounce.
Questions:
If you are using SwiftUI then .task { } would cancel when view is no longer available.
Are you using Observations in SwiftUI and want to debounce ?
Possible approach
If the concern is you don't have @Published to use Combine publisher then there are alternatives.
You could use Observations { } with AsyncAlgorithms (swift package)
Observations { } is however only on the os 26 and above.
Sample code:
import AsyncAlgorithms
@Observable
class Car {
var name = ""
func checkIfNameExists() async {
let values = Observations {
self.name
}
for await value in values.debounce(for: .seconds(0.5)) {
print("name: \(value)")
}
print("done")
}
}
import SwiftUI
struct CarView: View {
@State private var car = Car()
var body: some View {
TextField("name", text: $car.name)
.task {
// would get cancelled when view goes out of scope
await car.checkIfNameExists()
}
}
}
Yeah wrapping it up in a helper library makes sense if you’re using this pattern frequently, but I’m not sure it would be the right fit for the standard library as it’s fairly specific and pretty straightforward to implement if you find yourself needing it a lot in your codebase.
Might I suggest a slight API tweak though? Instead of Task(afterDelay:), what about something like Task.delayed(for delay: Duration)? It uses Swift’s Duration api & aligns better with existing Task api names like Task.detached.