【问题标题】:Unexpected events emitted by Swift Combine PassThroughSubjectSwift Combine PassThroughSubject 发出的意外事件
【发布时间】:2019-07-14 21:23:53
【问题描述】:

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

    }

}

【问题讨论】:

  • 我不是专家,所以请考虑到这一点。是什么引起了我的注意你的按钮。为什么它要更新 both isTimerRunning ...在您的 viewModel 中的某些内容?为什么不只是 (1) 摆脱 apply 和“本地”更新,而是 (2) 直接更新您的 viewModel.isTimerRunning?如果您不想直接在按钮操作中执行此操作,请在您的视图中创建一个本地 func 并从那里调用它。底线,如果实际所有者是模型,为什么你的 ControlPanelViewisTimerRunning 存在?
  • 我不介意你所说的“凌乱”的代码,但请考虑添加缺失的部分,以便其他人实际编译和复制它。这将增加您获得正确答案的机会,并可能获得解决方案。还要注意,很多时候,创建一个最小的例子来展示一个问题,会引导你取得突破。您甚至可能不需要一开始就发布您的问题。
  • 感谢你们两位 cmets - 这是有用的反馈。是的,视图模型绑定破坏了这种模式的对象,并且可能引入了一些意想不到的行为。我会在第一个实例中尝试纠正这个问题,它可以解决问题。创建一个最小示例的建议是一个绝妙的主意,我肯定会在此范围内运行。

标签: swift swiftui combine


【解决方案1】:

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

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

希望这会有所帮助。

【讨论】:

  • 我创建了一个不受递归影响的“基本”演示(将尝试并附加),但您的建议似乎最有可能。关于可取消数组,您必须保留对发布的引用以将它们保持在范围内(如果需要,它还会为您提供一个“句柄”来取消事物)所以我相信没关系。我倾向于认为它有点像 RxSwift 的 DisposeBag
  • 是的,但是如果您将它们添加到 bindInputbindOutput 中,它们将被添加两次。在您展示的代码中,它是一个数组。也许 DisposeBag 是 RxSwift 中的某种 Set(抱歉不熟悉 RxSwift)
  • 是的,你是对的,但我认为自订阅是递归的来源,而不是可取消数组。不是我最好的代码! :-)
  • 是的,只是想确保您知道其中的含义(使用数组)。至于组合 - 我们都在学习......祝你的项目好运!
【解决方案2】:

此示例按预期工作,但在此过程中我发现您无法将原始代码中的模式(用于定义输入和输出的内部结构)与 @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()
}
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多