这是在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
结构,因为VideoViewModel
是MainActor
。我更喜欢一次性下载所有内容,这样当用户进入子视图时,就没有加载时间了。
结构初始化后不能修改,这是正确的,因为它们的所有属性都是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
应该有一个获取视频信息的任务。