3

我目前正在使用 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
        ]

    }

}
4

2 回答 2

0

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

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

希望这可以帮助。

于 2019-07-21T04:31:28.333 回答
0

此示例按预期工作,但我在此过程中发现您不能将原始代码中的模式(用于定义输入和输出的内部结构)与 @Published 一起使用。这会导致一些相当奇怪的错误(以及 Playground 中的 BAD_ACCESS),并且是在 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()
}
}
于 2019-07-22T08:25:35.547 回答