みんなは、どんな実装にしていますか。
入力文字を監視しつつの検索結果のリアルタイム反映的なやつを作るとき。
詰まる感じをどう解消していますか。
Debounce
only emit an item from an Observable if a particular timespan has passed without it emitting another item
👉 ReactiveX - Debounce operator
👉 debounce(for:scheduler:options:) | Apple Developer Documentation
import Combine
public final class DebounceObject: ObservableObject {
@Published var text: String = ""
@Published var debouncedText: String = ""
private var bag = Set<AnyCancellable>()
public init(dueTime: TimeInterval = 0.5) {
$text
.removeDuplicates()
.debounce(for: .seconds(dueTime), scheduler: DispatchQueue.main)
.sink(receiveValue: { [weak self] value in
self?.debouncedText = value
})
.store(in: &bag)
}
}
struct SearchView: View {
@StateObject var debounceObject = DebounceObject()
var body: some View {
VStack {
TextField(text: $debounceObject.text)
.onChange(of: debounceObject.debouncedText) { text in
// perform search
}
}
}
}
👉 How to debounce TextField search in SwiftUI | Swift Discovery
combine てのもう時代遅れなのですか ?
今現在、本流の本筋の考え方のわかりやすいシンプルな記述を探します。
🤔 Tunous/DebouncedOnChange
直感的に使いやすい ViewModifier ライブラリです。
👉 Tunous/DebouncedOnChange: SwiftUI onChange View extension with debounce time
コード量が少ないので内部も把握しやすいです。
基本的な使い方は、0.25 秒をデバウンス時間として以下のように書けます。
TextField("Search-1", text: $text)
.onChange(of: text, debounceTime: .seconds(0.25)) { newValue in
debouncedText = newValue
}
使ってみると分かるのは、View 内に @State として、
text
と debouncedText
の2つが存在してしまうことが面倒です。
なので、それを避けるために、
DebounceTextField として View を外出しにして使います。
DebounceTextField2(titleKey: "Search-2", debouncedText: $debouncedText)
struct DebounceTextField2: View {
var titleKey: String
@Binding var debouncedText: String
@State private var local = ""
var body: some View {
TextField(titleKey, text: $local)
.onChange(of: local, debounceTime: .seconds(0.25)) { newValue in
debouncedText = newValue
}
}
}
onChange() を使うのはどうなのか。
🤔 Binding をカスタムする
そもそも @State は、
@Binding な引数をもつコンポーネントに対して双方向に値が流れる
ということをきちんと頭に置きながら考えてみます。
struct DebounceTextField3: View {
var titleKey: String
@Binding var debouncedText: String
@State private var delay: Task<Void, Never>?
var body: some View {
TextField(
titleKey,
text: Binding(
get: { debouncedText },
set: { newvalue in
delay?.cancel()
delay = Task {
do {
try await Task.sleep(for: .seconds(0.25))
} catch { return }
debouncedText = newvalue
}
}
)
)
}
}
続いて、ちょっとしっかりしたライブラリを使ってみます。
👉 boraseoksoon/Throttler: One Line to throttle, debounce and delay: Say Goodbye to Reactive Programming such as RxSwift and Combine.
struct DebounceTextField4: View {
var titleKey: String
@Binding var debouncedText: String
var body: some View {
TextField(
titleKey,
text: Binding(
get: { debouncedText },
set: { newvalue in
debounce(.seconds(0.25)) {
debouncedText = newvalue
}
}
)
)
}
}
🤔 まとめ
Custom Binding を使ったほうがスッキリします。