skip to Main Content

New to "PassthroughSubject" and I am trying to implement search using "PassthroughSubject". It worked fine but only issue being I was getting results for all the requests sent via search i.e. let’s say I try "Hello" in search bar, search for "Hello" is initiated and before it gets completed, I remove few characters i.e. I remove last "lo", leaving the text as "Hel" in search bar. Now a request for "Hel" is being made. I get results for both these requests in random order. I just want to get results of the last request and with to cancel any uncompleted/pending requests.

I read that ‘switchToLatest()’ helps with that, but when I try to use it in my code, I am getting "No exact matches in call to instance method 'switchToLatest'" error.

Sample code:

import SwiftUI
import Foundation
import Combine

struct ContentView: View {
    @State private var searchText = ""
    let searchTextPublisher = PassthroughSubject<String, Never>()
    
    var body: some View {
        NavigationStack {
            VStack {
                SearchResultsListView(searchText: searchText, searchTextPublisher: searchTextPublisher)
                .searchable(
                    text: $searchText,
                    placement: .navigationBarDrawer(displayMode: .always),
                    prompt: "search"
                )
                .onChange(of: searchText) { newText in
                    searchTextPublisher.send(newText) /// Publishes when a search term is changed. Used to debounce search.
                }
            }
        }
    }
}


struct SearchResultsListView: View {
    var searchText: String
    var searchTextPublisher: PassthroughSubject<String, Never>
    
    var body: some View {
        VStack {
            // List view and some other UI/UX
        }
        .onReceive(searchTextPublisher.debounce(for: 0.4, scheduler: RunLoop.main)) { debouncedSearchText in
            Task {
                if !debouncedSearchText.isEmpty {
                    searchTextPublisher
                        .map() {_ in
                            Task {
                                await self.initiateSearch(debouncedSearchText: debouncedSearchText)
                            }
                        }
                        .switchToLatest()
                        .sink {
                            print("####")
                        }
                    
                } else {
                    // show empty results view
                }
            }
        }
    }
    
    private func initiateSearch(debouncedSearchText: String) async -> AnyPublisher <[CustomObject], Never> {
        await Task {
            do {
                let searchResults = try await getSearchResultsAsync(searchText: debouncedSearchText)
            } catch {
                // Show error message
            }
        }
        .result.publisher.eraseToAnyPublisher()
    }
    
}

enter image description here

What I am doing wrong here? My call to get search results i.e. "getSearchResultsAsync" is async call.

Thanks!

2

Answers


  1. to remove the error, you have to change your sink to:

    .sink { _ in  // <-- there is a parameter there
      print("####")
    }
    

    While I doubt that mixing unstructured tasks and the combine world in the view is a good idea, or even possible.

    Just take a look on this and consider decoupling the business logic from the view.

    typealias SearchAction = (String) -> AnyPublisher<[String], Error>
    
    class Api {
        func search() -> SearchAction {
            return { searchText in
                // do the search here and return an AnyPublisher<[String], Error>
            }
        }
    }
    
    class SearchModel: ObservableObject {
        @Published var searchText = ""
        @Published var searchResults = [String]()
    
        init(searchAction: @escaping SearchAction) {
            $searchText
                .debounce(for: 0.4, scheduler: DispatchQueue.main)
                .map { searchAction($0) }
                .switchToLatest()
                .replaceError(with: []) // <-- not a proper error handling
                .assign(to: &$searchResults)
        }
    }
    
    struct ContentView: View {
        @ObservedObject var model: SearchModel
    
        var body: some View {
            NavigationStack {
                VStack {
                    SearchResultsListView(searchResults: model.searchResults)
                    .searchable(
                        text: $model.searchText,
                        placement: .navigationBarDrawer(displayMode: .always),
                        prompt: "search"
                    )
                }
            }
        }
    }
    
    
    struct SearchResultsListView: View {
        var searchResults: [String]
    
        var body: some View {
            VStack {
                List(searchResults, id: .self) {
                    Text($0)
                }
            }
        }
    }
    
    Login or Signup to reply.
  2. Since you are using async/await you can remove all the Combine and the onReceive. In SwiftUI it is .task, e.g.

    .task(id: searchText) { // when this value changes the sleep throws a cancelled exception.
        if searchText = "" {
            results = []
            return
        }
        do {
            try await Task.sleep(for: .seconds(0.4))
        }
        catch { // debouncing
            return
        }
        results = await performSearch(text: searchText) // declare this func in a non-View struct if you want it to run on a background thread
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search