简体   繁体   中英

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. I want to animate padding from 10 to 0.

I've tried to use 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. Basically, each time color property changes, I want to animate padding from 10 to 0 . 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 . You also cannot use didSet on a @Binding (at least as of Beta 4) the way you can with @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. 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.

As of Xcode 12, Swift 5

One way to achieve the desired outcome could be to move the currently selected index into an 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; accessing the Publisher provided by the @Published property wrapper using the $ prefix.

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.

You could use Computed Properties to get this working. 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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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