Combine Publisher的.sink仅执行一次问题及多次接收发布者值的解决方案咨询
你遇到的问题核心不是 .sink 只能被使用一次——实际上每次调用 .sink 都会创建一个全新的订阅,完全可以多次使用。问题出在你的订阅时机、发布者设计,以及刷新逻辑的配合上。
为什么初始化时就执行了 onRefresh 里的打印?
你的 myCurrentRatingsList 是一个 lazy var,只有当它第一次被访问时才会初始化。当 ViewModel 初始化后,某个逻辑(可能是 View 里的绑定触发)调用了 onRefresh,导致 myCurrentRatingsList 被初始化,同时订阅了 ratingsList 发布者。如果 ratingsList 此时已经有初始值(比如本地缓存的数据),订阅会立刻收到这个值,所以你看到了初始化时的打印。
为什么下拉刷新时没有打印输出?
当你下拉触发 refreshable 时,你确实调用了 onRefresh,但此时:
myCurrentRatingsList已经是初始化完成的共享发布者,如果你没有触发myCurrentRatingsRepository.ratingsList发出新的值(比如没执行实际的刷新数据请求),新的订阅不会收到任何新事件;- 就算你执行了刷新操作,如果
ratingsList没有更新并发布新值,订阅自然也不会有输出; - 每次调用
onRefresh都创建新订阅,但这些订阅如果没收到新事件,就不会执行闭包里的打印逻辑。
正确的实现方式
方案一:初始化时单次订阅,触发数据源更新
这是最推荐的方式,能避免重复订阅,让数据流更清晰:
class RatingsViewModel: ObservableObject { @Published var refreshed: Bool = false @Published var displayedRatings: [RatingsListSection: [RatingsListRow]] = [:] private let myCurrentRatingsRepository: RatingsRepository private var cancellableSet = Set<AnyCancellable>() private lazy var myCurrentRatingsList: AnyPublisher<[RatingsListSection: [RatingsListRow]], Never> = { myCurrentRatingsRepository.ratingsList .share() .replaceError(with: [:]) .eraseToAnyPublisher() }() init(repository: RatingsRepository) { self.myCurrentRatingsRepository = repository // 初始化时完成订阅,持续接收发布者的新值 myCurrentRatingsList .sink(receiveValue: { [weak self] ratingsList in guard let self = self else { return } print("Refreshed state: \(self.refreshed)") // 更新用于UI显示的数据 self.displayedRatings = ratingsList // 刷新完成后重置状态 self.refreshed = false }) .store(in: &cancellableSet) } func onRefresh() { // 设置刷新状态,触发UI上的刷新动画 refreshed = true // 通知Repository执行实际的数据刷新,触发ratingsList发布新值 myCurrentRatingsRepository.fetchLatestRatings() } }
在你的 Repository 中,fetchLatestRatings 方法需要执行实际的刷新操作(比如网络请求),并更新 ratingsList 发布者的值,这样订阅就能收到新的输出。
方案二:每次刷新订阅单次请求的发布者
如果你的刷新逻辑是每次发起独立请求,不需要持续监听数据变化,可以让 Repository 返回单次请求的发布者,再在 onRefresh 中订阅:
// 修改Repository的方法,返回单次请求的发布者 class RatingsRepository { func fetchLatestRatings() -> AnyPublisher<[RatingsListSection: [RatingsListRow]], Error> { // 这里写实际的网络请求/数据更新逻辑,返回新的发布者 URLSession.shared.dataTaskPublisher(for: ratingsURL) .map(\.data) .decode(type: [RatingsListSection: [RatingsListRow]].self, decoder: JSONDecoder()) .eraseToAnyPublisher() } } // ViewModel中的onRefresh方法 func onRefresh() { refreshed = true myCurrentRatingsRepository.fetchLatestRatings() .replaceError(with: [:]) .sink(receiveValue: { [weak self] ratingsList in guard let self = self else { return } print("Refreshed state: \(self.refreshed)") self.displayedRatings = ratingsList self.refreshed = false }) .store(in: &cancellableSet) }
这种方式每次刷新都会创建对应单次请求的订阅,适合不需要持续监听数据变化的场景。
总结
.sink 完全可以被多次调用,每次调用都会生成新的订阅。你的问题本质是没有让发布者在刷新时发出新值,同时订阅时机不合理导致了初始化时的意外触发。调整订阅逻辑和数据源的更新触发方式,就能解决问题。
内容的提问来源于stack exchange,提问作者Vader20FF

