SwiftUI如何基于首次API结果发起二次请求填充模型属性
运行环境
- 系统版本:iOS 15.5
- 开发规范:遵循最新SwiftUI开发标准
现有代码定义
User.swift
struct User: Codable, Identifiable, Hashable { let id: String let name: String var socialID: String? // 可变属性,预留后续修改逻辑 func getSocialID() async -> String { // 调用独立接口,通过用户ID拉取对应socialID // 业务逻辑省略 // 接口响应示例: // { // id: "aaaa", // name: "User1", // social_id: "user_1_social_id", // } } }
Video.swift
struct Video: Codable, Identifiable, Hashable { let id: String let title: String var uploadUser: User }
VideoViewModel.swift
@MainActor class VideoViewModel: ObservableObject { @Published var videoList: [Video] func getVideos() async { // 逻辑简化 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 } }
接口说明
视频列表接口由第三方提供,无结构修改权限,响应格式如下:
[ { id: "AAAA", title: "My first video. ", uploaded_user: { id: "aaaa", name: "User1" }, }, { id: "BBBB", title: "My second video. ", uploaded_user: { id: "aaaa", name: "User1" }, }, ]
单用户socialID查询接口响应格式如下:
{ id: "aaaa", name: "User1", social_id: "user_1_social_id", }
问题描述
需要为User结构体填充socialID字段,字段值需要传入用户ID调用上述独立接口获取。要求所有数据一次性拉取完成,用户进入子视图时无加载等待。
当前通用方案是通过withThrowingTaskGroup()为每个用户调用getSocialID()方法,最终返回存储所有用户socialID映射关系的字典供视图层使用。现在需要确认:是否存在无需使用独立字典,即可直接填充User结构体中socialID字段的实现方案?
由于VideoViewModel标注了@MainActor,在JSON解码完成视频列表初始化后,似乎无法直接修改videoList中每个Video实例关联的User结构体。
实现方案
完全可以直接填充User结构体内的socialID字段,不需要额外维护视图层使用的映射字典。很多人误以为无法直接修改,本质是踩了跨线程修改@MainActor隔离属性的坑:如果在任务组的后台线程直接修改self.videoList的元素,会触发线程安全报错,只需要调整数据处理流程即可规避问题。
具体实现逻辑:
- 拉取视频列表后先做用户去重,同一用户可能关联多个视频,不需要重复发起请求
- 后台并发拉取所有去重后用户的socialID,不阻塞主线程
- 对解码得到的局部可变视频数组做字段填充,所有修改完成后,在MainActor上下文一次性赋值给
@Published属性,触发视图刷新
修改后的ViewModel代码如下:
@MainActor class VideoViewModel: ObservableObject { @Published var videoList: [Video] = [] @Published var isDataLoaded: Bool = false func loadAllVideoData() async { isDataLoaded = false do { // 1. 拉取基础视频列表 let (data, _) = try await URLSession.shared.data(for: videoApiRequest) let decoder = getVideoJSONDecoder() // 解码为局部可变变量,后续所有修改先在局部变量完成 var processedVideoList = try decoder.decode([Video].self, from: data) // 2. 提取不重复用户,减少无效请求 let uniqueUserDict = Dictionary(uniqueKeysWithValues: processedVideoList.map { ($0.uploadUser.id, $0.uploadUser) }) // 3. 后台并发拉取所有用户的socialID var socialIDMap: [String: String] = [:] try await withThrowingTaskGroup(of: (String, String).self) { group in for user in uniqueUserDict.values { group.addTask { let socialID = await user.getSocialID() return (user.id, socialID) } } // 汇总请求结果 for try await (userID, socialID) in group { socialIDMap[userID] = socialID } } // 4. 直接修改局部数组内的User字段,填充socialID for index in processedVideoList.indices { let currentUserID = processedVideoList[index].uploadUser.id processedVideoList[index].uploadUser.socialID = socialIDMap[currentUserID] } // 5. 主线程一次性赋值,所有数据已填充完成 self.videoList = processedVideoList isDataLoaded = true } catch { // 按需补充错误处理逻辑 isDataLoaded = false } } }
该方案完全符合要求:
- 不需要额外维护供视图使用的socialID映射字典,视图直接访问
video.uploadUser.socialID即可获取值 - 所有数据拉取、填充完成后才更新UI,用户进入子视图无加载等待
- 所有
@Published属性的修改都在MainActor上下文执行,符合线程安全要求,不会出现UI卡顿或数据竞争问题 - 对重复用户做了请求去重,请求量仅和视频列表的独立用户数挂钩,性能优于逐视频发起请求的实现
内容的提问来源于stack exchange,提问作者Skk
相关产品推荐
相关产品推荐

