skip to Main Content

So I want to search books from google books api, but only through url query, how can I call API again when I enter the text in the search bar? How to reload the call?

I tried also with textfield onSumbit method, but nothing work.

I just want to insert value of textSearch to network.searchText and that network.searchText to insert into q=

here is my code of ContentView:

//
//  ContentView.swift
//  BookApi
//
//  Created by Luka Šalipur on 7.6.22..
//

import SwiftUI

struct URLImage: View{
    var urlString: String
    @State var data: Data?

    
    var body: some View{
        if let data = data, let uiimage = UIImage(data:data) {
            Image(uiImage: uiimage)
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width:80, height:120)
                .background(Color.gray)
        } else {
            Image(systemName: "book").onAppear {
                fetch()
            }
        }
    }
    
    private func fetch(){
        guard let url = URL(string: urlString) else {
            return
        }
                
                let task = URLSession.shared.dataTask(with:url) { data, _, error in
                    self.data = data
                }
        
        task.resume()
    }

}

// ContentView

struct ContentView: View {
    @ObservedObject var network = Network()
    @State var textSearch:String = "knjiga"
    @State private var shouldReload: Bool = false
    
    func context(){
        network.searchText = self.textSearch
        print(network.searchText)
    }

    var body: some View {
        
        NavigationView{
              
            List{
      
            ForEach(network.book, id:.self){ item in
                NavigationLink{
                    Webview(url: URL(string: "(item.volumeInfo.previewLink)")!)
                } label: {
                    
                
                HStack{
                    URLImage(urlString: item.volumeInfo.imageLinks.thumbnail)
                    Text("(item.volumeInfo.title)")
               
                }
                }
            }
                
            }
            .onAppear{
                context()
            }
            .onChange(of: textSearch, perform: { value in
                self.shouldReload.toggle()
            })
            .searchable(text: $textSearch)

            .navigationTitle("Books")
                .task{
                    await network.loadData()
                }
             
          
  
            
               
        }
        

    }

}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

And here is my API(network) call:

//
//  Network.swift
//  BookApi
//
//  Created by Luka Šalipur on 7.6.22..
//

import Foundation
import SwiftUI



class Network: ObservableObject{
    @Published var book = [Items]()
    
    var searchText: String = "watermelon" {
        willSet(newValue) {
            print(newValue)
        }
    }
    
    
    func loadData() async {
        
        guard let url = URL(string: "https://www.googleapis.com/books/v1/volumes?q=(searchText)&key=API_KEY_PRIVATE") else {
            return
        }
                
        
                    do {
                        let (data, _) = try await URLSession.shared.data(from: url)
                        if let decodedResponse = try? JSONDecoder().decode(Books.self, from: data) {
                           book = decodedResponse.items
                        }
                   
                    } catch {
                        print("There is an error")
                    
                    }
                }
        

    }

2

Answers


  1. This is a perfect candidate for the Combine framework.

    In Network create a publisher which removes duplicates, debounces the input for 0.3 seconds, builds the URL, loads the data and decodes it.

    I don’t have your types, probably there are many errors. But this is a quite efficient way for dynamic searching. By the way your naming with regard to singular and plural form is pretty confusing.

    import Combine
    import SwiftUI
    
    class Network: ObservableObject {
        @Published var book = [Items]()
        @Published var query = ""
        
        private var subscriptions = Set<AnyCancellable>()
        
        init() {
            searchPublisher
                .sink { completion in
                    print(completion) // show the error to the user
                } receiveValue: { [weak.self] books in
                    self?.book = books.items
                }
                .store(in: &subscriptions)
    
        }
        
        var searchPublisher : AnyPublisher<Books,Error> {
            return $query
                .removeDuplicates()
                .debounce(for: 0.3, scheduler: RunLoop.main)
                .compactMap{ query -> URL? in
                    guard !query.isEmpty else { return nil }
                    guard let url = URL(string: "https://www.googleapis.com/books/v1/volumes?q=(query)&key=API_KEY_PRIVATE") else {
                        return nil
                    }
                    return url
                }
                .flatMap { url -> AnyPublisher<Data, URLError> in
                    return URLSession.shared.dataTaskPublisher(for: url)
                        .map(.data)
                        .eraseToAnyPublisher()
                }
                .decode(type: Books.self, decoder: JSONDecoder())
                .receive(on: DispatchQueue.main)
                .eraseToAnyPublisher()
        }
    }
    

    In the view create the view model (must be @StateObject!)

    @StateObject var network = Network()
    

    and bind searchable to query in network

    .searchable(text: $network.query)
    

    The view is updated when the data is available in network.book

    The .task modifier ist not needed

    Login or Signup to reply.
  2. There is another version of task that runs again when a value changes task(id:priority:_:). If a task is still running when the param changes it will be cancelled and restarted automatically. In your case use it as follows:

    .task(id: textSearch) { newValue in
       books = await getBooks(newValue)
    }
    

    Now we have async/await and task there is no need for an ObservableObject anymore.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search