为 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
}
这是我想采用的方法的简化示例,但我无法让这个简单示例起作用。
我有一个 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
}