iOS - Card flip animation
Asked Answered
H

9

16

I have two UIImageViews. One is the 'Front' and the other is the 'Back'. I'm trying to implement it so that when you click onto the 'Back' it will trigger the animation and flip the card.

The animation works perfectly. But it animates the full page, which I don't want. I only want the UIImageView to flip. I can't see what I'm doing wrong. Probably something obvious.

@IBOutlet weak var answerImageViewer: UIImageView?
@IBOutlet weak var backAnswerImageViewer: UIImageView?

override func viewDidLoad() {
    super.viewDidLoad()
    startNewGame()
    retrieveNewQuestion()
    setupBannerMessage()
    customPlayButtons()

    let tapGesture = UITapGestureRecognizer(target: self, action: Selector("tap"))
    tapGesture.numberOfTapsRequired = 1
    backAnswerImageViewer?.addGestureRecognizer(tapGesture)
    backAnswerImageViewer?.userInteractionEnabled = true
}

var showingBack = true

func tap (){
    if showingBack{
       UIView.transitionFromView(self.backAnswerImageViewer!, toView: self.answerImageViewer!, duration: 1, options: UIViewAnimationOptions.TransitionFlipFromRight, completion: nil)

        backAnswerImageViewer?.hidden = true
        answerImageViewer?.hidden = false

        showingBack = false
    }
    else {
        showingBack = true

        UIView.transitionFromView(self.answerImageViewer!, toView: self.backAnswerImageViewer!, duration: 1, options: UIViewAnimationOptions.TransitionFlipFromRight, completion: nil)

        backAnswerImageViewer?.hidden = false
        answerImageViewer?.hidden = true
    }
}
Halbeib answered 15/9, 2016 at 19:40 Comment(3)
What do you mean animate the full page? Can you provide a screenshot?Howdoyoudo
Instead of just the UIImageView flipping, the View Controller flips insteadHalbeib
You should use UIView.AnimationOptions showHideTransitionViewsZworykin
S
17

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
        }   

    }
Strobile answered 15/9, 2016 at 20:47 Comment(0)
S
24

If you have a button that holds "card" as a variable, and you want to change his image to a new image called "1". All you have to do is this:

let image = UIImage(named: "1")
card.setImage(image, for: .normal)
UIView.transition(with: card, duration: 2, options: .transitionFlipFromRight, animations: nil, completion: nil)
Stabilize answered 21/9, 2017 at 0:44 Comment(2)
Such a simple solution. I implemented it in my upcoming flashcard app. I added a boolean in order to change between flips. "if isFlipped { UIView.transition(with: flashcardView, duration: 0.5, options: .transitionFlipFromRight, animations: nil, completion: nil) isFlipped = false } else { UIView.transition(with: flashcardView, duration: 0.5, options: .transitionFlipFromLeft, animations: nil, completion: nil) isFlipped = true }"Ryannryazan
When I'm trying this code, the image is only set after the flip, not while it is flipping...Handcraft
S
17

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
        }   

    }
Strobile answered 15/9, 2016 at 20:47 Comment(0)
K
8

This solution is for flipping "a card", using two UIImageView (just like you are using...), I am using it inside a UICollectionView, that that is of course not neccessary.

class CardCVCell: UICollectionViewCell {
    @IBOutlet weak var cardFrontImageView: UIImageView!
    @IBOutlet weak var cardBackImageView: UIImageView!

    private var flipped: Bool = false {
        didSet {
            cardFrontImageView.visible = flipped
            cardBackImageView.hidden = flipped
        }
    }

    override func awakeFromNib() {
        super.awakeFromNib()
        cardBackImageView.backgroundColor = UIColor.brownColor()
    }

    override func prepareForReuse() {
        super.prepareForReuse()
        cardFrontImageView.image = nil
        flipped = false
    }

    func flipCard(cardModel: Card) {
        let flipped = cardModel.flipped
        let fromView = flipped ? cardFrontImageView : cardBackImageView
        let toView = flipped ? cardBackImageView : cardFrontImageView
        let flipDirection: UIViewAnimationOptions = flipped ? .TransitionFlipFromRight : .TransitionFlipFromLeft
        let options: UIViewAnimationOptions = [flipDirection, .ShowHideTransitionViews]
        UIView.transitionFromView(fromView, toView: toView, duration: 0.6, options: options) {
            finished in
            cardModel.flipped = !flipped
        }
    }
}

The code is taken from my open source project introducing people to Swift development, called SwiftIntro which is a memory game, fetching images from Instagram.

Please observe that the model Card must be a class and not a value type

Kutzer answered 15/9, 2016 at 20:11 Comment(9)
My whole UIView is getting rotated instead of UIImageview :(Melvin
@ray-king are you passing in UIImageViews to transitionFromView:toView:duration ?Kutzer
Yes, but I figured it out, it was wrong subview structureMelvin
whole viewController is flipping even tho i have only set my uiview into transitionFromView:toView:duration. ideas?Nada
@david-seek And you are sure that you have not accidently set toView or fromView to view (self.view) on your UIViewController? :SKutzer
very sure. but the following threads points out, that you need to explicitly put the views into a new subview container otherwise the VC will flip as well. #27120737Nada
i have just for test purposes put 2 uiviews as subviews of a viewcontroller and flipped the subviews. but the whole VC slipped as well. so i guess putting them into a container or or add/remove them as subviews again would solve the issueNada
i dont understand why, but that seems to be the solution.Nada
@DavidSeek My solution is using two UIImageViews inside a UICollectionViewCell, I have not tested your setup.Kutzer
H
4

For Swift 4

Its best to use UIButton rather than UIImageView!

var isBackImageOpen = false

if isBackImageOpen {
isBackImageOpen = false
let image = UIImage(named: "backImage")
myBtn.setImage(image, for: .normal)
UIView.transition(with: myBtn, duration: 0.3, options: .transitionFlipFromLeft, animations: nil, completion: nil)

} else {

isBackImageOpen = true
let image = UIImage(named: "frontImage")
myBtn.setImage(image, for: .normal)
UIView.transition(with: myBtn, duration: 0.3, options: .transitionFlipFromRight, animations: nil, completion: nil)
}

Hope this answer helps :)!

Haemostatic answered 7/2, 2019 at 17:18 Comment(0)
H
3

You need to have a container view. Put answerImageViewer and backAnswerImageViewer into one single UIView. Here's my sample code.

    let frontView = UIView()
    let backView = UIView()

    let frame = CGRect(x: 40, y: 100, width: 300, height: 400)
    frontView.backgroundColor = .red
    backView.backgroundColor = .blue

    frontView.frame = CGRect(x: 0, y: 0, width: 300, height: 400)
    backView.frame = CGRect(x: 0, y: 0, width: 300, height: 400)

    let containerView = UIView()
    containerView.frame = frame

    containerView.addSubview(backView)
    containerView.addSubview(frontView)

    view.addSubview(containerView)
Howdoyoudo answered 15/9, 2016 at 20:20 Comment(0)
A
3

You can use this code

UIView.transition(with: self.yourViewName, duration: 0.5, options: [.transitionFlipFromRight], animations: nil, completion: nil)
Alfonso answered 9/5, 2018 at 12:55 Comment(1)
Dont copy paste answersStabilize
N
3

According to Pankaj's answer,

In iOS 12 usage of UIView.transition method is deprecated a little bit.

In iOS 12 device, although I've updated the code from Swift 4.0 to Swift 4.2, I encountered flipping card problem.

You might mis this because; somehow, Xcode don't suggest to change this. I realized this (thanks to Intellisense), when I've tried to rewrite UIView.transition method.

Before:

UIView.transition(with: cardView, duration: 1.0, options: transitionOptions, animations: {
        self.cardView.isHidden = true
        self.downButtonForBackCard.alpha = 1
    })

    UIView.transition(with: backCardView, duration: 1.0, options: transitionOptions, animations: {
        self.backCardView.isHidden = false
    })

After:

 UIView.transition(with: cardView, duration: 1.0, options: transitionOptions, animations: {
        self.downButtonForBackCard.alpha = 1

    }) { (finish) in
        self.cardView.isHidden = true
    }

    UIView.transition(with: backCardView, duration: 1.0, options: transitionOptions, animations: {
        self.backCardView.isHidden = false
    }) { (finish) in

    }

PS: Those two codes are all from my case. Don't copy paste, just take example.

I hope I can save someones's life.

Nipper answered 21/9, 2018 at 23:46 Comment(0)
V
0

Try this extension maybe it's easy to use

extension UIView{

    func showFlip(){
            if self.isHidden{
                UIView.transition(with: self, duration: 1, options: [.transitionFlipFromRight,.allowUserInteraction], animations: nil, completion: nil)
                self.isHidden = false
            }

        }
    func hideFlip(){
        if !self.isHidden{
            UIView.transition(with: self, duration: 1, options: [.transitionFlipFromLeft,.allowUserInteraction], animations: nil,  completion: nil)
            self.isHidden = true
        }
    }
}

use this in click action

func Flip(){
        if second_view.isHidden == true{
            second_view.showFlip()
            first_view.hideFlip()
        }else if first_view.isHidden == true{
            second_view.hideFlip()
            first_view.showFlip()
        }
}
Vergara answered 19/5, 2018 at 6:25 Comment(0)
C
-1

The previous answer are good but add 'showHideTransitionViews' as an options, it allows you to not have manually hide and shows the views that is animated. Just keep in mind that those two views need to be inside an other view. This transition will rotate the super view.

  UIView.transition(from: view1, to: view2, duration: 0.5, options: [.transitionFlipFromLeft, .showHideTransitionViews], completion: nil)
Chufa answered 22/8, 2018 at 21:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.