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 (CardView
s appear and dissapear).
CodePudding user response:
Here is a solution purely with .transition
using a custom transition:
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()
}