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.