How to Make a Simple iPhone Game with Gestures

Apple makes it easy to recognize gestures on iPhone and iPad. Today, we’re going to make a game that detects taps, pinches, and swipes. Much like the classic game Bop It, players will have to quickly perform a specified touch gesture on command.

Let’s get started! Create a new Single View Xcode project, and give your app a fun name. I’m calling mine TapIt. If you need some help starting a new Xcode project, be sure to check out my previous tutorial that outlines the basic introductory steps in detail.

Storyboard Layout

Head to the Main.storyboard, and drag a label to the middle of the canvas. This will be our label that gives the player different commands. Change the text to Tap It!. You’re going to want that exclamation point at the end to really drive the point home. Control drag from the label to the canvas, and add a constraint to Center Horizontally in Container. Control drag from the label again, but this time choose Center Vertically in Container. In the Attributes Inspector, change Font to System 45.0. Center the text alignment. As a precaution, change Autoshrink to Minimum Font Size, and set the minimum to 10. Our command label should be ready to go!

Now we need a button to start a new game. Drag a button to the bottom-middle of the screen, and change the text to New Game. Set Font to System 20.0. We also need to add a constraint to center the button horizontally in the container, just like the command label. For the button’s vertical constraint, control drag from the button to the bottom of the canvas. Select Vertical Spacing to Bottom Layout Guide. That takes care of the button!

Finally, we want a label to show how much time the player has to make a move. You didn’t think we were going to give him all day, did you? Drag another label onto the canvas. It doesn’t really matter where you put it yet. Change the text to Timer, although the player will never see that written. It just makes things easier for you to visualize on the storyboard. Change Font to System 20.0. Center the text alignment. We want this label to display right where the new game button is. Don’t worry, they won’t be visible at the same time. Give the timer label a Center Horizontally in Container constraint. Now control drag from the timer label to the button, and give it a Center Vertically constraint. If you click the warning triangle, and select Update Frames, the timer label will move right on top of the button. We’re done laying out our objects! They should look something like this.

Tap It Storyboard

Hook up the Storyboard to the Code

While you’re still in Main.storyboard, open up the Assistant Editor. It’s the button with the two circles at the top-right of your Xcode window. You should see ViewController.swift appear next to Main.storyboard. Control drag from the command label into ViewController.swift, just under the first open brace. Make the connection an outlet, and name it commandLabel. Now connect the button in the same manner, and call it newGameButton. Connect the timer label, and name it remainingTimeLabel.

After you have your three outlets, control drag from the button into ViewController.swift again. This time, make the connection an action, and name it newGameButtonTapped. Make its type UIButton. You’re done making connections! Your code should look like this.

@IBOutlet weak var commandLabel: UILabel!
@IBOutlet weak var newGameButton: UIButton!
@IBOutlet weak var remainingTimeLabel: UILabel!

@IBAction func newGameButtonTapped(_ sender: UIButton) {

}

Some Initial Variables

Return to the Standard Editor, and go to ViewController.swift.  Let’s add some variables at the top of the file to set the stage.

var commandIndex = 0
var gameMode = false

let maxMoveTime = 50 // move time for first move
var adjustedMaxMoveTime = 50 // move time for subsequent moves
var remainingMoveTime = 50 // move time remaining during move

The first variable, commandIndex, keeps track of what command the user is assigned. It can be 0, 1, or 2, corresponding to tap, pinch, or swipe, respectively.

The bool gameMode keeps track of whether the player is currently playing or waiting to start a new game. This is used to determine whether to process gestures.

The next three variables deal with the timer. The constant maxMoveTime equals 50. This is how long you have for your first move. We will use a variable adjustedMaxMoveTime to track how long you will have for each subsequent move. It will keep getting smaller! Finally, remainingMoveTime will always be counting down while the player is playing. If the player successfully makes a move, it will adjust back up to adjustedMaxMoveTime. If remainingMoveTime reaches zero, the player will lose.

Setting Up viewDidLoad

We want to make some programmatic adjustments to our layout when the app launches. This code belongs in viewDidLoad.

// hide commandLabel but don't hide button
commandLabel.isHidden = true
remainingTimeLabel.isHidden = true
newGameButton.isHidden = false

// set-up remaining time label
remainingTimeLabel.text = String(remainingMoveTime)

Notice that the first few lines hide the labels and display the new game button, since the player isn’t  currently playing. Then we go ahead and set remainingTimeLabel to equal the units of time remaining.

The New Game Button

Let’s return to newGameButtonTapped and add some code to it.

// determine command
commandIndex = Int(arc4random_uniform(3))

// update command Label
updateCommandLabel()

// adjust label and button visibility
newGameButton.isHidden = true
commandLabel.isHidden = false
remainingTimeLabel.isHidden = false

// turn on game mode
gameMode = true

First, we randomly choose a command, designated as 0, 1, or 2. Then we call a separate function called updateCommandLabel() to display the appropriate command for that number. Then, we adjust the label and button visibility, followed by setting gameMode to true. The game has begun!

Let’s go ahead and add the updateCommandLabel() function. You can add it below the other functions in the class, but inside the last closing brace.

func updateCommandLabel() {
    switch commandIndex {
    case 0:
        commandLabel.text = "Tap It!"
    case 1:
        commandLabel.text = "Pinch It!"
    case 2:
        commandLabel.text = "Swipe It!"
    default:
        commandLabel.text = "Tap It!"
    }
}

This is just a basic switch statement that displays the different commands based on commandIndex. The default case is trivial, since commandIndex can only equal 0, 1, or 2, but it is included since switch statements have to cover all values.

The Gesture Recognizers

Now it’s time for the heart of the project—the gesture recognizer code! Return to viewDidLoad, and add the following lines of code at the bottom.

// listen for taps, pinches, and swipes
let tapGesture = UITapGestureRecognizer(target: self, action:  #selector (self.touchAction (_:)))
self.view.addGestureRecognizer(tapGesture)

let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector (self.pinchAction(_:)))
self.view.addGestureRecognizer(pinchGesture)

let swipeGesture = UISwipeGestureRecognizer(target: self, action: #selector (self.swipeAction(_:)))
self.view.addGestureRecognizer(swipeGesture)

Apple has built-in classes to recognize gestures. We just have to create an instance of these classes, and add them to the view to keep track of the gestures. Notice that we call different selectors when these gestures are recognized. Let’s add those functions to the class. Put them below your other functions, but above the ViewController’s bottom brace.

func touchAction(_ sender:UITapGestureRecognizer){
    // if game mode, check if it was a good move
    if gameMode {
        if commandIndex == 0 {
            goodMove()
        } else {
            badMove()
        }
    }
}

func pinchAction(_ sender:UIPinchGestureRecognizer){
    // if game mode, check if it was a good move
    if gameMode {
        if commandIndex == 1 {
            goodMove()
        }
        // don't call bad move if it's wrong, because pinch will continue to be called several times during the gesture after the command index has changed to a different command
    }
}

func swipeAction(_ sender:UISwipeGestureRecognizer){
    // if game mode, check if it was a good move
    if gameMode {
        if commandIndex == 2 {
            goodMove()
        } else {
            badMove()
        }
    }
}

For all of these functions, we first check to see if gameMode is true. When gameMode is false, we don’t care about processing any gestures. If it is true, we check to see if the gesture’s index (0 for tap, 1 for pinch, 2 for swipe) corresponds to the current value of commandIndex. If it does, we call a new function, goodMove(). Otherwise, we call badMove().

There is a notable exception with the pinch gesture. We don’t call badMove() if we detect a different gesture while the user is pinching. Unlike tap and swipe that only get triggered once per gesture, pinching continues to trigger over and over throughout the gesture. Unfortunately, this means that even as we switch to a new commandIndex after a successful pinch, the user will still be completing a pinch, and the gesture recognizer will continue to trigger. Simply not penalizing the user for pinches during a tap or swipe prompt is the easiest way to avoid this dilemma.

Good Move / Bad Move

We need to define the goodMove() and badMove() functions we just called. They look like this.

func goodMove() {
    // find new command that is not equal to the current command
    if commandIndex == 0 {
        commandIndex = Int(arc4random_uniform(2)) + 1
    } else if commandIndex == 1 {
        commandIndex = Int(arc4random_uniform(2))
        if commandIndex == 1 {
            commandIndex = 2
        }
    } else {
        commandIndex = Int(arc4random_uniform(2))
    }
    // update the label
    updateCommandLabel()

    // reset the clock
    resetTheClock(true)
}

func badMove() {
    // turn off game mode
    gameMode = false

    // adjust label and button visibility
    newGameButton.isHidden = false
    commandLabel.isHidden = true
    remainingTimeLabel.isHidden = true

    // reset the clock
    resetTheClock(false)
}

If the user makes a good move, then we need a new command. The first bit of code does just that, while making sure we don’t give the user the same command twice in a row. After updating commandLabel for the new command, then we call resetTheClock(). We haven’t defined this function yet, but ultimately, the user will have a certain amount of time per move, and this function will refresh the timer.

If the user has the misfortune of playing a bad move, we flip gameMode to false, hide our labels, and redisplay the button to start a new game. We also refresh the clock, so the player will have a new timer for the next game.

Reset the Clock

Here’s the clock reset code that we call after each move.

func resetTheClock(_ winning: Bool) {
    if winning {
        adjustedMaxMoveTime -= 5

        // don't let the timer go below 3
        if adjustedMaxMoveTime < 3 {
            adjustedMaxMoveTime = 3
        }
    } else {
        adjustedMaxMoveTime = maxMoveTime
    }

    remainingMoveTime = adjustedMaxMoveTime
    remainingTimeLabel.text = String(remainingMoveTime)
}

The point of this function is to refresh the player’s timer, but to give the player slightly less time for each subsequent move. If the user made a good move, we decrement the user’s adjusted max time per move by 5 units. We never let this number get below 3 units. That’s too fast! Then we reset the remaining time back to this adjusted max time and update the timer label. If the player makes a bad move, then we return the adjusted max time back to its original unadjusted level of 500 units.

So how long is a unit you’re asking? We’ll see in our final piece of code.

The Timer

Return one last time to viewDidLoad. We need to add some timer code at the bottom.

// schedule function in background to run down remaining move time
Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(self.update), userInfo: nil, repeats: true);

We schedule a function called update() to fire every tenth of a second. This is to update the time, meaning the units of time we just discussed are in fact tenths of a second. You could change this schedule’s time interval if you want to adjust the length of our time units. Here’s what the update() function should look like. Add it below your other functions.

func update() {
    // only decrement the timer during game mode
    if gameMode {
        remainingMoveTime -= 1
        remainingTimeLabel.text = String(remainingMoveTime)

        // the game is over if the player runs out of time
        if remainingMoveTime < 0 {
            badMove()
        }
    }
}

We only want this function to do anything if the player is actually playing. Inside the if statement, we decrement the timer 1 unit every time this function is called, which is every tenth of a second. Then, we adjust the label accordingly.

If the timer drops below zero, then we call badMove(). This executes the same code that gets called when a player uses the wrong gesture. The result is the same—game over—so this way we don’t have to write the same code twice.

Putting it All Together

Here’s the entire ViewController.swift. Your functions don’t have to be in the same order.

import UIKit

class ViewController: UIViewController {

    var commandIndex = 0
    var gameMode = false

    let maxMoveTime = 50 // move time for first move
    var adjustedMaxMoveTime = 50 // move time for subsequent moves
    var remainingMoveTime = 50 // move time remaining during move

    @IBOutlet weak var commandLabel: UILabel!
    @IBOutlet weak var newGameButton: UIButton!
    @IBOutlet weak var remainingTimeLabel: UILabel!

    @IBAction func newGameButtonTapped(_ sender: UIButton) {
        // determine command
        commandIndex = Int(arc4random_uniform(3))
        
        // update command Label
        updateCommandLabel()

        // adjust label and button visibility
        newGameButton.isHidden = true
        commandLabel.isHidden = false
        remainingTimeLabel.isHidden = false

        // turn on game mode
       gameMode = true
     }

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view, typically from a nib.

        // hide commandLabel but don't hide button
        commandLabel.isHidden = true
        remainingTimeLabel.isHidden = true
        newGameButton.isHidden = false

        // set-up remaining time label
        remainingTimeLabel.text = String(remainingMoveTime)

        // listen for taps, pinches, and swipes
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector (self.touchAction (_:)))
        self.view.addGestureRecognizer(tapGesture)

        let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector (self.pinchAction(_:)))
        self.view.addGestureRecognizer(pinchGesture)

        let swipeGesture = UISwipeGestureRecognizer(target: self, action: #selector (self.swipeAction(_:)))
        self.view.addGestureRecognizer(swipeGesture)

        // schedule function in background to run down remaining move time
        Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(self.update), userInfo: nil, repeats: true);
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    func updateCommandLabel() {
        switch commandIndex {
        case 0:
            commandLabel.text = "Tap It!"
        case 1:
            commandLabel.text = "Pinch It!"
        case 2:
            commandLabel.text = "Swipe It!"
        default:
            commandLabel.text = "Tap It!"
        }
    }

    func touchAction(_ sender:UITapGestureRecognizer){
        // if game mode, check if it was a good move
        if gameMode {
            if commandIndex == 0 {
                goodMove()
            } else {
                badMove()
            }
        }
    }

    func pinchAction(_ sender:UIPinchGestureRecognizer){
        // if game mode, check if it was a good move
        if gameMode {
            if commandIndex == 1 {
                goodMove()
            }
            // don't call bad move if it's wrong, because pinch will continue to be called several times during the gesture after the command index has changed to a different command
        }
    }

    func swipeAction(_ sender:UISwipeGestureRecognizer){
        // if game mode, check if it was a good move
        if gameMode {
            if commandIndex == 2 {
                goodMove()
            } else {
                badMove()
            }
        }
    }

    func goodMove() {
        // find new command that is not equal to the current command
        if commandIndex == 0 {
            commandIndex = Int(arc4random_uniform(2)) + 
        } else if commandIndex == 1{
            commandIndex = Int(arc4random_uniform(2))
            if commandIndex == 1 {
                commandIndex = 2
            }
        } else {
            commandIndex = Int(arc4random_uniform(2))
        }

        // update the label
        updateCommandLabel()

        // reset the clock
        resetTheClock(true)
    }

    func badMove() {
        // turn off game mode
        gameMode = false

        // adjust label and button visibility
        newGameButton.isHidden = false
        commandLabel.isHidden = true
        remainingTimeLabel.isHidden = true

        // reset the clock
        resetTheClock(false)
    }

    func resetTheClock(_ winning: Bool) {
        if winning {
            adjustedMaxMoveTime -= 5

            // don't let the timer go below 3
            if adjustedMaxMoveTime < 3 {
                adjustedMaxMoveTime = 3
            }
        } else {
            adjustedMaxMoveTime = maxMoveTime
        }
        remainingMoveTime = adjustedMaxMoveTime
        remainingTimeLabel.text = String(remainingMoveTime)
    }

    func update() {
        // only decrement the timer during game mode
        if gameMode {
            remainingMoveTime -= 1
            remainingTimeLabel.text = String(remainingMoveTime) 

            // the game is over if the player runs out of time
            if remainingMoveTime < 0 {
                badMove()
            }
        }
    }
}

Final Thoughts

Run your project, and see how it works. The app could certainly use some polish, but it’s functional. Were you able to make 10 moves before it got too fast? Get some practice, because we may add a high score tracker some day!

You may notice that swipes only register left to right. If you want to swipe in a different direction, this presents a good opportunity to get acquainted with Apple’s iOS documentation. Here’s the UISwipeGestureRecognizerDirection page. Good luck!