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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 17:48:14