iOS - Card Flip Animation

iOS - Card flip animation

Your problem is that your UIImageViews are directly on the "full page" view.

transitionFromView removes the fromView from its superview and adds the toView on the superview with the given animation. Thus, it animates the superview.

You should include a UIView that servers as a container and have both imageViews as subviews. Add your tap gesture on the containerview. Also, you should not have weak references to the imageViews, since once you have done the animation once, your reference to the back imageView will be gone. It is probably better to add these in code rather than storyboard. No need to hide the imageViews.

Here are some sample code:

class MyViewController: UIViewController {

@IBOutlet weak var containerView: UIView!

private let backImageView: UIImageView! = UIImageView(image: UIImage(named: "back"))
private let frontImageView: UIImageView! = UIImageView(image: UIImage(named: "front"))

private var showingBack = false

override func viewDidLoad() {
super.viewDidLoad()

frontImageView.contentMode = .ScaleAspectFit
backImageView.contentMode = .ScaleAspectFit

containerView.addSubview(frontImageView)
frontImageView.translatesAutoresizingMaskIntoConstraints = false
frontImageView.spanSuperview()

let singleTap = UITapGestureRecognizer(target: self, action: #selector(flip))
singleTap.numberOfTapsRequired = 1
containerView.addGestureRecognizer(singleTap)
}

func flip() {
let toView = showingBack ? frontImageView : backImageView
let fromView = showingBack ? backImageView : frontImageView
UIView.transitionFromView(fromView, toView: toView, duration: 1, options: .TransitionFlipFromRight, completion: nil)
toView.translatesAutoresizingMaskIntoConstraints = false
toView.spanSuperview()
showingBack = !showingBack
}

}

Card flipping animation

In order to create a basic card flipping animation like the one in the video you've linked to, I suggest putting frontImageView and the backImageView directly on top of each other on the UIView you intend to flip. To start, set their images to front and back accordingly; and, in this particular case, hide the frontImageView and show the backImageView.

Assuming "card" is the UIView you intend to flip, to perform the flip try:

[UIView transitionWithView:card duration:0.65f
options:UIViewAnimationOptionTransitionFlipFromRight animations:^{
frontImageView.hidden = NO;
backImageView.hidden = YES;
} completion:^(BOOL finished) {
// whatever you'd like to do immediately after the flip completes
}];

Edit:

And to handle the shadow, first off, it appears in the video you posted that the shadow grows in length moreso than it just fades in. And it seems as if (and makes logical sense that) the shadow reaches its peak during the middle of the animation as the card is lifted at its highest point. Since the shadow grows then shrinks during the course of the flip animation, it doesn't make sense to include the shadow animation within the same animation block as the flip since they're on different time schedules.

Secondly with regard to the shadow, to animate the layer property, you have to use Core Animations.

Perhaps you can run the two animations concurrently, i.e. while the above animation is performing, also do something like:

CABasicAnimation *shadowAnimation = [CABasicAnimation animationWithKeyPath:@"shadowRadius"];
shadowAnimation.delegate = self;
[shadowAnimation setFromValue:[NSNumber numberWithFloat:3.0]];
[shadowAnimation setToValue:[NSNumber numberWithFloat:10.0]];
[shadowAnimation setDuration:0.65f];
shadowAnimation.autoreverses = YES;
[[card layer] addAnimation:shadowAnimation forKey:@"shadowRadius"];

The last portion has been adapted from this code and takes advantage of the autoreverse property to automatically reverse the shadow's growth.

SwiftUI Card flip with two views

Simple Solution
The approach you're taking can be made to work by putting your two views in a ZStack and then showing/hiding them as the flipped state changes. The rotation of the second view needs to be offset. But this solution relies on a cross-fade between the two views. It might be OK for some uses cases. But there is a better solution - though it's a bit more fiddly (see below).

Here's a way to make your approach work:

struct SimpleFlipper : View {
@State var flipped = false

var body: some View {

let flipDegrees = flipped ? 180.0 : 0

return VStack{
Spacer()

ZStack() {
Text("Front").placedOnCard(Color.yellow).flipRotate(flipDegrees).opacity(flipped ? 0.0 : 1.0)
Text("Back").placedOnCard(Color.blue).flipRotate(-180 + flipDegrees).opacity(flipped ? 1.0 : 0.0)
}
.animation(.easeInOut(duration: 0.8))
.onTapGesture { self.flipped.toggle() }
Spacer()
}
}
}

extension View {

func flipRotate(_ degrees : Double) -> some View {
return rotation3DEffect(Angle(degrees: degrees), axis: (x: 1.0, y: 0.0, z: 0.0))
}

func placedOnCard(_ color: Color) -> some View {
return padding(5).frame(width: 250, height: 150, alignment: .center).background(color)
}
}

Better Solution SwiftUI has some useful animation tools - such as GeometryEffect - that can generate a really smooth version of this effect. There are some excellent blog posts on this topic at SwiftUI Lab. In particular, see: https://swiftui-lab.com/swiftui-animations-part2/

I've simplified and adapted one of examples in that post to provide the card flipping functionality.

struct FlippingView: View {

@State private var flipped = false
@State private var animate3d = false

var body: some View {

return VStack {
Spacer()

ZStack() {
FrontCard().opacity(flipped ? 0.0 : 1.0)
BackCard().opacity(flipped ? 1.0 : 0.0)
}
.modifier(FlipEffect(flipped: $flipped, angle: animate3d ? 180 : 0, axis: (x: 1, y: 0)))
.onTapGesture {
withAnimation(Animation.linear(duration: 0.8)) {
self.animate3d.toggle()
}
}
Spacer()
}
}
}

struct FlipEffect: GeometryEffect {

var animatableData: Double {
get { angle }
set { angle = newValue }
}

@Binding var flipped: Bool
var angle: Double
let axis: (x: CGFloat, y: CGFloat)

func effectValue(size: CGSize) -> ProjectionTransform {

DispatchQueue.main.async {
self.flipped = self.angle >= 90 && self.angle < 270
}

let tweakedAngle = flipped ? -180 + angle : angle
let a = CGFloat(Angle(degrees: tweakedAngle).radians)

var transform3d = CATransform3DIdentity;
transform3d.m34 = -1/max(size.width, size.height)

transform3d = CATransform3DRotate(transform3d, a, axis.x, axis.y, 0)
transform3d = CATransform3DTranslate(transform3d, -size.width/2.0, -size.height/2.0, 0)

let affineTransform = ProjectionTransform(CGAffineTransform(translationX: size.width/2.0, y: size.height / 2.0))

return ProjectionTransform(transform3d).concatenating(affineTransform)
}
}

struct FrontCard : View {
var body: some View {
Text("One thing is for sure – a sheep is not a creature of the air.").padding(5).frame(width: 250, height: 150, alignment: .center).background(Color.yellow)
}
}

struct BackCard : View {
var body: some View {
Text("If you know you have an unpleasant nature and dislike people, this is no obstacle to work.").padding(5).frame(width: 250, height: 150).background(Color.green)
}
}

Update

The OP asks about managing the flip status outside of the view. This can be done by using a binding. Below is a fragment that implements and demos this. And OP also asks about flipping with and without animation. This is a matter of whether changing the flip state (here with the showBack var) is done within an animation block or not. (The fragment doesn't include FlipEffect struct which is just the same as the code above.)

struct ContentView : View {

@State var showBack = false

let sample1 = "If you know you have an unpleasant nature and dislike people, this is no obstacle to work."
let sample2 = "One thing is for sure – a sheep is not a creature of the air."

var body : some View {

let front = CardFace(text: sample1, background: Color.yellow)
let back = CardFace(text: sample2, background: Color.green)
let resetBackButton = Button(action: { self.showBack = true }) { Text("Back")}.disabled(showBack == true)
let resetFrontButton = Button(action: { self.showBack = false }) { Text("Front")}.disabled(showBack == false)
let animatedToggle = Button(action: {
withAnimation(Animation.linear(duration: 0.8)) {
self.showBack.toggle()
}
}) { Text("Toggle")}

return
VStack() {
HStack() {
resetFrontButton
Spacer()
animatedToggle
Spacer()
resetBackButton
}.padding()
Spacer()
FlipView(front: front, back: back, showBack: $showBack)
Spacer()
}
}
}

struct FlipView<SomeTypeOfViewA : View, SomeTypeOfViewB : View> : View {

var front : SomeTypeOfViewA
var back : SomeTypeOfViewB

@State private var flipped = false
@Binding var showBack : Bool

var body: some View {

return VStack {
Spacer()

ZStack() {
front.opacity(flipped ? 0.0 : 1.0)
back.opacity(flipped ? 1.0 : 0.0)
}
.modifier(FlipEffect(flipped: $flipped, angle: showBack ? 180 : 0, axis: (x: 1, y: 0)))
.onTapGesture {
withAnimation(Animation.linear(duration: 0.8)) {
self.showBack.toggle()
}
}
Spacer()
}
}
}

struct CardFace<SomeTypeOfView : View> : View {
var text : String
var background: SomeTypeOfView

var body: some View {
Text(text)
.multilineTextAlignment(.center)
.padding(5).frame(width: 250, height: 150).background(background)
}
}

Want to flip two views so that one view hides and other shows swift

UIView.transition(with applies to a container view so you can try putting your subviews in a container view and apply UIView.transition on container view. Because you havent added any code am assuming few things here to answer

class ViewController: UIViewController {
@IBOutlet weak var button: UIButton!
let view1 = UIView()
let view2 = UIView()
let containerView = UIView()

override func viewDidLoad() {
super.viewDidLoad()

view1.backgroundColor = UIColor.red
view2.backgroundColor = UIColor.green

view.addSubview(containerView)
containerView.addSubview(view1)
containerView.addSubview(view2)

containerView.translatesAutoresizingMaskIntoConstraints = false
view1.translatesAutoresizingMaskIntoConstraints = false
view2.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.activate([
containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
containerView.topAnchor.constraint(equalTo: view.topAnchor, constant: 100),

view1.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
view1.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
view1.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
view1.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 100),

view2.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
view2.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
view2.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
view2.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 100)
])

view1.isHidden = false
view2.isHidden = true
}

@IBAction func changeTapped() {
UIView.transition(with: containerView,
duration: 1.0,
options: .transitionFlipFromBottom, animations: {[weak self] in
guard let self = self else { return }
self.view1.isHidden = !self.view1.isHidden
self.view2.isHidden = !self.view2.isHidden
})
}

O/P:

Sample Image

Can't get card flip animation to work

The views you want to transition between should be contained within another view, see the following hierarchy:

View Hierarchy

The transition should then be applied to the "Transition View":

UIView.transitionWithView(self.transitionView, duration: 1.5, options: .TransitionFlipFromRight, animations:{

self.frontImageView.hidden = self.showingImage;
self.behindView.hidden = !self.showingImage;

}) { (complete) -> Void in
self.showingImage = !self.showingImage
}

self.showingImage is a simple Bool instance variable to keep track of which view is currently being shown.



Related Topics



Leave a reply



Submit