我的应用程序进行 API 调用来获取大量数据,如果是简单的少量数据,则可以使用
.task
修饰符,或者使用模型。令我有点惊讶的是,预览实际上运行了获取数据的代码。但是,许多 API 调用需要身份验证和要调用的服务器,在存储库中进行身份验证(即使它是测试帐户)似乎不是一个好的做法,并且将预览绑定到(测试或演示)服务器看起来也不太好。如何在获取数据的视图的预览中使用示例数据?
struct ListView: View {
@State private var books: [Book]
var body: some View {
List(books) { book in
Text(book.title)
}
.task {
books = await HTTPClient.fetchBooks()
}
}
}
struct ListView: View {
@State private var model = BookModel()
var body: some View {
List(model.books) { book in
Text(book.title)
}
.task {
await model.fetchBooks()
}
}
}
您应该创建一个像这样的协议:
protocol HTTPClient {
static func fetchBooks() async throws -> [Book]
}
并且该协议有两个实现:
enum RealHTTPClient: HTTPClient {
static func fetchBooks() async throws -> [Book] {
// ... do actual work here!
}
}
enum MockHTTPClient: HTTPClient {
static func fetchBooks() -> [Book] {
// return fake data here
[Book(title: "Book 1"), Book(title: "Book 2"), Book(title: "Book 3")]
}
}
然后,将其注入
Environment
。
extension EnvironmentValues {
// using the @Entry macro in iOS 18 for convenience
// you don't have to use @Entry. Just do it the old way if you are on a lower version
@Entry var httpClient: any HTTPClient.Type = MockHTTPClient.self
}
这里我将
MockHTTPClient
设置为默认值。这使得使用真正的客户选择加入。另外,我在这里使用元类型和 static
方法来避免并发问题。如果您需要 HTTP 客户端具有状态,则需要确保它们不会跨并发上下文传递,但这超出了本问题的范围。
在您看来,您可以这样做:
struct ContentView: View {
@State private var books: [Book] = []
@Environment(\.httpClient) var httpClient
var body: some View {
List(books) { book in
Text(book.title)
}
.task {
do {
books = try await httpClient.fetchBooks()
} catch {
print(error)
}
}
}
}
#Preview {
ContentView() // this will not call the real server because the default value of httpClient is MockHTTPClient.self
}
在根视图,你可以注入真正的HTTP客户端。
WindowGroup {
RootView()
}
.environment(\.httpClient, RealHTTPClient.self)
如果你想在
fetchBooks
中有一个 BookModel
方法,它应该以 HTTPClient.Type
作为参数:
@Observable
class BookModel {
var books = [Book]()
// don't worry about this @MainActor - nonisolated async operations
// you do in the body of fetchBooks is still running off of the MainActor
@MainActor
func fetchBooks(with httpClient: any HTTPClient.Type) async throws {
// as an example
books = try await httpClient.fetch(url: "https://example.com/books")
}
}