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 the ObservableObject instance is created externally and passed to the view that uses it mark your property with @ObservedObject.