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

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)")
        }
    }
}

关键注意点

  1. 使用\.nodes的投影绑定\.nodes替代assign(to: \.nodes, on: self),避免持有self的引用,更符合SwiftUI的最佳实践。
  2. 必须添加receive(on: DispatchQueue.main),因为网络请求通常在后台线程,确保UI更新在主线程执行。
  3. 点击"Stop"按钮时,记得清空订阅集合,手动取消所有活跃的订阅。

内容的提问来源于stack exchange,提问作者Yogurt

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 14:19:56