Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI Card Flip animation with two views, one of which is embedded within a Stack

I'm trying to create a 'card flip' animation between two Views:

  • View 'A' is a CardView within a LazyVGrid
  • View 'B' is a custom modal overlay view

The LazyVGrid and View 'B' are together in a ZStack

Specifically, the ContentView is organized like so:

var body: some View {
    ZStack {
        NavigationView {
            ScrollView {
                LazyVGrid(columns: columns, spacing: 10) {
                    ForEach(model.events, id: \.self) { event in
                        SmallCardView(event: event)
                            .opacity(!showModal || event != modifiableEvent ? 1.0 : 0.0)
                    }
                }
            }
        }
        .brightness(self.showModal ? -0.1 : 0)
        .blur(radius: self.showModal ? 16 : 0)
        
        if self.showModal {
            AddEventView(
                showModal: $showModal,
                existingEvent: modifiableEvent,
            )
            .opacity(showModal ? 1.0 : 0.0)
            .padding(.horizontal, 16)
        }            
    }
}

I came across this SO post, and the answer seems super promising, however the answer doesn't take into account if one of the views is within a Stack / Grid, which is the case for me. So, my question is, how can I adapt the linked solution so that it works as expected if one of the views is indeed embedded within a Stack or a Grid.

Edit: Another thing to note is that the size and position of the Views are different

I tried adding .modifier(FlipEffect(flipped: $showModal, angle: animate3d ? 180 : 0, axis: (x: 0, y: 1))) to both the ZStack and SmallCardView, however neither yielded the expected results.

Thanks!

Edit: For clarity, I want to animate in a card flip style between these two views:

enter image description here

enter image description here

like image 961
Richard Robinson Avatar asked Dec 10 '25 08:12

Richard Robinson


1 Answers

I never managed to get it working without glitches when the cards are in a LazyVGrid using .matchedGeometryEffect(). So this is the rather messy solution abusing offsets and scaling I am using in my project:

import SwiftUI
import PlaygroundSupport

struct GridTestView: View {
   @State var flippedCard: Int?
   @State var frontCard: Int?
   let cards = [1,2,3,4,5,6,7,8,9,10]
   
   var body: some View {
      let columns = [
         GridItem(.flexible(), spacing: 0),
         GridItem(.flexible(), spacing: 0),
         GridItem(.flexible(), spacing: 0)
      ]
      
      GeometryReader { screenGeometry in
         ZStack {
            ScrollView {
               LazyVGrid(columns: columns, alignment: .center, spacing: 0) {
                  ForEach(cards, id: \.self) { card in
                     let isFaceUp = flippedCard == card
                     GeometryReader { cardGeometry in
                        ZStack {
                           CardBackView(card: card)
                              .modifier(FlipOpacity(pct: isFaceUp ? 0 : 1))
                              .rotation3DEffect(Angle.degrees(isFaceUp ? 180 : 360), axis: (0,1,0))
                              .frame(width: cardGeometry.size.width, height: cardGeometry.size.height)
                              .scaleEffect(isFaceUp ? screenGeometry.size.width / cardGeometry.size.width: 1)
                           CardFrontView(card: card)
                              .modifier(FlipOpacity(pct: isFaceUp ? 1 : 0))
                              .rotation3DEffect(Angle.degrees(isFaceUp ? 0 : 180), axis: (0,1,0))
                              .frame(width: screenGeometry.size.width, height: screenGeometry.size.height)
                              .scaleEffect(isFaceUp ? 1 : cardGeometry.size.width / screenGeometry.size.width)
                        }
                        .offset(x: isFaceUp ? -cardGeometry.frame(in: .named("mainFrame")).origin.x: -screenGeometry.size.width/2 + cardGeometry.size.width/2,
                                y: isFaceUp ? -cardGeometry.frame(in: .named("mainFrame")).origin.y: -screenGeometry.size.height/2 + cardGeometry.size.height/2)
                        .onTapGesture {
                           withAnimation(.linear(duration: 1.0)) {
                              if flippedCard == nil {
                                 flippedCard = card
                                 frontCard = card
                              } else if flippedCard == card {
                                 flippedCard = nil
                              }
                           }
                        }
                     }
                     .aspectRatio(1, contentMode: .fit)
                     .zIndex(frontCard == card ? 1 : 0)
                  }
               }
            }
         }
         .background(Color.black)
      }
      .coordinateSpace(name: "mainFrame")
   }
}

struct FlipOpacity: AnimatableModifier {
   var pct: CGFloat = 0
   
   var animatableData: CGFloat {
      get { pct }
      set { pct = newValue }
   }
   
   func body(content: Content) -> some View {
      return content.opacity(Double(pct.rounded()))
   }
}

struct CardBackView: View {
   var card: Int
   
   var body: some View {
      ZStack {
         RoundedRectangle(cornerRadius: 10)
            .fill(Color.red)
            .padding(5)
         Text("Back \(card)")
      }
   }
}

struct CardFrontView: View {
   var card: Int
   
   var body: some View {
      ZStack {
         RoundedRectangle(cornerRadius: 10)
            .fill(Color.blue)
            .padding(10)
            .aspectRatio(1.0, contentMode: .fit)
         Text("Front \(card)")
      }
   }
}

// Present the view controller in the Live View window
PlaygroundPage.current.setLiveView(GridTestView().frame(width: 400, height: 600))

Animation of card flipping in VGrid

like image 135
RyanM Avatar answered Dec 12 '25 22:12

RyanM



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!