SwiftUI 设计模式 - 如何从第一个调用的结果进行"第二阶段"API 调用



这是在iOS 15.5上使用最新的SwiftUI标准。

我的SwiftUI应用程序中有以下两个结构:

用户.swift

struct User: Codable, Identifiable, Hashable {
let id: String
let name: String
var socialID: String? // it's a var so I can modify it later
func getSocialID() async -> String {
// calls another API to get the socialID using the user's id
// code omitted
// example response:
// {
//     id: "aaaa",
//     name: "User1",
//     social_id: "user_1_social_id",
// }        
}
}

视频.swift

struct Video: Codable, Identifiable, Hashable {
let id: String
let title: String
var uploadUser: User
}

我的SwiftUI应用程序显示了一个视频列表,视频列表是从API获取的(我无法控制),响应如下:

[
{
id: "AAAA",
title: "My first video. ",
uploaded_user: { id: "aaaa", name: "User1" },
},
{
id: "BBBB",
title: "My second video. ",
uploaded_user: { id: "aaaa", name: "User1" },
},
]

我的视频视图模型如下:

VideoViewModel.swift

@MainActor
class VideoViewModel: ObservableObject {
@Published var videoList: [Video]
func getVideos() async {
// code simplified
let (data, _) = try await URLSession.shared.data(for: videoApiRequest)
let decoder = getVideoJSONDecoder()

let responseResult: [Video] = try decoder.decode([Video].self, from: data)
self.videoList = responseResult
}
func getSocialIDForAll() async throws -> [String: String?] {
var socialList: [String: String?] = [:]

try await withThrowingTaskGroup(of: (String, String?).self) { group in
for video in self.videoList {
group.addTask {
return (video.id, try await video.uploadedUser.getSocialId())
}
}

for try await (userId, socialId) in group {
socialList[userId] = socialId
}
}

return socialList
}
}

现在,我希望为User结构体填充socialID字段,我必须使用每个用户的ID从另一个API获得该字段。每个用户的响应如下:

{
id: "aaaa",
name: "User1",
social_id: "user_1_social_id",
}

现在,获取信息的唯一可行方法似乎是使用withThrowingTaskGroup()并为每个用户调用getSocialID(),我现在正在使用它,然后我可以返回一个字典,其中包含每个用户的所有socialID信息,然后该字典可以在SwiftUI视图中使用。

但是,有没有一种方法可以让我在不使用单独字典的情况下填写User结构中的socialID字段?一旦JSON解码器初始化视频列表,我似乎无法修改videoList中每个Video中的User结构,因为VideoViewModelMainActor。我更喜欢一次性下载所有内容,这样当用户进入子视图时,就没有加载时间了。

结构初始化后不能修改,这是正确的,因为它们的所有属性都是let变量;但是,您可以在VideoViewModel中修改videoList,从而可以免除Dictionary

@MainActor
class VideoViewModel: ObservableObject {
@Published var videoList: [Video]
func getVideos() async {
// code simplified
let (data, _) = try await URLSession.shared.data(for: videoApiRequest)
let decoder = getVideoJSONDecoder()

let responseResult: [Video] = try decoder.decode([Video].self, from: data)
self.videoList = try await Self.getSocialIDForAll(in: responseResult)
}
private static func updatedWithSocialID(_ user: User) async throws -> User {
return User(id: user.id, name: user.name, socialID: try await user.getSocialID())
}
private static func updatedWithSocialID(_ video: Video) async throws -> Video {
return Video(id: video.id, title: video.title, uploadUser: try await updatedWithSocialID(video.uploadUser))
}
static func getSocialIDForAll(in videoList: [Video]) async throws -> [Video] {
return try await withThrowingTaskGroup(of: Video.self) { group in
videoList.forEach { video in
group.addTask {
return try await self.updatedWithSocialID(video)
}
}

var newVideos: [Video] = []
newVideos.reserveCapacity(videoList.count)

for try await video in group {
newVideos.append(video)
}

return newVideos
}
}
}

使用视图模型对象不是SwiftUI的标准,它更像是UIKit的设计模式,但实际上使用内置的子视图控制器更好。SwiftUI是围绕使用值类型来防止对象出现典型的一致性错误而设计的,因此如果使用对象,仍然会遇到这些问题。View结构被设计成主要的封装机制,因此使用View结构及其属性包装器会取得更大的成功。

因此,为了解决您的用例,您可以使用@State属性包装器,它像对象一样为View结构(具有值语义)提供引用类型语义,并使用它来保存与屏幕上的View匹配的生存期的数据。对于下载,您可以通过task(id:)修饰符使用async/await。这将在视图出现时运行任务,并在id参数更改时取消并重新启动任务。结合使用这两个功能,你可以做到:

@State var socialID
.task(id: videoID) { newVideoID in 
socialID = await Social.getSocialID(videoID: newViewID)
}

View应该有一个获取视频信息的任务。

最新更新