Home > Software engineering >  How to call API again after change was made?
How to call API again after change was made?

Time:06-19

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")
                    
                    }
                }
        

    }

CodePudding user response:

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

CodePudding user response:

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.

  • Related