Search code examples
swiftcocoa-touchswiftuiswiftui-animation

SwiftUI - How to limit the scope of animation to only the onAppear Transition


I'm new to SwiftUI and working through some sample projects to get the hang of it and I'm getting stuck on limiting the scope of the animation I set for the .transition for an AnimationModifier so it only impacts the animation of the transition and nothing else in the view.

While the separate transitions are respected for onAppear() and another for onDisappear(). The animation in the AnimatableModifier is overriding the removal over the item from the grid even when explicitly declared

I've tried explcicitly setting the Animation to the .offset transition in both the AnimatableModifier and for the CardView in the GameView, and when I do, no animation is triggered at all:

.transition(AnyTransition.offset(CGSize.init(width: randomXLocation, height: -offset.height-50)).animation(Animation.easeInOut(duration: 1.25).delay(delay)))
                    

So, there's gotta be a way to limit the scope or explicitly declare the animation for transition or two separate Animations in the Animation Modifier, but I'm not finding any resources on how to move forward.

GameView.swift

struct GameView: View {
    @ObservedObject var viewModel: SetGameViewModel
    @State var delay: Double = 0.1
    
    var body: some View {
        GeometryReader { geometry in
            VStack {
                Grid(newItems: self.viewModel.newCards,
                    items: self.viewModel.cards.itemsAtWithIds(ids: self.viewModel.idOfCardsToDisplay)) { card in
                        CardView(card: card, bodyGeoProxy: geometry, delay: self.delay).onTapGesture {
                            self.viewModel.choose(card: card)
                            }
                    .transition(AnyTransition.offset(CGSize.init(width: randomXLocation, height: -offset.height-50)))
                    .animation(Animation.easeInOut(duration: 1.25).delay(delay))
                        .onAppear() {
                            let maxDelay: Double = Double(self.viewModel.cards.itemsAtWithIds(ids: self.viewModel.idOfCardsToDisplay).count)*0.2 + 0.2
                            if self.delay < 2.5 {
                                self.delay = self.delay + 0.2
                            } else if self.delay >= maxDelay {
                                self.delay = 0.1
                            }
                        }
                    
                }
                
                HStack{
                    Button(action: {
                        self.viewModel.dealThreeCards()
                    }) {
                        Text("Hit Me")
                    }
                    Spacer()
                    Text("Score: \(self.viewModel.score)")
                    Spacer()
                    Button(action: {
                        self.viewModel.dealThreeCards()
                    }) {
                        Text("New Game")
                    }
                }
            }
        }
        
        
        
        
        
    }
} 

GameView.swift

struct CardView: View{
    var card: SetGame<SoloSetCardContent>.Card
    var bodyGeoProxy: GeometryProxy
    var delay: Double
    
    var body: some View {
        GeometryReader { geometry in
            self.body(for: geometry)
        }
        
    }
    
    init(card: SetGame<SoloSetCardContent>.Card, bodyGeoProxy: GeometryProxy, delay: Double) {
        self.card = card
        self.bodyGeoProxy = bodyGeoProxy
        self.delay = delay
    }
    
    
    @ViewBuilder
    func body(for geometryProxy: GeometryProxy) -> some View {
        
            ZStack {
                if card.isSelected {
                    RoundedRectangle(cornerRadius: 5)
                        .fill(Color.gray)
                        .frame(width: geometryProxy.size.width-4, height: geometryProxy.size.height-4, alignment: .center)
                        .border(Color.blue, width: 2)
                        .animation(nil)
                } else {
                    RoundedRectangle(cornerRadius: 5)
                        .fill(Color.gray)
                        .frame(width: geometryProxy.size.width-4, height: geometryProxy.size.height-4, alignment: .center)
                        .border(Color.red, width: 2)
                        .animation(nil)
                }
                VStack {
                    ForEach(0..<self.card.content.deckShapes.count) { index in
                        VStack {
                            Spacer(minLength: 5)
                            ShapeView(setShape: self.card.content.deckShapes[index])
                                .frame(width: (geometryProxy.size.width-geometryProxy.size.width/5), height: geometryProxy.size.height/5, alignment: .center)
                            Spacer(minLength: 5)
                        }
                    }
                }
            }
            .deal(delay: self.delay, offset: bodyGeoProxy.size)
        }
}

Dealer.Swift - AnimatableModifier

struct Dealer: AnimatableModifier {
    
    
    @State var show: Bool = false
    var delay: Double
    var offset: CGSize
    var randomXLocation: CGFloat {
        CGFloat.random(in: -offset.width ..< offset.width)
    }
    
    func body(content: Content) -> some View {
        ZStack {
            if show {
                content
                    .transition(AnyTransition.offset(CGSize.init(width: randomXLocation, height: -offset.height-450)))
                    .animation(Animation.easeInOut(duration: 1.25).delay(delay))
                    
            }
        }
            
        .onAppear {
            withAnimation {
                self.show = true
            }
        }
        .onDisappear {
            withAnimation {
                self.show = false
            }
        }

    }

}

extension View {
    func deal(delay: Double, offset: CGSize) -> some View {
        self.modifier(Dealer(delay: delay, offset: offset))
    }
}


Solution

  • I was able to resolve this by removing the Animation from the body content (and elsewhere) and adding to withAnimation portion of the .onAppear method in body function of the AnimationModifier

    func body(content: Content) -> some View {
            ZStack {
                if show {
                    content
                        .transition(.asymmetric(insertion: .offset(CGSize.init(width: randomXLocation, height: -offset.height-50)),
                                                removal: .offset(CGSize.init(width: randomXLocation, height: offset.height+50))))
                }
            }
            .onDisappear {
                withAnimation (Animation.easeInOut(duration: 1.25).delay(0))  {
                    self.show = false
                }
            }
            .onAppear {
                withAnimation (Animation.easeInOut(duration: 1.25).delay(self.delay)) {
                    self.show = true
                }
            }
        }