Reputation: 1
So I'm finishing up work on a SwiftUI based iOS App. I'm in the process of refining animations and visual layout stuff. My issue is as follows:
I have a parent view (A) which contains a child view (B) in its body, both views use an @ObservedObject view model for their data. I also have an overlay view (C) applied within A's body onto B. When B changes its frame size due to reloading on a data change from its view model, the overlay does not animate through the frame size change and just jumps to the new position.
struct A: View {
@ObservedObject var viewModel: ViewModelForA
var body: some View {
VStack {
Spacer()
B().overlay{
VStack{
HStack{
Spacer()
C()
}
Spacer()
}
}
}
}
}
How can I get C to animate correctly according to B's changing frame size?
Upvotes: 0
Views: 1039
Reputation: 1505
I just had this same issue and can't find anything online for how to do this so I came up with a solution of my own that uses an environment action (similar to how the OpenURLAction is structured).
To use it, you use this custom .childTriggeredAnimation(animation: .default)
modifier on the parent view. This adds an animateParent
action to the environment which can be used by child views to trigger an animation in the parent.
struct ParentView: View {
// without the solution, the text above
// and below the child view jump instantly
// even though the child view size is animating
// with the solution, they animate along with the child
var body: some View {
VStack {
Text("Above")
ChildView() // grows and shrinks in an animated way
Text("Below")
}
.childTriggeredAnimation(animation: .default) // part 1
}
}
And then in the child view you pull the animateParent
action out of the environment with @Environment(\.animateParent)
and then call that action when you want the animation to take place in the parent.
struct ChildView: View {
@State private var isBig = false
@Environment(\.animateParent)
private var animateParent // part 2
var body: some View {
Button {
// causes animation in child view
isBig.toggle()
// triggers animation in parent
animateParent() // part 3
// OR animateParent(overrideAnimation: whatever)
} label: {
Rectangle()
.frame(height: isBig ? 400 : 200)
}
.animation(.default, value: isBig)
}
}
For the implementation we define a custom EnvironmentKey (the docs demonstrate this very well) to use to store the action to run that will trigger the animation in the parent.
We also make a custom ViewModifier to place on the parent view to setup the animation in the parent and to add the custom action to the environment so that the child views can access it when they need to.
import SwiftUI
struct AnimateParentAction {
private let action: (Animation?) -> Void
init(action: @escaping (Animation?) -> Void) {
self.action = action
}
func callAsFunction() {
action(nil)
}
func callAsFunction(overrideAnimation: Animation) {
action(overrideAnimation)
}
}
private struct AnimateParentActionKey: EnvironmentKey {
static var defaultValue: AnimateParentAction = .init(action: { _ in })
}
extension EnvironmentValues {
var animateParent: AnimateParentAction {
get { self[AnimateParentActionKey.self] }
set { self[AnimateParentActionKey.self] = newValue }
}
}
private struct OnAnimateParentCalledModifier: ViewModifier {
let animation: Animation
@State private var animationToggle = false
@State private var overrideAnimation: Animation?
func body(content: Content) -> some View {
content
.environment(\.animateParent, AnimateParentAction(action: { overrideAnimation in
self.overrideAnimation = overrideAnimation
animationToggle.toggle()
}))
.animation(overrideAnimation ?? animation, value: animationToggle)
}
}
extension View {
func childTriggeredAnimation(animation: Animation) -> some View {
modifier(OnAnimateParentCalledModifier(animation: animation))
}
}
This solution worked for me, I am curious to see if anyone has come up with other better solutions but I feel like this is decently clean and reusable despite the boilerplate needed to set it up initially.
Upvotes: 1