SwiftUI中如何使用async/await实现AnyPublisher链式调用
Combine Publisher 转 async/await 串行调用方案
原有实现方式
链式调用AnyPublisher的常规方式是使用flatMap等Combine运算符,对应实现代码如下:
class MyService { func getUserList() -> AnyPublisher<[User], Error> { // .... } func getPostList(user: User) -> AnyPublisher<[Post], Error> { // ... } } class ViewModel: ObservableObject { let service = MyService() @Published var post: [Post] = [] private var cancellables = Set<AnyCancellable>() func fetchAllPostFromLastUser() { service.getUserList().flatMap { [weak self] users in if let user = users.last { return self?.service.getPostList(user: user) ?? Fail(error: APIError.emptyUsers).eraseToAnyPublisher() } else { return Fail(error: APIError.emptyUsers).eraseToAnyPublisher() } } .sink(receiveCompletion: { completion in // 处理错误/完成逻辑 }, receiveValue: { [weak self] posts in self?.post = posts }) .store(in: &cancellables) } } enum APIError: Error { case emptyUsers }
期望实现效果
通过特定转换方法将Publisher转为可直接await调用的形式,按顺序串行获取所需数据,代码编写效果类似:
class ViewModel: ObservableObject { let service = MyService() @Published var post: [Post] = [] func fetchAllPostFromLastUser() async { let users = await service.getUserList().somethingMagicToConvertPublisherToAsync() let posts = await service.getPostList(user: users.last).somethingMagicToConvertPublisherToAsync() post = posts } }
原生实现方案
从iOS 15、macOS 12及以上系统开始,Combine已经原生提供了async/await适配能力,不需要引入第三方库,也不需要写复杂的自定义逻辑。
Publisher协议自带values属性,会把Publisher输出包装成一个AsyncThrowingSequence,对于业务中常见的「发起请求->返回单个结果->结束」的单值Publisher,直接取序列的第一个值即可拿到结果,配合try await就能实现串行调用。
基础用法
直接调用系统API的实现代码如下:
import Combine import SwiftUI class ViewModel: ObservableObject { let service = MyService() @Published var posts: [Post] = [] func fetchAllPostFromLastUser() async throws { // 等待用户列表接口返回 guard let users = try await service.getUserList().values.first else { throw CancellationError() } guard let lastUser = users.last else { throw APIError.emptyUsers } // 等待文章列表接口返回 guard let posts = try await service.getPostList(user: lastUser).values.first else { throw CancellationError() } // 刷新UI数据 self.posts = posts } }
调用时只需要把方法放在Task上下文里即可,比如在SwiftUI视图中可以直接用.task修饰符触发:
struct PostListView: View { @StateObject var viewModel = ViewModel() var body: some View { List(viewModel.posts, id: \.id) { post in // 渲染列表单元 } .task { do { try await viewModel.fetchAllPostFromLastUser() } catch { // 统一处理错误 print(error) } } } }
简化封装
如果觉得每次写.values.first比较繁琐,可以给Publisher写一个简单的扩展,封装成统一的单值转换方法,和期望的「魔法转换方法」效果完全一致:
extension Publisher where Failure == Error { /// 把单值输出的Publisher转成async返回值 func asyncSingleValue() async throws -> Output { guard let value = try await values.first else { throw CancellationError() } return value } }
封装后的调用代码非常简洁,逻辑完全线性:
func fetchAllPostFromLastUser() async throws { let users = try await service.getUserList().asyncSingleValue() guard let lastUser = users.last else { throw APIError.emptyUsers } let posts = try await service.getPostList(user: lastUser).asyncSingleValue() self.posts = posts }
方案优势
- 代码完全线性串行,没有闭包嵌套,可读性远高于flatMap链式写法
- 不需要手动维护
AnyCancellable集合,异步任务的生命周期由系统自动管理 - 错误处理直接用Swift原生的
do/catch逻辑,不需要在sink的completion回调里单独处理 - 完全基于系统原生API实现,没有额外依赖,稳定性有保障
内容的提问来源于stack exchange,提问作者LiangWang
相关产品推荐
相关产品推荐

