0

I made a custom SearchBar with SwiftUI and put it in the view, having BookListViewModel as @ObservedObject, but how do it call fetchBooks() from BookListViewModel and show as list?

Here is my SearchView()

struct SearchView: View {
    @ObservedObject var bookListVM : BookListViewModel
    
    var body: some View {
        VStack {
            SearchBar(searchText: $bookListVM.keyword)
            Text("\(bookListVM.keyword)")
        
            if bookListVM.keyword.count > 0 {
                List(bookListVM.books, id: \.id) { bookVM in
                    NavigationLink(
                        destination: BookDetailView(id: bookVM.id),
                        label: {
                            Text("\(bookVM.title)")
                        })
                }                
            } else {
                Text("Please Search Your Item")
                Spacer()
            }
        }
    }
}

and in the BookListViewModel,

class BookListViewModel: ObservableObject {
    @Published public private(set) var books: [BookViewModel] = []
    private var cancellable: AnyCancellable?
    @Published var keyword : String = ""
    
    init() {
        self.keyword = keyword
        self.fetchBooks()
    }
    
    func fetchBooks() {
        self.cancellable = Webservice().getSearchBooks(keyword: self.keyword).map { books in
            books.map { BookViewModel(book: $0) }
        }
            .sink(receiveCompletion: { completion in
                if case .failure(let err) = completion {
                    print("Retrieving data failed with error \(err)")
                }
            }, receiveValue: { bookViewModel in
                self.books = bookViewModel
                print(bookViewModel)
            })
    }
}

class BookViewModel: ObservableObject {
    let book: Book
    let id: String
    var title: String
    
    init(book: Book){
        self.book = book
        self.id = book.isbn13
        self.title = book.title
    }
}

and my Webservice, function getSearchBooks()

func getSearchBooks(keyword: String) -> AnyPublisher<[Book], Error> {
        URLSession.shared.dataTaskPublisher(for: EndPoint.books(keyword).url)
            .receive(on: RunLoop.main)
            .map{ $0.data }
            .decode(type: BooksData.self, decoder: JSONDecoder())
            .map{$0.books}
            .catch { _ in Empty<[Book], Error>()}
            .eraseToAnyPublisher()
    
    }

What did I do wrong? Any advice will be appreciated. Thanks in advance!

J L
  • 11

2 Answers2

2

There is no action being performed after the keyword is updated.
One way is to observe $keyword for changes & do something when it updates.

Example Solution:

class BookListViewModel: ObservableObject {
    //...
    private var searchSubscription: AnyCancellable?
    
    init() {
        searchSubscription = self.$keyword
            .debounce(for: 0.3, scheduler: DispatchQueue.main)
            .sink(receiveValue: { newValue in
                self.fetchBooks(searchTerm: newValue)
            })
    }

    func fetchBooks(searchTerm: String) {
        self.cancellable = Webservice().getSearchBooks(keyword: searchTerm)
                                       .map { books in
        //...
}

NOTE:

  • Not related to the core problem but having a .debounce(for:scheduler:) is good for optimization; we don't want the request being sent for every keystroke. Lets wait 0.3s instead.
staticVoidMan
  • 19,275
  • 6
  • 69
  • 98
0

If I understand your question correctly, how to call "fetchBooks()" when you have a keyword from your SearchBar.

You could try this approach with your SearchBar (simulated here with a TextField):

struct SearchView: View {
    @ObservedObject var bookListVM : BookListViewModel
    
    var body: some View {
        VStack {
            //   SearchBar(searchText: $bookListVM.keyword)
            TextField("book search", text: $bookListVM.keyword) // <-- for testing
                .onSubmit {
                    if !bookListVM.keyword.isEmpty {
                        bookListVM.fetchBooks()
                    }
                }
            
            Text("\(bookListVM.keyword)")
            
            if bookListVM.keyword.count > 0 {
                List(bookListVM.books, id: \.id) { bookVM in
                    NavigationLink(
                        destination: BookDetailView(id: bookVM.id),
                        label: {
                            Text("\(bookVM.title)")
                        })
                }
            } else {
                Text("Please Search Your Item")
                Spacer()
            }
        }
    }
}