NEW! Master Swift design patterns with my latest book! >>

< Previous: Laying out the cards: addChildViewController()   Next: Adding a CAGradientLayer with IBDesignable and IBInspectable >

Animating a 3D flip effect using transition(with:)

There's a reason I've made you put the card functionality into a separate view controller, and it's because we're going to be adding some functionality to cards to handle them being flipped over. iOS makes this kind of animation really easy, but it's done in a slightly different way to our previous animations.

To handle tap detection we're going to use a UITapGestureRecognizer rather than something like touchesBegan. This will make more sense later on, but the TL;DR version is that part of the hoax effect will be you running your finger over the cards using your powers to "feel" for the star – something like touchesBegan() will just cause problems.

So, please add this gesture recognizer to the end of viewDidLoad() in the CardViewController class:

let tap = UITapGestureRecognizer(target: self, action: #selector(cardTapped))
back.isUserInteractionEnabled = true
back.addGestureRecognizer(tap)

We haven't written the cardTapped() method yet, but it's trivial because all it will do is pass the message on to the ViewController class to handle. This is important: we need each card to decide if it was tapped, but we need to pass control onto the ViewController class to act upon the tap, otherwise it's possible users might tap two cards at the same time and cause problems.

So, the cardTapped() method in the card view controller is simple:

@objc func cardTapped() {
    delegate.cardTapped(self)
}

Of course, that just pushes all the work to the ViewController class, where things get more complicated. The cardTapped() method there needs to:

  • Ensure that only one card can be tapped at any time
  • Loop through all the cards in the allCards array.
  • When it finds the card that was tapped, animate it to flip over then fade away.
  • For all other cards, animate them fading away.
  • Reset the game after two seconds so that more cards appear.

We'll be doing the animation using methods inside CardViewController, and resetting the game is done just by calling loadCards(), so that's all straightforward. But what's the best way to ensure that only one card can be chosen by the player?

It turns out this is pretty easy: as soon as the user taps any card, we're going to disable user interaction for our main view. We can then check that property inside the cardTapped() method using the guard keyword, then set it back to true inside loadCards().

To make things slightly more interesting, I want to introduce you to the perform() method family. These exist on objects that inherit from NSObject, which is both our view controllers, and allow us to call a method after a delay or in the background really easily.

Let's take this step by step. First, here's the cardTapped() method for the ViewController class:

func cardTapped(_ tapped: CardViewController) {
    guard view.isUserInteractionEnabled == true else { return }
    view.isUserInteractionEnabled = false

    for card in allCards {
        if card == tapped {
            card.wasTapped()
            card.perform(#selector(card.wasntTapped), with: nil, afterDelay: 1)
        } else {
            card.wasntTapped()
        }
    }

    perform(#selector(loadCards), with: nil, afterDelay: 2)
}

You can see that calls wasTapped() and wasntTapped() methods in the card view controllers, each of which will perform some animation – we'll get onto that in a moment. Using the afterDelay variant of perform() will cause wasntTapped() to be called after 1 second, and loadCards() to be called after 2 seconds.

For now, focus on the first two lines of that method: that's what stops users tapping two cards at once. By disabling the user interaction (and also checking that it was enabled beforehand) we can be sure the user gets to make only one choice. But we do need to re-enable user interaction when we're done, otherwise our app will be useless.

So, add this line somewhere into the loadCards() method:

view.isUserInteractionEnabled = true

Now all we need to do is write the wasTapped() and wasntTapped() methods of the card view controller. We'll do wasntTapped() first because it uses code you already know, so re-open CardViewController.swift and add this:

@objc func wasntTapped() {
    UIView.animate(withDuration: 0.7) {
        self.view.transform = CGAffineTransform(scaleX: 0.00001, y: 0.00001)
        self.view.alpha = 0
    }
}

That tells the card to zoom down and fade away over 0.7 seconds. Things are more interesting in the wasTapped() method because it needs to animate a 3D flip effect from the card back to the card front. But if you were imagining this was going to be hard, you're wrong: this flip effect has been around since the earliest days of iOS, so Apple made it extremely easy.

Here is the wasTapped() method in its entirety:

func wasTapped() {
    UIView.transition(with: view, duration: 0.7, options: [.transitionFlipFromRight], animations: { [unowned self] in
        self.back.isHidden = true
        self.front.isHidden = false
    })
}

As you can see, all the work is done by the transition(with:) method. This takes a view to operate on as its first parameter, and all the animations you perform need to be done on subviews of this container view. We pass .transitionFlipFromRight to create the flip effect, but you should try using the code completion to explore other options.

Inside the animations block, we just adjust the isHidden properties of the front and back image views, but in the context of .transitionFlipFromRight that will cause iOS to animate this change as a flip – it really is that simple.

That's it! Run the project now and you'll find you can tap on any card to flip it over – a neat effect with hardly any code. Thanks, iOS!

Go from iOS to macOS the easy way!

If you like Hacking with Swift, you'll love Hacking with macOS – learn to build macOS apps today, using 18 real-world projects!

< Previous: Laying out the cards: addChildViewController()   Next: Adding a CAGradientLayer with IBDesignable and IBInspectable >
MASTER SWIFT NOW
Buy Practical iOS 12 Buy Pro Swift Buy Swift Design Patterns Buy Practical iOS 11 Buy Swift Coding Challenges Buy Server-Side Swift (Vapor Edition) Buy Server-Side Swift (Kitura Edition) Buy Hacking with macOS Buy Advanced iOS Volume One Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with Swift Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let me know!

Click here to visit the Hacking with Swift store >>