[英]SwiftUI Combine Debounce TextField
我有一個具有 SwiftUI 應用程序生命周期的 SwiftUI 應用程序。 我正在嘗試設置一種標准方法來向 TextFields 添加打字去抖動。 理想情況下,我想創建自己的 TextField 修飾符,可以輕松地將其應用於具有許多要編輯的文本字段的視圖。 我已經嘗試了很多方法來做到這一點,但我一定錯過了一些基本的東西。 這是一個例子。 這不起作用:
struct ContentView: View {
@State private var searchText = ""
var body: some View {
VStack {
Text("You entered: \(searchText)")
.padding()
TextField("Enter Something", text: $searchText)
.frame(height: 30)
.padding(.leading, 5)
.overlay(
RoundedRectangle(cornerRadius: 6)
.stroke(Color.blue, lineWidth: 1)
)
.padding(.horizontal, 20)
.onChange(of: searchText, perform: { _ in
var subscriptions = Set<AnyCancellable>()
let pub = PassthroughSubject<String, Never>()
pub
.debounce(for: .seconds(1), scheduler: DispatchQueue.main)
.collect()
.sink(receiveValue: { t in
self.searchText = t.first ?? "nothing"
} )
.store(in: &subscriptions)
})
}
}
}
任何指導將不勝感激。 Xcode 12.4,iOS 14.4
我認為您必須保留兩個變量:一個用於用戶鍵入時字段中的文本,另一個用於去抖動文本。 否則,用戶將看不到實時輸入的輸入,我假設這不是您想要的行為。 我猜這可能是針對更標准的用例,例如,一旦用戶暫停輸入,就執行數據提取。
我喜歡 ObservableObjects 和 Combine 來管理這類事情:
class TextFieldObserver : ObservableObject {
@Published var debouncedText = ""
@Published var searchText = ""
private var subscriptions = Set<AnyCancellable>()
init() {
$searchText
.debounce(for: .seconds(1), scheduler: DispatchQueue.main)
.sink(receiveValue: { [weak self] t in
self?.debouncedText = t
} )
.store(in: &subscriptions)
}
}
struct ContentView: View {
@StateObject var textObserver = TextFieldObserver()
@State var customText = ""
var body: some View {
VStack {
Text("You entered: \(textObserver.debouncedText)")
.padding()
TextField("Enter Something", text: $textObserver.searchText)
.frame(height: 30)
.padding(.leading, 5)
.overlay(
RoundedRectangle(cornerRadius: 6)
.stroke(Color.blue, lineWidth: 1)
)
.padding(.horizontal, 20)
Divider()
Text(customText)
TextFieldWithDebounce(debouncedText: $customText)
}
}
}
struct TextFieldWithDebounce : View {
@Binding var debouncedText : String
@StateObject private var textObserver = TextFieldObserver()
var body: some View {
VStack {
TextField("Enter Something", text: $textObserver.searchText)
.frame(height: 30)
.padding(.leading, 5)
.overlay(
RoundedRectangle(cornerRadius: 6)
.stroke(Color.blue, lineWidth: 1)
)
.padding(.horizontal, 20)
}.onReceive(textObserver.$debouncedText) { (val) in
debouncedText = val
}
}
}
我包括了兩個例子——頂部,容器視圖( ContentView
)擁有 ObservableObject 和底部,它被制成一個更可重用的組件。
來自@jnpdx 的文本去抖動器的簡化版本
請注意, .assign(to: &$debouncedText)
不會創建參考周期並自動為您管理訂閱
class TextFieldObserver : ObservableObject {
@Published var debouncedText = ""
@Published var searchText = ""
init(delay: DispatchQueue.SchedulerTimeType.Stride) {
$searchText
.debounce(for: delay, scheduler: DispatchQueue.main)
.assign(to: &$debouncedText)
}
}
如果您無法使用ObservableObject
(即,如果您的視圖由 state 機器驅動,或者您正在將輸入結果傳遞給委托,或者只是發布輸入),有一種方法可以使用只查看代碼。 這是通過將文本更改轉發到本地Publisher
來完成的,然后對該Publisher
的 output 進行去抖動。
struct SomeView: View {
@State var searchText: String = ""
let searchTextPublisher = PassthroughSubject<String, Never>()
var body: some View {
TextField("Search", text: $searchText)
.onChange(of: searchText) { searchText in
searchTextPublisher.send(searchText)
}
.onReceive(
searchTextPublisher
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
) { debouncedSearchText in
print(debouncedSearchText)
}
}
}
或者如果廣播更改:
struct DebouncedSearchField: View {
@Binding var debouncedSearchText: String
@State private var searchText: String = ""
private let searchTextPublisher = PassthroughSubject<String, Never>()
var body: some View {
TextField("Search", text: $searchText)
.onChange(of: searchText) { searchText in
searchTextPublisher.send(searchText)
}
.onReceive(
searchTextPublisher
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
) { debouncedSearchText in
self.debouncedSearchText = debouncedSearchText
}
}
}
但是,如果您可以選擇,使用ObservableObject
方法對 go 可能更“正確”。
GitHub 上的 Tunous 最近為 onChange 添加了去抖動擴展。 https://github.com/Tunous/DebouncedOnChange使用起來超級簡單。 而不是 add.onChange(of: value) {newValue in doThis(with: newValue) } 你可以 add.onChange(of: value, debounceTime: 0.8 / sec / ) {newValue in doThis(with: newValue) }
他設置了一個在 debounceTime 時間內休眠的任務,但它會在每次更改值時被取消並重置。 他創建的視圖修改器使用了 State var debounceTask。 我突然想到,這個任務可能是一個 Binding 而不是共享數量的多個 onChange 視圖修飾符,允許在同一個 debounce 上修改許多文本字段。 這樣,如果您使用相同的 debounceTask 以編程方式更改一堆文本字段,則只會調用一次操作,這通常是人們想要做的。 這是帶有一個簡單示例的代碼。
//
// Debounce.swift
//
// Created by Joseph Levy on 7/11/22.
// Based on https://github.com/Tunous/DebouncedOnChange
import SwiftUI
import Combine
extension View {
/// Adds a modifier for this view that fires an action only when a time interval in seconds represented by
/// `debounceTime` elapses between value changes.
///
/// Each time the value changes before `debounceTime` passes, the previous action will be cancelled and the next
/// action /// will be scheduled to run after that time passes again. This mean that the action will only execute
/// after changes to the value /// stay unmodified for the specified `debounceTime` in seconds.
///
/// - Parameters:
/// - value: The value to check against when determining whether to run the closure.
/// - debounceTime: The time in seconds to wait after each value change before running `action` closure.
/// - action: A closure to run when the value changes.
/// - Returns: A view that fires an action after debounced time when the specified value changes.
public func onChange<Value>(
of value: Value,
debounceTime: TimeInterval,
perform action: @escaping (_ newValue: Value) -> Void
) -> some View where Value: Equatable {
self.modifier(DebouncedChangeViewModifier(trigger: value, debounceTime: debounceTime, action: action))
}
/// Same as above but adds before action
/// - debounceTask: The common task for multiple Values, but can be set to a different action for each change
/// - action: A closure to run when the value changes.
/// - Returns: A view that fires an action after debounced time when the specified value changes.
public func onChange<Value>(
of value: Value,
debounceTime: TimeInterval,
task: Binding< Task<Void,Never>? >,
perform action: @escaping (_ newValue: Value) -> Void
) -> some View where Value: Equatable {
self.modifier(DebouncedTaskBindingChangeViewModifier(trigger: value, debounceTime: debounceTime, debouncedTask: task, action: action))
}
}
private struct DebouncedChangeViewModifier<Value>: ViewModifier where Value: Equatable {
let trigger: Value
let debounceTime: TimeInterval
let action: (Value) -> Void
@State private var debouncedTask: Task<Void,Never>?
func body(content: Content) -> some View {
content.onChange(of: trigger) { value in
debouncedTask?.cancel()
debouncedTask = Task.delayed(seconds: debounceTime) { @MainActor in
action(value)
}
}
}
}
private struct DebouncedTaskBindingChangeViewModifier<Value>: ViewModifier where Value: Equatable {
let trigger: Value
let debounceTime: TimeInterval
@Binding var debouncedTask: Task<Void,Never>?
let action: (Value) -> Void
func body(content: Content) -> some View {
content.onChange(of: trigger) { value in
debouncedTask?.cancel()
debouncedTask = Task.delayed(seconds: debounceTime) { @MainActor in
action(value)
}
}
}
}
extension Task {
/// Asynchronously runs the given `operation` in its own task after the specified number of `seconds`.
///
/// The operation will be executed after specified number of `seconds` passes. You can cancel the task earlier
/// for the operation to be skipped.
///
/// - Parameters:
/// - time: Delay time in seconds.
/// - operation: The operation to execute.
/// - Returns: Handle to the task which can be cancelled.
@discardableResult
public static func delayed(
seconds: TimeInterval,
operation: @escaping @Sendable () async -> Void
) -> Self where Success == Void, Failure == Never {
Self {
do {
try await Task<Never, Never>.sleep(nanoseconds: UInt64(seconds * 1e9))
await operation()
} catch {}
}
}
}
// MultiTextFields is an example
// when field1, 2 or 3 change the number times is incremented by one, one second later
// when field changes the three other fields are changed too but the increment task only
// runs once because they share the same debounceTask
struct MultiTextFields: View {
@State var debounceTask: Task<Void,Never>?
@State var field: String = ""
@State var field1: String = ""
@State var field2: String = ""
@State var field3: String = ""
@State var times: Int = 0
var body: some View {
VStack {
HStack {
TextField("Field", text: $field).padding()
.onChange(of: field, debounceTime: 1) { newField in
field1 = newField
field2 = newField
field3 = newField
}
Text(field+" \(times)").padding()
}
Divider()
HStack {
TextField("Field1", text: $field1).padding()
.onChange(of: field1, debounceTime: 1, task: $debounceTask) {_ in
times+=1 }
Text(field1+" \(times)").padding()
}
HStack {
TextField("Field2", text: $field2).padding()
.onChange(of: field2, debounceTime: 1, task: $debounceTask) {_ in
times+=1 }
Text(field2+" \(times)").padding()
}
HStack {
TextField("Field3", text: $field3).padding()
.onChange(of: field3, debounceTime: 1, task: $debounceTask) {_ in
times+=1 }
Text(field3+" \(times)").padding()
}
}
}
}
struct View_Previews: PreviewProvider {
static var previews: some View {
MultiTextFields()
}
}
我還沒有嘗試使用 ObservedObject 或 StateObject 的共享 debounceTask 綁定,只是 State var。 如果有人嘗試,請發布結果。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.