UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Build a Simon game for watchOS

Repeat the sequence right on your wrist with WatchKit

Paul Hudson       @twostraws

I find watchOS such a fascinating platform to code for, because you have limited screen space, limited system resources, and limited user attention all at the same time. At the same time, watches go everywhere with your users, which makes them perfect for diverting little apps for times they forget their phone or run out of battery.

In this article we’re going to build a simple Simon game: users will see four colors, and will be asked to repeat increasingly long sequences of colors – when they get a sequence wrong they lose immediately. This ought to be a nice and gentle introduction to watchOS if you haven’t used it before, and you can use the watchOS simulator if you don’t have a real device.

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Setting up our basic user interface

Start by making a new watchOS app using the iOS App With WatchKit App template. Call it Simon, and make sure you deselect “Including Notification Scene” from the list of checkboxes – we won’t be using it here, and it just adds clutter to the storyboard.

Yes, I said storyboard: all watchOS apps must have their user interfaces designed in a storyboard, with no option to do it in code.

So, let’s start there: open Interface.storyboard, and you should a single empty interface controller. This is watchOS’s equivalent of a view controller, although it has a much simpler layout system as you’ll see.

Use the object library to drag out two groups, one above the other. Groups let us arrange elements horizontally or vertically next to each other, to help keep our UI organized. Select both of the groups and change their height from “Size to Fit” Content to “Relative to Container”, and enter 0.5 into the box below so they each take up half the height of the screen.

Tip: Apple Watch comes in four sizes: 38mm, 40mm, 42mm, and 44mm. Using relative sizes like these makes sure your UI scales well across all four sizes.

Next, drag out four buttons: two into the first group and two into the second. As you add the second button in each group you should see a blue vertical line appear next to the first button, which is Interface Builder’s way of showing you it will be positioned to the side. Don’t worry if you don’t see the second button each time – it’s just off screen.

Obviously we don’t want half our user interface to be off the screen, so we need to make the buttons take up less space. To do that, select all four buttons using the document outline, then change them to have width of “Relative to Container” with a value of 0.5, and a height of “Relative to Container” with a value of 1. This should make all four buttons take up equal space in the interface. While you have them all selected, delete their “Button” titles, but change their font size to be System 120 – we’ll be using that to show which thing to tap.

This is a Simon game, so we need to change each button to have a different color to make them easier to remember. I went for red, yellow, green, and blue, but choose whatever looks good to you.

Finally, we need to create some connections for our user interface. So, switch to the assistant editor and create four outlets for the four buttons (I went for red, green, yellow, and blue), and four actions (redTapped(), greenTapped(), yellowTapped(), and blueTapped() for me.)

We’re done with the storyboard now, although if you’re a pedant like me you might have noticed the horizontal gaps between our buttons is ever so slightly larger than the vertical gap. To fix that, select Interface Controller in the document outline, then use the attributes inspector to change spacing to 3. The default is only 1 point more, but that slight difference offended my eyes!

Making a flashing sequence

Switch back to the standard editor, and open InterfaceController.swift for editing.

This game has two states: either the user is watching buttons flash, or is tapping buttons to recreate the sequence. We’re going to start with that first state, which means making our app flash while the user watches.

First, we need to add three properties:

  1. An isWatching boolean that tracks whether the player is watching or interacting. We’ll add a property observer so that we can show a simple instruction to the player.
  2. A sequence array, which tracks which buttons are in our current sequence.
  3. A sequenceIndex integer, which tracks which button has flashed, or which button the user must tap.

So, add these properties to the InterfaceController class:

var isWatching = true {
    didSet {
        if isWatching {
            setTitle("WATCH!")
        } else {
            setTitle("REPEAT!")
        }
    }
}

var sequence = [WKInterfaceButton]()
var sequenceIndex = 0

To play a sequence, we’re going to call one method repeatedly: playNextSequenceItem(). This will check whether the sequence has finished: if it has it will set isWatching to false and reset sequenceIndex so the player can take over, but if it hasn’t then it will make one of the buttons flash.

We already gave our buttons a nice and big font size, so all we need to do is write some text in the button to make it clear that’s the one to press. However, we need to do this with precise timing: we can’t show the text immediately because otherwise flashing the same button twice won’t be obvious – the user will just see the text for a little longer. So, we need to delay by a fraction of a second. We then need to delay again, after which we can clear the button’s title and call playNextSequenceItem() again.

Here’s that in code:

func playNextSequenceItem() {
    // stop flashing if we've finished our sequence
    guard sequenceIndex < sequence.count else {
        isWatching = false
        sequenceIndex = 0
        return
    }

    // otherwise move our sequence forward
    let button = sequence[sequenceIndex]
    sequenceIndex += 1

    // wait a fraction of a second before flashing
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
        // mark this button as being active            
        button.setTitle("•")

        // wait again
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            // deactivate the button and flash again
            button.setTitle("")
            self?.playNextSequenceItem()
        }
    }
}

Next, we need a method that advances our game each time it’s called. This will pick a random button and add it to our sequence, reset sequenceIndex so the flashing starts at the beginning, set isWatching back to true so the player instruction is correct, then call playNextSequenceItem() after a second so the user has a short break.

There is one small hiccup here, and it’s a quirk of Swift. Our four buttons are all typed as WKInterfaceButton!, but if we make an array of them Swift will make that array [WKInterfaceButton?] – an array of optional buttons.

Now, when we ask for a random element from an array, Swift returns us an optional because the array might be empty. This means for an array of buttons we’ll get back an optional optional – something you rarely want to see.

One solution is to write code like this:

let colors = [red, yellow, green, blue]
sequence.append(colors.randomElement()!!)

And honestly I wouldn’t blame you: we know that randomElement() must return an item because we just made the array, and we also know the optional types inside all have values, because they are our buttons. However, the double exclamation mark sits uneasily with me because it’s not obvious why it’s there, so a slightly better solution is this:

let colors: [WKInterfaceButton] = [red, yellow, green, blue]
sequence.append(colors.randomElement()!)

By specifying the array type [WKInterfaceButton] we lose one level of optionality, and I think the use of randomElement()! is self-explanatory – there’s little point adding a guard in there because we literally just made the array.

Anyway, let’s put all that into a new method called addToSequence(). Here’s the code:

func addToSequence() {
    // add a random button to our sequence
    let colors: [WKInterfaceButton] = [red, yellow, green, blue]
    sequence.append(colors.randomElement()!)

    // start the flashing at the beginning
    sequenceIndex = 0

    // update the player instructions
    isWatching = true

    // give the player a little respite, then start flashing
    DispatchQueue.main.asyncAfter(wallDeadline: .now() + 1) {
        self.playNextSequenceItem()
    }
}

OK, so we have a method that flashes one button, and we have a method that levels up our game each time it’s called. We need one more method to finish off this step: startNewGame(), which will remove all items from sequence then call addToSequence() to add our initial item:

func startNewGame() {
    sequence.removeAll()
    addToSequence()
}

We just need to call that when we’re ready to start. The watchOS equivalent of viewDidLoad() is awake(withContext:), and you should have a method stub for that already. So, modify it to this:

override func awake(withContext context: Any?) {
    super.awake(withContext: context)
    startNewGame()
}

And we’re done with this step! To try out the game so far, such as it is, go to the Product menu and choose Scheme > Simon WatchKit App. This will activate one of the watchOS simulators, but if you go back to the Product menu and choose Destination you can select whichever one you prefer – personally I prefer using the largest option while testing and the smallest while designing.

All being well you should see one color flash when the game begins. If you’d like to double check everything worked, try changing the addToSequence() method to this:

func addToSequence() {
    let colors: [WKInterfaceButton] = [red, yellow, green, blue]

    for _ in 1...10 {
        sequence.append(colors.randomElement()!)
    }

    sequenceIndex = 0
    isWatching = true

    DispatchQueue.main.asyncAfter(wallDeadline: .now() + 1) {
        self.playNextSequenceItem()
    }
}

That will add 10 buttons each time the method is called, so you should see ten flashes when the app starts. Make sure you undo that change once you’ve verified that it worked!

Giving the player control

So far we’ve designed our user interface, and made a sequence of buttons flash. To make this an actual game, we need to let the player tap buttons, then write code to make sure they tapped in the correct sequence.

This will mostly be done with one new method, called makeMove(). This will accept a WKInterfaceButton as its only parameter, and check whether it’s the next item in our sequence. If it is then the player is correct, so we can increment the sequence and perhaps even go back to watching if they reached the end of the sequence. If it isn’t the next button in our sequence then the player is wrong: we need to show an alert with their score, then restart.

Of all that code, the only part that’s new is the last one: showing an alert. watchOS makes this nice and easy: our interface controller has a dedicated presentAlert() method that shows an alert with a title and message, and we can attach any number of WKAlertAction instances to it. Each of those actions has a title plus a closure to run when it’s triggered, so we’ll create one with the title “Play Again” that just calls startNewGame() to reset the game.

Here’s the code:

func makeMove(_ color: WKInterfaceButton) {
    // don't let the player touch stuff while in watch mode
    guard isWatching == false else { return }

    if sequence[sequenceIndex] == color {
        // they were correct! Increment the sequence index.
        sequenceIndex += 1

        if sequenceIndex == sequence.count {
            // they made it to the end; add another button to the sequence
            addToSequence()
        }
    } else {
        // they were wrong! End the game.
        let playAgain = WKAlertAction(title: "Play Again", style: .default) {
            self.startNewGame()
        }

        presentAlert(withTitle: "Game over!", message: "You scored \(sequence.count - 1).", preferredStyle: .alert, actions: [playAgain])
    }
}

To finish up, all we need to do is call makeMove() from our four @IBAction methods, passing in the correct button:

@IBAction func redTapped() {
    makeMove(red)
}

@IBAction func yellowTapped() {
    makeMove(yellow)
}

@IBAction func greenTapped() {
    makeMove(green)
}

@IBAction func blueTapped() {
    makeMove(blue)
}

And that’s our game finished!

Where now?

In just over 100 lines of code we’ve implemented a simple game for watchOS, and to be honest simple games really are the key with this platform – for anything serious your user will always prefer their phone, so try to think of games that work well as distractions for when their phone isn’t around.

If you wanted to take this game further, you could:

  1. Add musical tones to each button.
  2. Increase the flash speed every time the sequence grows to make the game harder.
  3. Track their high score.

I hope you take the time to experiment further – watchOS is a surprisingly powerful system, and even includes larger frameworks such as SpriteKit and SceneKit. So, put your phone to one side for an hour or two and see what you can build!

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.7/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.