简体   繁体   English

属性更改 SwiftUI 的动画视图

[英]Animate view on property change SwiftUI

I have a view我有意见

struct CellView: View {
    @Binding var color: Int
    @State var padding : Length = 10
    let colors = [Color.yellow, Color.red, Color.blue, Color.green]

    var body: some View {
        colors[color]
            .cornerRadius(20)
            .padding(padding)
            .animation(.spring())
    }
}

And I want it to have padding animation when property color changes.我希望它在属性color更改时具有填充动画。 I want to animate padding from 10 to 0.我想动画填充从 10 到 0。

I've tried to use onAppear我试过使用onAppear

    ...onAppear {
      self.padding = 0
    }

But it work only once when view appears(as intended), and I want to do this each time when property color changes.但是当视图出现时它只工作一次(按预期),我想每次当属性color改变时都这样做。 Basically, each time color property changes, I want to animate padding from 10 to 0 .基本上,每次color属性更改时,我都希望将 padding 从100 Could you please tell if there is a way to do this?你能告诉我是否有办法做到这一点?

As you noticed in the other answer, you cannot update state from within body .正如您在另一个答案中注意到的那样,您无法从body内部更新状态。 You also cannot use didSet on a @Binding (at least as of Beta 4) the way you can with @State .你也不能使用didSet@Binding (至少在测试版4)与方式,您可以@State

The best solution I could come up with was to use a BindableObject and sink/onReceive in order to update padding on each color change.我能想出的最佳解决方案是使用BindableObjectsink/onReceive以便在每次color更改时更新padding I also needed to add a delay in order for the padding animation to finish.我还需要添加延迟以使填充动画完成。

class IndexBinding: BindableObject {
    let willChange = PassthroughSubject<Void, Never>()
    var index: Int = 0 {
        didSet {
            self.willChange.send()
        }
    }
}

struct ParentView: View {
    @State var index = IndexBinding()
    var body: some View {
        CellView(index: self.index)
            .gesture(TapGesture().onEnded { _ in
                self.index.index += 1
            })
    }
}

struct CellView: View {

    @ObjectBinding var index: IndexBinding
    @State private var padding: CGFloat = 0.0

    var body: some View {
            Color.red
                .cornerRadius(20.0)
                .padding(self.padding + 20.0)
                .animation(.spring())
                .onReceive(self.index.willChange) {
                    self.padding = 10.0
                    DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
                        self.padding = 0.0
                    }
                }
    }
}


This example doesn't animate in the Xcode canvas on Beta 4. Run it on the simulator or a device.此示例不会在 Beta 4 上的 Xcode 画布中设置动画。在模拟器或设备上运行它。

As of Xcode 12, Swift 5从 Xcode 12 开始,Swift 5

One way to achieve the desired outcome could be to move the currently selected index into an ObservableObject .实现预期结果的一种方法是将当前选定的索引移动到ObservableObject

final class CellViewModel: ObservableObject {
    @Published var index: Int
    init(index: Int = 0) {
        self.index = index
    }
}

Your CellView can then react to this change in index using the .onReceive(_:) modifier;然后,您的CellView可以使用.onReceive(_:)修饰符对索引的这种变化做出反应; accessing the Publisher provided by the @Published property wrapper using the $ prefix.使用$前缀访问由@Published属性包装器提供的Publisher者。

You can then use the closure provided by this modifier to update the padding and animate the change.然后,您可以使用此修饰符提供的闭包来更新填充并为更改设置动画。

struct CellView: View {
    @ObservedObject var viewModel: CellViewModel
    @State private var padding : CGFloat = 10
    let colors: [Color] = [.yellow, .red, .blue, .green]
    
    var body: some View {
        colors[viewModel.index]
            .cornerRadius(20)
            .padding(padding)
            .onReceive(viewModel.$index) { _ in
                padding = 10
                withAnimation(.spring()) {
                    padding = 0
                }
            }
    }
}

And here's an example parent view for demonstration:这是用于演示的示例父视图:

struct ParentView: View {
    let viewModel: CellViewModel
    
    var body: some View {
        VStack {
            CellView(viewModel: viewModel)
                .frame(width: 200, height: 200)
            HStack {
                ForEach(0..<4) { i in
                    Button(action: { viewModel.index = i }) {
                        Text("\(i)")
                            .padding()
                            .frame(maxWidth: .infinity)
                            .background(Color(.secondarySystemFill))
                    }
                }
            }
        }
    }
}

Note that the Parent does not need its viewModel property to be @ObservedObject here.请注意,此处的 Parent 不需要其 viewModel 属性为@ObservedObject

You could use Computed Properties to get this working.您可以使用 Computed Properties 使其正常工作。 The code below is an example how it could be done.下面的代码是如何完成的示例。

import SwiftUI

struct ColorChanges: View {
    @State var color: Float = 0

    var body: some View {
        VStack {
            Slider(value: $color, from: 0, through: 3, by: 1)
            CellView(color: Int(color))
        }
    }
}

struct CellView: View {
    var color: Int
    @State var colorOld: Int = 0

    var padding: CGFloat {
        if color != colorOld {
            colorOld = color
            return 40
        } else {
            return 0
        }
    }

    let colors = [Color.yellow, Color.red, Color.blue, Color.green]

    var body: some View {
        colors[color]
            .cornerRadius(20)
            .padding(padding)
            .animation(.spring())
    }
}

每当颜色属性发生单个增量更改时,这将在 10 和 0 之间切换填充

padding =  color % 2 == 0 ? 10 : 0

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM