错误是指出API返回的数组为Plant
,即[Plant]
,因此请相应更改方法签名:
func fetchPlantsFromAPI() async throws -> [Plant] {…}
然后将@State
变量改为数组:
struct ContentView: View {
@State var plants: [Plant] = []
var body: some View {
List(plants) { plant in
Text(plant.commonName)
}
.padding(20.0)
.task {
do {
plants = try await fetchPlantsFromAPI()
} catch {
print(error)
plants = []
}
}
}
}
我宣布Plant
是Identifiable
和Sendable
:
struct Plant: Codable, Identifiable, Sendable {…}
测试这个,我看到了一些额外的问题:
我会使用URLComponents
来确保查询正确转义:
func fetchPlantsFromAPI() async throws -> [Plant] {
guard var components = URLComponents(string: "https://trefle.io/api/v1/plants") else {
throw URLError(.badURL)
}
components.queryItems = [
URLQueryItem(name: "token", value: "…"),
URLQueryItem(name: "filter[common_name]", value: "beach strawberry")
]
guard let url = components.url else {
throw URLError(.badURL)
}
let (data, _) = try await URLSession.shared.data(from: url)
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let decoded = try decoder.decode(PlantResponse.self, from: data)
return decoded.data
}
我会避免try!
.正如你在上面修改过的例子中看到的,try
就足够了.如果出现编码错误,您不希望应用程序崩溃.
如果您在catch
中所做的一切只是重新抛出错误,那么在do
-try
-catch
中没有意义.同样,我已经从上面的第一点中删除了这一点.
您已将Plant
属性添加到Plant
类型.你应该删除:
struct Plant: Codable, Identifiable, Sendable {
let id: Int
let commonName: String // camelCase
let slug: String
let scientificName: String // camelCase
let year: Int
let bibliography: String
let author: String
let status: String
let rank: String
let familyCommonName: String // camelCase
let family: String
let genusId: Int // camelCase
let genus: String
let imageUrl: String // camelCase
let synonyms: [String] // an array of strings
let links: Links // a custom type, defined below
// let Plant: [Plant]
}
如果我们看一下JSON,植物数组中没有Plant
键:
{
"data": [
{
"id": 263319,
"common_name": "Beach strawberry",
"slug": "fragaria-chiloensis",
"scientific_name": "Fragaria chiloensis",
"year": 1768,
"bibliography": "Gard. Dict. ed. 8 : n.° 4 (1768)",
"author": "(L.) Mill.",
"status": "accepted",
"rank": "species",
"family_common_name": "Rose family",
"genus_id": 12147,
"image_url": "https://bs.plantnet.org/image/o/8ee87e6f94833055db1c7df5fc07761852b7b1eb",
"synonyms": [
"Fragaria vesca var. chiloensis",
"Potentilla chiloensis"
],
"genus": "Fragaria",
"family": "Rosaceae",
"links": {
"self": "/api/v1/species/fragaria-chiloensis",
"plant": "/api/v1/plants/fragaria-chiloensis",
"genus": "/api/v1/genus/fragaria"
}
}
],
"links": {
"self": "/api/v1/plants?filter%5Bcommon_name%5D=beach+strawberry",
"first": "/api/v1/plants?filter%5Bcommon_name%5D=beach+strawberry\u0026page=1",
"last": "/api/v1/plants?filter%5Bcommon_name%5D=beach+strawberry\u0026page=1"
},
"meta": {
"total": 1
}
}
我会解码API错误,这样我们就可以优雅地处理它们.例如,
struct ErrorResponse: Codable {
let error: Bool
let messages: String
}
enum PlantError: Error {
case apiError(Int, String)
case invalidResponse(URLResponse)
}
func fetchPlantsFromAPI() async throws -> [Plant] {
guard var components = URLComponents(string: "https://trefle.io/api/v1/plants") else {
throw URLError(.badURL)
}
components.queryItems = [
URLQueryItem(name: "token", value: "…"),
URLQueryItem(name: "filter[common_name]", value: "beach strawberry")
]
guard let url = components.url else {
throw URLError(.badURL)
}
let (data, response) = try await URLSession.shared.data(from: url)
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
guard let httpResponse = response as? HTTPURLResponse else {
throw PlantError.invalidResponse(response)
}
guard 200...299 ~= httpResponse.statusCode else {
let errorObject = try decoder.decode(ErrorResponse.self, from: data)
throw PlantError.apiError(httpResponse.statusCode, errorObject.messages)
}
return try decoder
.decode(PlantResponse.self, from: data)
.data
}
FWIW,我倾向于使API代码泛型,这样它就可以与任何端点一起工作(这样我们就不必到处重复这些代码).也许:
struct SuccessResponse<T: Decodable>: Decodable {
let data: T
}
struct ErrorResponse: Decodable {
let error: Bool
let messages: String
}
struct Plant: Codable, Identifiable, Sendable {
let id: Int
let commonName: String // camelCase
let slug: String
let scientificName: String // camelCase
let year: Int
let bibliography: String
let author: String
let status: String
let rank: String
let familyCommonName: String // camelCase
let family: String
let genusId: Int // camelCase
let genus: String
let imageUrl: String // camelCase
let synonyms: [String] // an array of strings
let links: Plant.Links // a custom type, defined below
}
extension Plant {
struct Links: Codable {
let `self`: String
let plant: String
let genus: String
}
}
enum ApiError: Error {
case apiError(Int, String)
case invalidResponse(URLResponse)
}
func urlForPlantSearch(_ string: String) throws -> URL {
guard var components = URLComponents(string: "https://trefle.io/api/v1/plants") else {
throw URLError(.badURL)
}
components.queryItems = [
URLQueryItem(name: "token", value: apiToken),
URLQueryItem(name: "filter[common_name]", value: string)
]
guard let url = components.url else {
throw URLError(.badURL)
}
return url
}
func fetchPlantsFromAPI() async throws -> [Plant] {
let url = try urlForPlantSearch("beach strawberry")
return try await fetchFromAPI(url: url)
}
// Make this generic so we can reuse this code with any query
private func fetchFromAPI<T: Decodable>(url: URL) async throws -> T {
let (data, response) = try await URLSession.shared.data(from: url)
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
guard let httpResponse = response as? HTTPURLResponse else {
throw ApiError.invalidResponse(response)
}
guard 200...299 ~= httpResponse.statusCode else {
let errorObject = try decoder.decode(ErrorResponse.self, from: data)
throw ApiError.apiError(httpResponse.statusCode, errorObject.messages)
}
return try decoder
.decode(SuccessResponse<T>.self, from: data)
.data
}