Reputation: 4420
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?
Upvotes: 2
Views: 7239
Reputation: 1061
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.
Upvotes: 2
Reputation: 818
Whenever there is a single incremental change in the color property this will toggle the padding between 10 and 0
padding = color % 2 == 0 ? 10 : 0
Upvotes: 0
Reputation: 13276
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.
Upvotes: 2
Reputation: 5340
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())
}
}
Upvotes: 0