使用Combine的flatMap且多订阅者时,Future代码块被重复调用
这个问题的核心原因其实是Combine里的Future属于「冷订阅者(Cold Publisher)」——每一次对它的订阅,都会重新触发它内部闭包的执行逻辑。你这里对authenticateAndDownloadUserInfo返回的Publisher做了两次订阅(一次是cancellable1的sink,另一次是showActivityIndicatorWhileWaiting里的sink),所以flatMap里的downloadUserInfo(它返回的也是Future)会被执行两次。
最简洁的解决方案:使用share()操作符
share()可以把冷Publisher转换成可共享的热Publisher,让多个订阅者共用同一个订阅流,这样整个异步逻辑只会执行一次。你有两种修改方式:
方式1:在组合流的末尾添加share()
修改authenticateAndDownloadUserInfo函数:
func authenticateAndDownloadUserInfo(username: String, password: String) -> some Publisher { return authenticate(username: username, password: password) .flatMap { isAuthenticated -> Future<String, ServerErrors> in guard isAuthenticated else { return Future {$0(.failure(.authenticationFailed)) } } return downloadUserInfo(username: username) } .share() // 添加这行,共享订阅流 }
方式2:在订阅前添加share()
如果不想修改原函数,也可以在创建future变量时添加:
let future = authenticateAndDownloadUserInfo(username: "stack", password: "overflow").share()
修改后运行代码,输出里的Downloading user info就只会出现一次,符合预期。
进阶方案:使用Multicast手动控制订阅时机
如果需要更精确地控制订阅触发时机(比如先添加完所有订阅者,再执行异步逻辑),可以用multicast配合PassthroughSubject:
// 创建可连接的Publisher let future = authenticateAndDownloadUserInfo(username: "stack", password: "overflow") .multicast(subject: PassthroughSubject<String, ServerErrors>()) // 先添加所有订阅者 let cancellable2 = future.showActivityIndicatorWhileWaiting(message: "Please wait downloading") let cancellable1 = future.sink(receiveCompletion: { (completion) in switch completion { case .finished: print("Completed without errors.") case .failure(let error): print("received error: '\(error)' ") } }) { (output) in print("received userInfo: '\(output)' ") } // 手动触发整个流的执行 let connectCancellable = future.connect()
这种方式适合需要批量添加订阅者后再发起请求的场景,避免因为订阅顺序导致的事件丢失。
关于你的临时方案的说明
你用额外Future包裹flatMap的方案之所以有效,是因为外层的Future只会被订阅一次,内部的flatMap流共享这次订阅,相当于手动实现了类似share()的效果。但share()是Combine提供的标准操作符,更简洁通用,也更符合Combine的设计思路。
补充:冷/热Publisher的区别
- 冷Publisher(比如
Future、URLSession.DataTaskPublisher):每个订阅者都会触发一次完整的逻辑执行,相当于每个订阅都新建一个"任务"。 - 热Publisher(比如
PassthroughSubject、CurrentValueSubject,或者用share()/multicast转换后的Publisher):不管有多少订阅者,逻辑只会执行一次,订阅者接收的是当前或后续产生的事件。
内容的提问来源于stack exchange,提问作者Nick Ager

