Displaying State of an Async Api call in SwiftUI
I would go a step further and add idle
and failed
states.
Then instead of throwing an error change the state to failed
and pass the error description. I removed the Double
value from the loading
state to just show a spinning ProgressView
@MainActor
class GoogleBooksApi: ObservableObject {
enum LoadingState {
case idle
case loading
case loaded(GoogleBook)
case failed(Error)
}
@Published var state: LoadingState = .idle
func fetchBook(id identifier: String) async {
var components = URLComponents(string: "https://www.googleapis.com/books/v1/volumes")
components?.queryItems = [URLQueryItem(name: "q", value: "isbn=\(identifier)")]
guard let url = components?.url else { state = .failed(URLError(.badURL)); return }
self.state = .loading
do {
let (data, _) = try await URLSession.shared.data(from: url)
let response = try JSONDecoder().decode(GoogleBook.self, from: data)
self.state = .loaded(response)
} catch {
state = .failed(error)
}
}
}
In the view you have to switch
on the state
and show different views.
And – very important – you have to declare the observable object as @StateObject
. This is a very simple implementation
struct ContentView: View {
@State var code = "ISBN"
@StateObject var api = GoogleBooksApi()
var body: some View {
VStack {
switch api.state {
case .idle: EmptyView()
case .loading: ProgressView()
case .loaded(let books):
if let info = books.items.first?.volumeInfo {
Text("Name: \(info.title)")
Text("Author: \(info.authors?.joined(separator: ", ") ?? "")")
Text("total: \(books.totalItems)")
}
case .failed(let error):
if error is DecodingError {
Text(error.description)
} else {
Text(error.localizedDescription)
}
}
Button(action: {
code = "978-0441013593"
Task {
await api.fetchBook(id: code)
}
}, label: {
Rectangle()
.frame(width: 200, height: 100)
.foregroundColor(.blue)
})
}
}
}
It seems like you're not initializing the GoogleBooksApi
.
@ObservedObject var api: GoogleBooksApi
neither any init where it can be modified.
Other than that - I'd suggest using @StateObject
(provided you deployment target is minimum iOS 14.0). Using ObservableObject
might lead to multiple initializations of the GoogleBooksApi
(whereas you need only once)
You should use
@StateObject
for any observable properties that you initialize in the view that uses it. If theObservableObject
instance is created externally and passed to the view that uses it mark your property with@ObservedObject
.