简体   繁体   English

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

[英]SwiftUI design pattern - How to make a "2nd-stage" API call from the result of the first one

This is on iOS 15.5 using the latest SwiftUI standards.这是在 iOS 15.5 上使用最新的 SwiftUI 标准。

I have these two structs in my SwiftUI application:我的 SwiftUI 应用程序中有这两个结构:

User.swift用户.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",
        // }        
    }
}

Video.swift视频.swift

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

My SwiftUI application displays a list of videos, the list of videos are obtained from an API (which I have no control over), the response looks like this:我的 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" },
    },
]

My video's view model looks like this:我的视频的视图模型如下所示:

VideoViewModel.swift 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
    }
}

Now, I wish to fill in the socialID field for the User struct, which I must obtain from another API using each user's ID.现在,我希望填写User结构的socialID字段,我必须使用每个用户的 ID 从另一个 API 获取该字段。 the response looks like this for each user:每个用户的响应如下所示:

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

Right now the only viable way to get the information seems to be using withThrowingTaskGroup() and call getSocialID() for each user, which I am using right now, then I can return a dictionary that contains all the socialID information for each user, then the dictionary can be used in SwiftUI views.现在,获取信息的唯一可行方法似乎是使用withThrowingTaskGroup()并为我正在使用的每个用户调用getSocialID() ,然后我可以返回一个包含每个用户的所有socialID信息的字典,然后该字典可以在 SwiftUI 视图中使用。

But, is there a way for me to fill in the socialID field in the User struct without having to use a separate dictionary?但是,有没有办法让我填写User结构中的socialID字段而无需使用单独的字典? It doesn't seem like I can modify the User struct in each Video inside videoList once the JSON decoder initializes the list of videos, due to the fact that VideoViewModel is a MainActor .一旦 JSON 解码器初始化视频列表,我似乎无法修改videoList中每个Video中的User结构,因为VideoViewModelMainActor I would prefer to have everything downloaded in one go, so that when the user enters a subview, there is no loading time.我宁愿一次性下载所有内容,这样当用户进入子视图时,就没有加载时间了。

You are correct that you can't modify the structs once they are initialized, because all of their properties are let variables;你是对的,一旦初始化就不能修改结构,因为它们的所有属性都是let变量; however, you can modify the videoList in VideoViewModel , allowing you to dispense with the Dictionary .但是,您可以修改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
        }
    }
}

Using a view model object is not standard for SwiftUI, it's more of a UIKit design pattern but actually using built-in child view controllers was better.使用视图模型对象不是 SwiftUI 的标准,它更像是一种 UIKit 设计模式,但实际上使用内置的子视图控制器更好。 SwiftUI is designed around using value types to prevent the consistency errors typical for objects so if you use objects then you will still get those problems. SwiftUI 的设计是围绕使用值类型来防止对象典型的一致性错误,所以如果你使用对象,那么你仍然会遇到这些问题。 The View struct is designed to be the primary encapsulation mechanism so you'll have more success using the View struct and its property wrappers. View 结构被设计为主要的封装机制,因此您可以使用 View 结构及其属性包装器获得更大的成功。

So to solve your use case, you can use the @State property wrapper, which gives the View struct (which has value semantics) reference type semantics like an object would, use this to hold the data that has a lifetime matching the View on screen.因此,为了解决您的用例,您可以使用@State属性包装器,它为 View 结构(具有值语义)提供引用类型语义,就像一个对象一样,使用它来保存生命周期与屏幕上的 View 匹配的数据. For the download, you can use async/await via the task(id:) modifier.对于下载,您可以通过task(id:)修饰符使用 async/await。 This will run the task when the view appears and cancel and restart it when the id param changes.这将在视图出现时运行任务,并在id参数更改时取消并重新启动它。 Using these 2 features together you can do:结合使用这 2 个功能,您可以执行以下操作:

@State var socialID

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

The parent View should have a task that got the video infos.View应该有一个获取视频信息的任务。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM