0

I am trying to return the search result when user type into search bar . Here I am using combine approach with Swift UI . I am trying to binding the object form view model on change function but I am getting following errors.

Cannot convert value of type 'Published.Publisher' to expected argument type '(String) -> Void'

Here is my viewModel code .

    final class FruitListViewModel: FruitListViewModelType, ObservableObject {
        
        private let service: Service!
        @Published private(set) var fruits = [Fruits]()
        // @Published var filteredFruit: [Fruits] = []
        private var cancellable = Set<AnyCancellable>()
        @Published var searchText: String = ""
        
        
        init(service:Service = ServiceImpl()) {
            self.service = service
            
        }
        
        func fetchFruit() {
            let client = ServiceClient(baseUrl:EndPoints.baseUrl.rawValue, path:Path.fruitList.rawValue, params:"", method:"get")
            
            service.fetchData(client: client, type: [Fruits].self)
                .receive(on: RunLoop.main)
                .sink { completion in
                    switch completion {
                    case let .failure(error):
                        print(error)
                    default:
                        break
                    }
                } receiveValue: { response in
                    self.fruits = response
                }.store(in: &cancellable)
            
            $searchText
                .combineLatest($fruits)
                .debounce(for: .milliseconds(80), scheduler: RunLoop.main)
                .map { (searchText, fruit) -> [Fruits]  in
                    guard !searchText.isEmpty else {
                        return fruit
                    }
                    let lowercasedText = searchText.lowercased()
                    let filterFruitList = fruit.filter { (fruit) -> Bool in
                        fruit.name.lowercased().contains(lowercasedText) ||
                        fruit.genus.lowercased().contains(lowercasedText) ||
                        fruit.family.lowercased().contains(lowercasedText)
                    }
                    return filterFruitList
                }
                .sink { [weak self] (filterList) in
                    self?.fruits = filterList
                }
                .store(in: &cancellable)
        }

}

Here is the code into view ..

struct ContentView: View {

    @EnvironmentObject private var viewModel: FruitListViewModel
    @State var searchText = ""

    var body: some View {
        NavigationView {
            List {
                ForEach(viewModel.fruits) { fruit in
                    NavigationLink(destination: FruitDetailsView(fruit: fruit)) {
                        RowView(name: fruit.name, genus: fruit.genus, family: fruit.family)
                    }
                }
            }
            .searchable(text: $viewModel.searchText)
            .onChange(of: viewModel.searchText, perform: viewModel.$searchText)
            .task {
                viewModel.fetchFruit()
            }
            .navigationTitle("Fruits List")
        }
        .onAppear {

            viewModel.fetchFruit()
        }
    }

}

Here is the screenshot of the error ..

enter image description here

Search filter result .

enter image description here

  • 1
    There are a lot of issues in the code. Delete the `.task` expression and the `onChange` line. Both are pointless. Delete also the `@State var searchText = ""` line, it's unused. Basically you need a non-publishing property `allFruit` for the result of the fetch and then a `@Published` property to display either the filtered items or – if `searchText` is empty – the contents of `allFruit`. At the moment you filter the filtered items *to death*. And remove the exclamation mark after `Service`. – vadian Mar 13 '23 at 12:21
  • Updated . got it –  Mar 13 '23 at 12:25
  • Thanks but where I should define the allFruit ? and How it will call the fetch function to get all the list of data ? –  Mar 13 '23 at 12:51
  • but when I click cancel button it not returning the list of fruits –  Mar 13 '23 at 13:02
  • Remove `@Published` from `fruits`, remove the // from `filteredFruit`, at the end of the pipeline assign `self?.filteredFruit = filterList` and show `filteredFruit` in the view. The method to fetch the data will be called in `onAppear` – vadian Mar 13 '23 at 13:11
  • If you removed @ Published form fruit it will give you error on view model on this line .combineLatest($fruits) –  Mar 13 '23 at 13:16
  • Right, keep it, it doesn’t matter. – vadian Mar 13 '23 at 13:17
  • but when I click cancel button into search bar it not returning the list of fruits – –  Mar 13 '23 at 13:18
  • You have to call a method which sets `searchText` to an empty string. For example see https://stackoverflow.com/questions/69355159/swiftui-perform-action-when-cancel-is-clicked-searchable-function?rq=1 – vadian Mar 13 '23 at 13:19
  • when it should be called into view model ? –  Mar 13 '23 at 13:21
  • here we already doing it right ? guard !searchText.isEmpty else { return fruit } –  Mar 13 '23 at 13:22
  • Yes, that’s perfect. – vadian Mar 13 '23 at 13:22
  • But I do not see it working when I click the cancel button –  Mar 13 '23 at 13:23
  • Please see the linked question above how to catch the cancel button. – vadian Mar 13 '23 at 13:24
  • Let me add the screenshot it reutrn the filtered list not the whole –  Mar 13 '23 at 13:25
  • This the example using Sate property wrapper but my case it is viewmodel object –  Mar 13 '23 at 13:35

1 Answers1

0

Replace

@Published private(set) var fruits = [Fruits]()
// @Published var filteredFruit: [Fruits] = []

with

@Published private(set) var fruits = [Fruits]()
@Published var filteredFruit: [Fruits] = []

Replace

.sink { [weak self] (filterList) in
    self?.fruits = filterList
}

with

.sink { [weak self] (filterList) in
    self?.filteredFruit = filterList
}

Replace

struct ContentView: View {

    @EnvironmentObject private var viewModel: FruitListViewModel
    @State var searchText = ""
   
    var body: some View {
        NavigationView {
            List {
                ForEach(viewModel.fruits) { fruit in
                    NavigationLink(destination: FruitDetailsView(fruit: fruit)) {
                        RowView(name: fruit.name, genus: fruit.genus, family: fruit.family)
                    }
                }
            }
            .searchable(text: $viewModel.searchText)
            .onChange(of: viewModel.searchText, perform: viewModel.$searchText)
            .task {
                viewModel.fetchFruit()
            }
            .navigationTitle("Fruits List")
        }
        .onAppear {

            viewModel.fetchFruit()
        }
    }

}

with

struct ContentView: View {

    @EnvironmentObject private var viewModel: FruitListViewModel

    var body: some View {
        NavigationView {
            List {
                ForEach(viewModel.filteredFruit) { fruit in
                    NavigationLink(destination: FruitDetailsView(fruit: fruit)) {
                        RowView(name: fruit.name, genus: fruit.genus, family: fruit.family)
                    }
                }
            }
            .searchable(text: $viewModel.searchText)
            .navigationTitle("Fruits List")
        }
        .onAppear {
            viewModel.fetchFruit()
        }
    }

}

And (not related) declare

private let service: Service // no exclamation mark

Side note:

Please consolidate the fruit(s) spelling, there is no plural form with trailing s in English and also the model struct is supposed to be named Fruit.

vadian
  • 274,689
  • 30
  • 353
  • 361