0

这个问题建立在我之前的问题之上。基本上,当按下某个按钮时,我会对 Google Books Api 进行异步调用。虽然当它是 View 的一种方法时我得到了调用,但是我想在它加载时覆盖一个活动指示器。因此,我尝试制作一个 ObservableObject 来进行调用,但我不知道该怎么做。

这是我到目前为止所拥有的:

class GoogleBooksApi: ObservableObject {
    
    enum LoadingState<Value> {
        case loading(Double)
        case loaded(Value)
    }
    
    @Published var state: LoadingState<GoogleBook> = .loading(0.0)
    
    enum URLError : Error {
        case badURL
    }

    func fetchBook(id identifier: String) async throws {
        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 { throw URLError.badURL }
        self.state = .loading(0.25)
        
        let (data, _) = try await URLSession.shared.data(from: url)
        self.state = .loading(0.75)
        self.state = .loaded(try JSONDecoder().decode(GoogleBook.self, from: data))
    }
}


struct ContentView: View {
    @State var name: String = ""
    @State var author: String = ""
    @State var total: String = ""
    
    @State var code = "ISBN"
    
    @ObservedObject var api: GoogleBooksApi
    
    var body: some View {
        VStack {
            Text("Name: \(name)")
            Text("Author: \(author)")
            Text("total: \(total)")

            Button(action: {
                code = "978-0441013593"
                Task {
                    do {
                        try await api.fetchBook(id: code)
                        let fetchedBooks = api.state
                        let book = fetchedBooks.items[0].volumeInfo
                        name = book.title
                        author = book.authors?[0] ?? ""
                        total = String(book.pageCount!)
                    } catch {
                        print(error)
                    }
                }
            }, label: {
                Rectangle()
                    .frame(width: 200, height: 100)
                    .foregroundColor(.blue)
            })
        }
    }
}

// MARK: - GoogleBook
struct GoogleBook: Codable {
    let kind: String
    let totalItems: Int
    let items: [Item]
}

// MARK: - Item
struct Item: Codable {
    let id, etag: String
    let selfLink: String
    let volumeInfo: VolumeInfo
}

// MARK: - VolumeInfo
struct VolumeInfo: Codable {
    let title: String
    let authors: [String]?
    let pageCount: Int?
    let categories: [String]?

    enum CodingKeys: String, CodingKey {
        case title, authors
        case pageCount, categories
    }
}

这就是没有加载状态的工作:

struct ContentView: View {
    @State var name: String = ""
    @State var author: String = ""
    @State var total: String = ""
    
    @State var code = "ISBN"
    
    enum URLError : Error {
        case badURL
    }

    private func fetchBook(id identifier: String) async throws -> GoogleBook {
        guard let encodedString = "https://www.googleapis.com/books/v1/volumes?q={\(identifier)}"
                                  .addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
              let url = URL(string: encodedString) else { throw URLError.badURL}
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode(GoogleBook.self, from: data)
    }
    
    var body: some View {
        VStack {
            Text("Name: \(name)")
            Text("Author: \(author)")
            Text("total: \(total)")

            Button(action: {
                code = "978-0441013593"
                Task {
                    do {
                        let fetchedBooks = try await fetchBook(id: code)
                        let book = fetchedBooks.items[0].volumeInfo
                        name = book.title
                        author = book.authors?[0] ?? ""
                        total = String(book.pageCount!)
                    } catch {
                        print(error)
                    }
                }
            }, label: {
                Rectangle()
                    .frame(width: 200, height: 100)
                    .foregroundColor(.blue)
            })
        }
    }
}

// MARK: - GoogleBook
struct GoogleBook: Codable {
    let kind: String
    let totalItems: Int
    let items: [Item]
}

// MARK: - Item
struct Item: Codable {
    let id, etag: String
    let selfLink: String
    let volumeInfo: VolumeInfo
}

// MARK: - VolumeInfo
struct VolumeInfo: Codable {
    let title: String
    let authors: [String]?
    let pageCount: Int?
    let categories: [String]?

    enum CodingKeys: String, CodingKey {
        case title, authors
        case pageCount, categories
    }
}
4

2 回答 2

1

我会更进一步并添加idlefailed状态。

然后不要抛出错误,而是将状态更改为failed并传递错误描述。我Double从状态中删除了值loading以显示旋转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)
        }
    }
}

在视图中你必须switchstate和显示不同的视图。而且——非常重要——你必须将 observable 对象声明为@StateObject. 这是一个非常简单的实现

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)
              })
          }
      }
}
于 2022-01-13T08:58:48.600 回答
1

好像你没有初始化GoogleBooksApi.

@ObservedObject var api: GoogleBooksApi

没有任何可以修改的init。

除此之外 - 我建议使用@StateObject(前提是您的部署目标是最低 iOS 14.0)。使用ObservableObject可能会导致多次初始化GoogleBooksApi(而您只需要一次)

您应该使用@StateObject在使用它的视图中初始化的任何可观察属性。如果ObservableObject实例是在外部创建并传递给使用它的视图,则使用 . 标记您的属性@ObservedObject

于 2022-01-13T08:39:42.187 回答