为 SwiftUI View 订阅合并发布者的每次更新

Subscribe SwiftUI View to every update of a combine publisher

这是我想采用的方法的简化示例,但我无法让这个简单示例起作用。

我有一个 Combine 发布者,其主题是视图模型状态:

struct State {
    let a: Bool
    let b: Bool
    let transition: Transition?
}

该州包括 transition 属性。这描述了 State 为了成为当前状态所做的 Transition

enum Transition {
    case onAChange, onBChange
}

我想使用 transition 属性 来驱动订阅发布者的 View 中的动画,以便不同的过渡以特定方式进行动画处理。

查看代码

这是视图的代码。您可以看到它如何尝试使用 transition 选择要更新的动画。

struct TestView: View {
    
    let model: TestViewModel

    @State private var state: TestViewModel.State
    private var cancel: AnyCancellable?

    init(model: TestViewModel) {
        self.model = model
        self._state = State(initialValue: model.state.value)
        self.cancel = model.state.sink(receiveValue: updateState(state:))
    }
    
    var body: some View {
        VStack(spacing: 20) {
            Text("AAAAAAA").scaleEffect(state.a ? 2 : 1)
            Text("BBBBBBB").scaleEffect(state.b ? 2 : 1)
        }
        .onTapGesture {
            model.invert()
        }
    }
    
    private func updateState(state: TestViewModel.State) {
        withAnimation(animation(for: state.transition)) {
            self.state = state
        }
    }

    private func animation(for transition: TestViewModel.Transition?) -> Animation? {
        guard let transition = transition else { return nil }

        switch transition {
        case .onAChange: return .easeInOut(duration: 1)
        case .onBChange: return .easeInOut(duration: 2)
        }
    }
}

struct TestView_Previews: PreviewProvider {
    static var previews: some View {
        TestView(model: TestViewModel())
    }
}

型号代码

final class TestViewModel: ObservableObject {
    
    var state = CurrentValueSubject<State, Never>(State(a: false, b: false, transition: nil))
    
    struct State {
        let a: Bool
        let b: Bool
        let transition: Transition?
    }
    
    enum Transition {
        case onAChange, onBChange
    }

    func invert() {
        let oldState = state.value
        setState(newState: .init(a: !oldState.a, b: oldState.b, transition: .onAChange))
        setState(newState: .init(a: !oldState.a, b: !oldState.b, transition: .onBChange))
    }
    
    private func setState(newState: State) {
        state.value = newState
    }
}

在模型代码中可以看到,调用invert()时,会发生两次状态变化。该模型首先使用 .onAChange 转换切换 a,然后使用 .onBChange 转换切换 b

应该发生什么

当这是 运行 时应该发生的是每次单击视图时,文本“AAAAAAA”和“BBBBBBB”应该切换大小。但是,“AAAAAAA”文本应该快速变化(1 秒)而“BBBBBBB”文本应该缓慢变化(2 秒)。

实际发生了什么

但是,当我 运行 点击视图时,视图根本没有更新。

我可以从调试器中看到调用了 onTapGesture { … } 并且正在模型上调用 invert()updateState(state:) 也被调用。然而,TestView 在屏幕上没有变化,并且 body 没有被再次调用。

我试过的其他东西

使用回调

我尝试将模型中的回调函数设置为视图的 updateState(state:) 函数,而不是使用发布者将事件发送到视图。我在视图的 init 中用 model.handleUpdate = self.update(state:) 分配给了它。同样,这没有用。正如预期的那样,函数 invert()update(state:) 被调用,但视图实际上并没有改变。

使用@ObservedObject

我将模型更改为 ObservableObject,其 state@Published。我将视图设置为具有模型的 @ObservedOject。有了这个,视图 确实 更新了,但是它使用相同的动画更新了两段文本,这是我不想要的。似乎两个状态更新被压扁了,它只看到最后一个,并使用了那个的 transition

确实有用的东西——有点

最后,我尝试直接将模型的 invert() 函数复制到视图的 onTapGesture 处理程序中,以便视图直接更新自己的状态。这确实有效!这很重要,但我不想将所有模型更新逻辑放在我的视图中。

问题

我如何让 SwiftUI 视图订阅 all 声明模型通过其发布者发送,以便它可以使用 transition 属性控制用于该状态更改的动画的状态?

向发布者订阅视图的方式是使用 .onRecieve(_:perform:),因此不要在 init 中保存可取消项,而是这样做:

var body: some View {
   VStack(spacing: 20) {
      Text("AAAAAAA").scaleEffect(state.a ? 2 : 1)
      Text("BBBBBBB").scaleEffect(state.b ? 2 : 1)
   }
   .onTapGesture {
      model.invert()
   }
   .onReceive(model.state, perform: updateState(state:)) // <- here
}