SwiftUI Combine Debounce TextField

use*_*617 4 xcode ios swiftui combine

I have a SwiftUI app with SwiftUI App life cycle. I'm trying to setup a standard way to add typing debounce to TextFields. Ideally, I'd like to create my own TextField modifier that can easily be applied to views that have many textfields to edit. I've tried a bunch of ways to do this but I must be missing something fundamental. Here's one example. This does not work:

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)
                })
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Any guidance would be appreciated. Xcode 12.4, iOS 14.4

Iva*_*kiy 19

来自 @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)
    }
}
Run Code Online (Sandbox Code Playgroud)


Alb*_*ori 11

如果您无法使用ObservableObject(即,如果您的视图由状态机驱动,或者您将输入结果传递给委托,或者只是发布输入),则有一种方法可以仅使用来完成去抖查看代码。这是通过将文本更改转发到本地Publisher,然后消除该输出的抖动来完成的Publisher

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)
            }
    }
}
Run Code Online (Sandbox Code Playgroud)

或者如果广播更改:

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
            }
    }
}
Run Code Online (Sandbox Code Playgroud)

但是,如果您有选择,采用该ObservableObject方法可能会更“正确”。


jn_*_*pdx 7

I think you'll have to keep two variables: one for the text in the field as the user is typing and one for the debounced text. Otherwise, the user wouldn't see the typing coming in in real-time, which I'm assuming isn't the behavior you want. I'm guessing this is probably for the more standard use case of, say, performing a data fetch once the user has paused their typing.

I like ObservableObjects and Combine to manage this sort of thing:

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: { 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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

I included two examples -- the top, where the container view (ContentView) owns the ObservableObject and the bottom, where it's made into a more-reusable component.

  • TextFieldObserver 中的接收器订阅中的强引用会导致内存泄漏,请将其更改为 `.sink(receiveValue: { [weak self] in self?.debouncedText = $0 } )` (2认同)