0

I am struggling with the animation of views when inserting them into a stack. In general, I am trying to create an effect where cards fly out from the stack and move to their final destination. I have tried many options with built-in transitions, but I still cannot achieve the desired result.

Maybe there is another way to solve this problem so the animation would look more natural? All the user needs to do is press the stack and the card will fly out of the stack exactly (the center of the stack is should be a starting point of animation). It would be better to fly it directly to its final destination (I know the final cards' amount at the very beginning, so I can clearly see when all the cards have been placed, when all of them appear on the screen).

Now it looks strange and unnaturalЖ Step 1 Step 2 Step 3 Step 4

Here is the code snippet, but I guess it is not very helpful

        VStack {
            HStack {
                if viewModel.firstCardShown {
                    RotatableCardView(cardLayout: viewModel.firstCardLayout, width: cardWidth - 20, isFlipped: $isFirstCardFlipped)
                        .transition(.move(edge: .bottom))
                }
                
                if viewModel.secondCardShown {
                    RotatableCardView(cardLayout: viewModel.secondCardLayout, width: cardWidth - 20, isFlipped: $isSecondCardFlipped)
                        .transition(.move(edge: .bottom))
                }
                
                if viewModel.thirdCardShown {
                    RotatableCardView(cardLayout: viewModel.thirdCardLayout, width: cardWidth - 20, isFlipped: $isThirdCardFlipped)
                        .transition(.move(edge: .bottom))
                }
            }
            .padding(.top, 40)
            .containerRelativeFrame(.vertical) { height, _ in
                height / 2
            }

                CardsDeckView()
                    .onTapGesture(perform: {
                        withAnimation(.easeIn(duration: 0.6)) {
                            if !viewModel.firstCardShown {
                                viewModel.firstCardShown.toggle()
                            } else if !viewModel.secondCardShown {
                                viewModel.secondCardShown.toggle()
                            } else if !viewModel.thirdCardShown {
                                viewModel.thirdCardShown.toggle()
                            }
                        }
                    })
                    .containerRelativeFrame(.vertical) { height, _ in
                        height / 2
                    }
2
  • Look into SwiftUI.Layout, one of the WWDC videos when it was introduced has a leaderboard animation/setup that you could use. Commented Feb 22, 2024 at 19:42
  • @loremipsum it would be very kind of you to suggest the name of the session, so I could find it Commented Feb 22, 2024 at 21:24

1 Answer 1

1

I assume your screenshots are describing the movement you are currently seeing, not the movement you actually want.

If I understand correctly, you want the cards to fly directly to their final positions. So when the second card is dealt, the first card shouldn't be seen to move any more. The same goes for additional cards.

One way to achieve this would be to use .matchedGeometryEffect:

  • prepare the target positions using hidden cards
  • the cards in the stack start off with their position matched to the stack itself
  • when dealt, the cards fly to their target positions.

Hiding the placeholders with .hidden() doesn't work, but .opacity(0) does.

struct ContentView: View {

    private let nCards = 3
    @State private var nDealtCards = 0
    @Namespace private var nsCards

    private var aCard: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 10)
                .fill(.red)
            RoundedRectangle(cornerRadius: 8)
                .stroke(.yellow)
                .padding(4)
            Rectangle()
                .fill(.image(Image(systemName: "xmark")))
                .foregroundStyle(.yellow)
                .padding(8)
        }
        .frame(width: 110, height: 150)
    }

    var body: some View {
        VStack(spacing: 30) {
            HStack {

                // The target locations for the cards
                ForEach(1...nCards, id: \.self) { n in
                    aCard
                        .opacity(0)
                        .matchedGeometryEffect(id: n, in: nsCards, isSource: true)
                }
            }
            ZStack {

                // The floating cards
                ForEach(1...nCards, id: \.self) { n in
                    aCard
                        .matchedGeometryEffect(
                            id: n > nDealtCards ? 0 : n,
                            in: nsCards,
                            properties: .position,
                            isSource: false
                        )
                }
                // A few cards to form the top of the stack
                aCard.rotationEffect(.degrees(3)).offset(x: -2, y: 4)
                aCard.rotationEffect(.degrees(-2)).offset(x: -4, y: 1)
                aCard.rotationEffect(.degrees(4)).offset(x: -1, y: 3)
            }
            .matchedGeometryEffect(id: 0, in: nsCards, isSource: true)
            .onTapGesture { nDealtCards += 1 }

            Button("Reset") { nDealtCards = 0 }
                .buttonStyle(.borderedProminent)
                .opacity(nDealtCards > 0 ? 1 : 0)
        }
        .animation(.easeInOut, value: nDealtCards)
    }
}

Animation

Sign up to request clarification or add additional context in comments.

1 Comment

Wow, It is exactly what I am looking for! Thank you so much! I will read a little more about the matchedGeometryEffect, but your answer perfectly solves the issue

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.