Swift Combine PassThroughSubject 发出的意外事件

Unexpected events emitted by Swift Combine PassThroughSubject

我目前正在使用 Combine 和 SwiftUI 并且已经使用 MVVM 模式构建了一个原型应用程序。该应用程序使用一个计时器,控制它的按钮的状态被(不优雅地)绑定到使用 PassThroughSubject 的视图模型。

按下按钮时,这应该会切换状态变量的值; this 的值被传递给视图模型的主题(使用.send),它应该在每次按下按钮时发送一个事件。然而,似乎有递归或一些同样奇怪的事情发生,因为多个事件被发送到主题并且运行时崩溃结果没有 UI 被更新。

这有点令人费解,我不确定这是 Combine 中的错误还是我遗漏了什么。任何指针将不胜感激。下面的代码 - 我知道它很乱 ;-) 我已经将它精简到看起来相关的部分,但如果您需要更多,请告诉我。

查看:

struct ControlPanelView : View {
    @State private var isTimerRunning = false
    @ObjectBinding var viewModel: ControlPanelViewModel

    var body: some View {
        HStack {
            Text("Case ID") // replace with binding to viewmode

            Spacer()
            Text("00:00:00") // repalce with binding to viewmodel

            Button(action: {
                self.isTimerRunning.toggle()
                self.viewModel.apply(.isTimerRunning(self.isTimerRunning))
                print("Button press")
            }) {
                isTimerRunning ? Image(systemName: "stop") : Image(systemName: "play")
            }

        }
//            .onAppear(perform: { self.viewModel.apply(.isTimerRunning(self.isTimerRunning)) })
            .font(.title)
            .padding(EdgeInsets(top: 0, leading: 32, bottom: 0, trailing: 32))
    }
}

视图模型:

final class ControlPanelViewModel: BindableObject, UnidirectionalDataType {

    typealias InputType = Input
    typealias OutputType = Output

    private let didChangeSubject = PassthroughSubject<Void, Never>()
    private var cancellables: [AnyCancellable] = []

    let didChange: AnyPublisher<Void, Never>

    // MARK:- Input
...
    private let isTimerRunningSubject = PassthroughSubject<Bool, Never>()
....


    enum Input {
...
        case isTimerRunning(Bool)
...
    }
    func apply(_ input: Input) {
        switch input {
...
        case .isTimerRunning(let state): isTimerRunningSubject.send(state)
...
        }
    }

    // MARK:- Output
    struct Output {
        var isTimerRunning = false
        var elapsedTime = TimeInterval(0)
        var concernId = ""
    }
    private(set) var output = Output() {
        didSet { didChangeSubject.send() }
    }

    // MARK:- Lifecycle
    init(timerService: TimerService = TimerService()) { 
        self.timerService = timerService

        didChange = didChangeSubject.eraseToAnyPublisher()

        bindInput()
        bindOutput()
    }

    private func bindInput() {
        utilities.debugSubject(subject: isTimerRunningSubject)

        let timerToggleStream = isTimerRunningSubject
            .subscribe(isTimerRunningSubject)

...

        cancellables += [
            timerToggleStream,
            elapsedTimeStream,
            concernIdStream
        ]
    }

    private func bindOutput() {
        let timerToggleStream = isTimerRunningSubject
            .assign(to: \.output.isTimerRunning, on: self)
...
        cancellables += [
            timerToggleStream,
            elapsedTimeStream,
            idStream
        ]

    }

}

在您的 bindInput 方法中,isTimerRunningSubject 订阅了自己。我怀疑这不是您想要的,并且可能解释了您所描述的奇怪递归。也许你在某处遗漏了 self.

同样奇怪的是,bindInputbindOutput 都将所有流都添加到 cancellables 数组中,所以它们会在那里出现两次。

希望这对您有所帮助。

这个例子按预期工作,但我在这个过程中发现你不能使用原始代码中的模式(内部结构来定义输入和输出)@Published。这会导致一些相当奇怪的错误(并且 BAD_ACCESS 在 Playground 中)并且是 Combine beta 3 中报告的错误。

final class ViewModel: BindableObject {

var didChange = PassthroughSubject<Void, Never>()

@Published var isEnabled = false

private var cancelled = [AnyCancellable]()

init() {
    bind()
}

private func bind() {
    let t = $isEnabled
        .map { _ in  }
        .eraseToAnyPublisher()
        .subscribe(didChange)

    cancelled += [t]
}
}

struct ContentView : View {
    @ObjectBinding var viewModel = ViewModel()

var body: some View {
    HStack {
        viewModel.isEnabled ? Text("Button ENABLED") : Text("Button disabled")
        Spacer()
        Toggle(isOn: $viewModel.isEnabled, label: { Text("Enable") })
    }
    .padding()
}
}