swiftuiasync-awaitcombine

How to chain AnyPublisher with async/await in SwiftUI


A typical way to chain AnyPublisher is to use Combine operators like flatMap.


class MyService {
    func getUserList() -> AnyPublisher<[User], Error> {
        ....
    }

    func getPostList(user: User) -> AnyPublisher<[Post], Error> {
        ...
    }
}

class ViewModel: ObserableObject {
    let service = MyService()
    
    @published var post: [Post] = []
    
    func fetchAllPostFromLastUser() {
        service.getUserList().flatMap { [weak self] users in
            if let user = users.last {
                return self.service.getPosts(user: user)
            } else {
                return Fail(error:APIError.emptyUsers).eraseToAnyPublisher()
            }
        }
        .sink { result in
            
        }
    }
}

Is there a more elegant way to use async/await, so the code can be similar like

class ViewModel: ObserableObject {
    let service = MyService()
    
    @published var post: [Post] = []
    
    func fetchAllPostFromLastUser() {
        let users = await service.getUserList().somethingMagicToConvertPublisherToAsync()
        let posts = await service.getPostList(user: user.first).somethingMagicToConvertPublisherToAsync()
    }
}


Solution

  • To use async/await we need to convert Publisher into AsyncStream of values, so get rid of errors.

    So, it is possible to be like (tested with Xcode 13.4)

    @MainActor class ViewModel: ObserableObject {
        // ...
    
        func collectPosts() async {
            for await newUsers in service.getUserList().replaceError(with: []).values {
                for user in newUsers {
                    for await newPost in service.getPost(user: user).replaceError(with: []).values {
                        post.append(contentsOf: newPost)
                    }
                }
            }
        }
    

    and usage like

    struct ContentView: View {
        @StateObject var vm = ViewModel()
    
        var body: some View {
            YourViewHere()
                .task {
                    await vm.collectPosts()
                }
        }
    }