skip to Main Content

This is on iOS 15.5 using the latest SwiftUI standards.

I have these two structs in my SwiftUI application:

User.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

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:

[
    {
        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

@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. 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.

But, is there a way for me to fill in the socialID field in the User struct without having to use a separate dictionary? 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. I would prefer to have everything downloaded in one go, so that when the user enters a subview, there is no loading time.

2

Answers


  1. You are correct that you can’t modify the structs once they are initialized, because all of their properties are let variables; however, you can modify the videoList in VideoViewModel, allowing you to dispense with the 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
            }
        }
    }
    
    Login or Signup to reply.
  2. 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 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. 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.

    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. For the download, you can use async/await via the task(id:) modifier. This will run the task when the view appears and cancel and restart it when the id param changes. Using these 2 features together you can do:

    @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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search