如何在 SwiftUI 中准备后从外部切换视图动画?

4
我正在使用SwiftUI构建一个UI组件,它需要外部触发动画并做一些内部准备。以下示例中的函数 prepareArray() 用于此目的。
我最初的方法是使用绑定(bindings),但我发现没有办法监听 @Binding 变量的更改以触发某些操作:
struct ParentView: View {
    @State private var animated: Bool = false

    var body: some View {
        VStack {
            TestView(animated: $animated)
            Spacer()
            Button(action: {
                self.animated.toggle()
            }) {
                Text("Toggle")
            }
            Spacer()
        }
    }
}

struct TestView: View {
    @State private var array = [Int]()

    @Binding var animated: Bool {
        didSet {
           prepareArray()
        }
    }

    var body: some View {
        Text("\(array.count): \(animated ? "Y" : "N")").background(animated ? Color.green : Color.red).animation(Animation.easeIn(duration: 0.5).delay(0.1))
    }

    private func prepareArray() {
        array = [1]
    }
}

如果didSet监听器不起作用,为什么它允许用于@Binding var?然后我转而使用简单的Combine信号,因为它可以在onReceive闭包中捕获。但是,在值传递时,@State on signal未使视图失效:

struct ParentView: View {
    @State private var animatedSignal = CurrentValueSubject<Bool, Never>(false)

    var body: some View {
        VStack {
            TestView(animated: animatedSignal)
            Spacer()
            Button(action: {
                self.animatedSignal.send(!self.animatedSignal.value)
            }) {
                Text("Toggle")
            }
            Spacer()
        }
    }
}

struct TestView: View {
    @State private var array = [Int]()

    @State var animated: CurrentValueSubject<Bool, Never>

    var body: some View {
        Text("\(array.count): \(animated.value ? "Y" : "N")").background(animated.value ? Color.green : Color.red).animation(Animation.easeIn(duration: 0.5).delay(0.1)).onReceive(animated) { animated in
            if animated {
                self.prepareArray()
            }
        }
    }

    private func prepareArray() {
        array = [1]
    }
}

所以我的最终方法是根据信号值触发内部状态变量:
struct ParentView: View {
    @State private var animatedSignal = CurrentValueSubject<Bool, Never>(false)

    var body: some View {
        VStack {
            TestView(animated: animatedSignal)
            Spacer()
            Button(action: {
                self.animatedSignal.send(!self.animatedSignal.value)
            }) {
                Text("Toggle")
            }
            Spacer()
        }
    }
}

struct TestView: View {
    @State private var array = [Int]()

    let animated: CurrentValueSubject<Bool, Never>
    @State private var animatedInnerState: Bool = false {
        didSet {
            if animatedInnerState {
                self.prepareArray()
            }
        }
    }

    var body: some View {
        Text("\(array.count): \(animatedInnerState ? "Y" : "N")").background(animatedInnerState ? Color.green : Color.red).animation(Animation.easeIn(duration: 0.5).delay(0.1)).onReceive(animated) { animated in
            self.animatedInnerState = animated
        }
    }

    private func prepareArray() {
        array = [1]
    }
}

这个方案可以工作,但我不敢相信如此简单的任务需要如此复杂的结构!我知道SwiftUI是声明式的,但也许我错过了更简单的方法来完成这个任务?实际上,在真正的代码中,这个动画触发器将不得不传递到更深一层(

1个回答

1

有很多方法可以实现,包括你尝试过的方法。选择哪种方法可能取决于实际项目需求。(所有测试都是在 Xcode 11.3 上进行的,且可行)。

变体1:使用 @Binding 修改了您的第一次尝试。仅更改了TestView

struct TestView: View {
    @State private var array = [Int]()

    @Binding var animated: Bool
    private var myAnimated: Binding<Bool> { // internal proxy binding
        Binding<Bool>(
            get: { // called whenever external binding changed
                self.prepareArray(for: self.animated) 
                return self.animated
            },
            set: { _ in } // here not used, so just stub
        )
    }

    var body: some View {
        Text("\(array.count): \(myAnimated.wrappedValue ? "Y" : "N")")
            .background(myAnimated.wrappedValue ? Color.green : Color.red).animation(Animation.easeIn(duration: 0.5).delay(0.1))
    }

    private func prepareArray(for animating: Bool) {
        DispatchQueue.main.async { // << avoid "Modifying state during update..."
            self.array = animating ? [1] : [Int]() // just example
        }
    }
}
变体2(我更喜欢的):基于视图模型和发布,但需要更改ParentViewTestView,但总体上更简单明了。
class ParentViewModel: ObservableObject {
    @Published var animated: Bool = false
}

struct ParentView: View {
    @ObservedObject var vm = ParentViewModel()

    var body: some View {
        VStack {
            TestView()
               .environmentObject(vm) // alternate might be via argument
            Spacer()
            Button(action: {
                self.vm.animated.toggle()
            }) {
                Text("Toggle")
            }
            Spacer()
        }
    }
}

struct TestView: View {
    @EnvironmentObject var parentModel: ParentViewModel
    @State private var array = [Int]()

    var body: some View {
        Text("\(array.count): \(parentModel.animated ? "Y" : "N")")
            .background(parentModel.animated ? Color.green : Color.red).animation(Animation.easeIn(duration: 0.5).delay(0.1))
            .onReceive(parentModel.$animated) {
                self.prepareArray(for: $0)
            }
    }

    private func prepareArray(for animating: Bool) {
        self.array = animating ? [1] : [Int]() // just example
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接