You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 09:54:25