SwiftUI视图中无法设置订阅Cancellable的问题求助
SwiftUI中处理Combine Cancellable的正确方式
问题背景
在SwiftUI View中使用返回Publisher的网络服务时,尝试将订阅的Cancellable存入Set<AnyCancellable>集合时,触发编译错误:
"Cannot pass immutable value as inout argument: 'self' is immutable"
这是因为SwiftUI的View是值类型,直接修改其内部的Set<AnyCancellable>属性时,self处于不可变状态,无法完成修改。
原始代码示例
import SwiftUI import PlaygroundSupport import Combine import Foundation import Network struct Node: Identifiable { var id: String } class NetworkService { func startScan() -> AnyPublisher<[Node], Never> { return Just<[Node]>([Node(id: "mike")]) .delay(for: .seconds(1), scheduler: DispatchQueue(label: "test")) .eraseToAnyPublisher() } func stopScan() { } } struct ContentView: View { @State private var nodes = [Node]() var cancellables: Set<AnyCancellable> = [] var service = NetworkService() var body: some View { List() { Text("Hello, world!") .padding() Button("Start") { service.startScan() .assign(to: \.nodes, on: self) .store(in: &self.cancellables) // 触发编译错误 } Button("Stop") { service.stopScan() nodes.removeAll() } } List(nodes, id: \.id) { node in Text("node: \(node.id)") } .onReceive($nodes.publisher.receive(on: DispatchQueue.main)) { print("recieve data: \($0)") } } } PlaygroundPage.current.setLiveView(ContentView())
你的思路是否正确?
你提出的用类包装Set<AnyCancellable>的思路是可行的,但需要配合SwiftUI的属性包装器来确保生命周期正确,否则可能出现订阅意外失效的问题。
推荐解决方案
方案一:直接用@State包裹Set<AnyCancellable>
这是最简洁的方案,利用@State的引用类型包装特性,让值类型的View可以修改内部的集合:
struct ContentView: View { @State private var nodes = [Node]() @State private var cancellables = Set<AnyCancellable>() // 用@State修饰 var service = NetworkService() var body: some View { List() { Text("Hello, world!") .padding() Button("Start") { service.startScan() .receive(on: DispatchQueue.main) // 确保在主线程更新UI .assign(to: \.$nodes) // 使用投影绑定替代on:self .store(in: &cancellables) } Button("Stop") { service.stopScan() nodes.removeAll() cancellables.removeAll() // 手动取消所有订阅 } } List(nodes, id: \.id) { node in Text("node: \(node.id)") } .onReceive($nodes.publisher.receive(on: DispatchQueue.main)) { print("recieve data: \($0)") } } }
方案二:类包装+@StateObject
如果需要更灵活的订阅管理(比如在多个方法中操作订阅集合),可以用类包装Set<AnyCancellable>,并通过@StateObject持有,确保类实例的生命周期与View一致:
// 包装类 fileprivate class CancellableStore: ObservableObject { var cancellables = Set<AnyCancellable>() } struct ContentView: View { @State private var nodes = [Node]() @StateObject private var cancellableStore = CancellableStore() // 用@StateObject持有 var service = NetworkService() var body: some View { List() { Text("Hello, world!") .padding() Button("Start") { service.startScan() .receive(on: DispatchQueue.main) .assign(to: \.$nodes) .store(in: &cancellableStore.cancellables) } Button("Stop") { service.stopScan() nodes.removeAll() cancellableStore.cancellables.removeAll() } } List(nodes, id: \.id) { node in Text("node: \(node.id)") } .onReceive($nodes.publisher.receive(on: DispatchQueue.main)) { print("recieve data: \($0)") } } }
关键注意点
- 使用
\.nodes的投影绑定\.nodes替代assign(to: \.nodes, on: self),避免持有self的引用,更符合SwiftUI的最佳实践。 - 必须添加
receive(on: DispatchQueue.main),因为网络请求通常在后台线程,确保UI更新在主线程执行。 - 点击"Stop"按钮时,记得清空订阅集合,手动取消所有活跃的订阅。
内容的提问来源于stack exchange,提问作者Yogurt
相关产品推荐
相关产品推荐

