[英]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.