Home > OS >  Simultaneous matchedGeometryEffect and rotation3DEffect
Simultaneous matchedGeometryEffect and rotation3DEffect

Time:03-25

I want to animate a card, which flies from top half of screen to the bottom half and flips during the fly.

I control the flipping logic using custom .cardify modifier. It seems working alone, e.g. when I flip a card by onTapGesture { withAnimation { ... } }.

I also made a card fly from top of screen to the bottom and vice versa using matchedGeometryEffect.

But, when I tap card it flies without rotation.

I tried to apply .transition(.assymetric(...)) for both if-branches (see code below) but it did not help.

So, the code

import SwiftUI

struct ContentView: View {
    @State var cardOnTop = true
    @State var theCard = Card(isFaceUp: true)

    @Namespace private var animationNameSpace
    
    var body: some View {
        VStack {
            if cardOnTop {
                CardView(card: theCard)
                    .matchedGeometryEffect(id: 1, in: animationNameSpace)
                    .onTapGesture {
                        withAnimation {
                            theCard.toggle()
                            cardOnTop.toggle()  // comment me to test flipping alone
                        }
                    }
                Color.white
            } else {
                Color.white
                CardView(card: theCard)
                    .matchedGeometryEffect(id: 1, in: animationNameSpace)
                    .onTapGesture {
                        withAnimation {
                            theCard.toggle()
                            cardOnTop.toggle()
                        }
                    }
            }
        }
        .padding()
    }

    struct Card {
        var isFaceUp: Bool
        
        mutating func toggle() {
            isFaceUp.toggle()
        }
    }
    
    struct CardView: View {
        var card: Card

        var body: some View {
            Rectangle()
                .frame(width: 100, height: 50)
                .foregroundColor(.red)
                .cardify(isFaceUp: card.isFaceUp)
        }
    }
}

/* Cardify ViewModifier */

struct Cardify: ViewModifier, Animatable {
    init(isFaceUp: Bool){
        rotation = isFaceUp ? 0 : 180
    }
    
    var rotation: Double  // in degrees

    var animatableData: Double {
        get { return rotation }
        set { rotation = newValue }
    }
    
    func body(content: Content) -> some View {
        ZStack {
            let shape = RoundedRectangle(cornerRadius: DrawingConstants.cornerRadius)
            if rotation < 90 {
                shape.fill().foregroundColor(.white)
                shape.strokeBorder(lineWidth: DrawingConstants.lineWidth).foregroundColor(.gray)
            } else {
                shape.fill().foregroundColor(.gray)
            }
            
            content
                .opacity(rotation < 90 ? 1 : 0)
        }
        .rotation3DEffect(Angle.degrees(rotation), axis: (0, 1, 0))
    }
    
    private struct DrawingConstants {
        static let cornerRadius: CGFloat = 15
        static let lineWidth: CGFloat = 2
    }
}

extension View {
    func cardify(isFaceUp: Bool) -> some View {
        return self.modifier(Cardify(isFaceUp: isFaceUp))
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

How can I make the card flying with rotation?

Also, I found that this works (if put in the body of VStack alone, no if-branches)

CardView(card: theCard)
    .offset(x: 0, y: cardOnTop ? 0 : 100)
    .onTapGesture {
        withAnimation {
            theCard.toggle()
            cardOnTop.toggle()  // comment me to test flipping alone
        }
    }

but in my app I have a deck of flipped down cards, which are dealt to board flipping up. I tried to model the behavior by if-branch in the code of ContentView above (CardViews appear and dissapear).

CodePudding user response:

Here is a solution purely with .transition using a custom transition:

enter image description here

struct ContentView: View {
    @State var cardOnTop = true
    
    var body: some View {
        VStack(spacing:0) {
            if cardOnTop {
                RoundedRectangle(cornerRadius: 15)
                    .fill(.blue)
                    .transition(.rotate3D(direction: 1).combined(with: .move(edge: .bottom)))
                Color.clear
            } else {
                Color.clear
                RoundedRectangle(cornerRadius: 15)
                    .fill(.green)
                    .transition(.rotate3D(direction: -1).combined(with: .move(edge: .top)))
            }
        }
        .onTapGesture {
            withAnimation(.easeInOut(duration: 2)) {
                cardOnTop.toggle()
            }
        }
        .padding()
    }
}

/* Transition Modifier */

extension AnyTransition {
    static func rotate3D(direction: Double) -> AnyTransition {
        AnyTransition.modifier(
            active: Rotate3DModifier(value: 1, direction: direction),
            identity: Rotate3DModifier(value: 0, direction: direction))
    }
}

struct Rotate3DModifier: ViewModifier {
    let value: Double
    let direction: Double
    
    func body(content: Content) -> some View {
        content
            .rotation3DEffect(Angle(degrees: 180 * value * direction), axis: (x: 0, y: 1, z: 0))
            .opacity(1 - value)
    }
}

CodePudding user response:

The rotation isn't triggered because you remove the card and insert another one into the view, so only the .matchedGeometryEffect remains.

What you want is one card that stays in view so it can rotate, but also change its position.

You can achieve this e.g. with .offset. The GeometryReader is just to find the right offset value.

var body: some View {
    GeometryReader { geo in
        VStack {
            CardView(card: theCard)
                .offset(x: 0, y: cardOnTop ? 0 : geo.size.height / 2)
                .onTapGesture {
                    withAnimation {
                        cardOnTop.toggle() 
                        theCard.toggle()
                    }
                }
            Color.clear
        }
    }
    .padding()
}

enter image description here

  • Related