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

< Previous: Designing your layout   Next: Guess which flag: random numbers >

Making the basic game work: UIButton and CALayer

We're going to create an array of strings that will hold all the countries that will be used for our game, and at the same time we're going to create two more properties that will hold the player's current score – it's a game, after all!

Let's start with the new properties. Add these two lines directly beneath the @IBOutlet lines you added earlier in ViewController.swift:

var countries = [String]()
var score = 0

The first line is something you saw in project 1: it creates a new property called countries that will hold a new array of strings. The second one creates a new property called score that is set to 0.

What you're seeing here is called type inference. This means that Swift figures out what data type a variable or constant should be based on what you put into it. This means a) you need to put the right thing into your variables otherwise they'll have a different type from what you expect, b) you can't change your mind later and try to put an integer into an array, and c) you only have to give something an explicit type if Swift's inference is wrong.

To get you started, here are some example type inferences:

  • var score = 0 This makes an Int (integer), so it holds whole numbers.
  • var score = 0.0 This makes a Double, which is one of several ways of holding decimal numbers, e.g. 3.14159.
  • var score = "hello" This makes a String, so it holds text.
  • var score = "" Even though there's no text in the quote marks, this still makes a String.
  • var score = ["hello"] This makes a [String] with one item, so it's an array where every item is a String.
  • var score = ["hello", "world"] This makes a [String] with two items, so it's an array where every item is a String.

It's preferable to let Swift's type inference do its work whenever possible. However, if you want to be explicit, you can be:

  • var score: Double = 0 Swift sees the 0 so thinks you want an Int, but we're explicitly forcing it to be a Double anyway.
  • var score: Float = 0.0 Swift sees the 0.0 and thinks you want a Double, but we're explicitly forcing it to be a Float. I said that Double is one of several ways of holding decimal numbers, and Float is another. Put simply, Double is a high-precision form of Float, which means it holds much larger numbers, or alternatively much more precise numbers.

We're going to be putting all this into practice over the next few minutes. First, let's fill our countries array with the flags we have, so add this code inside the viewDidLoad() method:

countries.append("estonia")
countries.append("france")
countries.append("germany")
countries.append("ireland")
countries.append("italy")
countries.append("monaco")
countries.append("nigeria")
countries.append("poland")
countries.append("russia")
countries.append("spain")
countries.append("uk")
countries.append("us")

This is identical to the code you saw in project 1, so there's nothing to learn here. There's a more efficient way of doing this, which is to create it all on one line. To do that, you would write:

countries += ["estonia", "france", "germany", "ireland", "italy", "monaco", "nigeria", "poland", "russia", "spain", "uk", "us"]

This one line of code does two things. First, it creates a new array containing all the countries. Like our existing countries array, this is of type [String]. It then uses something new, +=. This is called an operator, which means it operates on variables and constants – it does things with them. + is an operator, as are -, *, = and more. So, when you say "5 + 4" you've got a constant (5) an operator (+) and another constant (4).

In the case of += it combines the + operator (add) and the = operator (assign) to make "add and assign." Translated, this means "add the thing on the right to the thing on the left," or in the case of our countries line of code it means, "add the new array of countries on the right to the existing array of countries on the left."

Now that we have the countries all set up, there's one more line to put just before the end of viewDidLoad():

askQuestion()

This calls the askQuestion() method. This method doesn't actually exist yet, so Swift will complain. However, it's going to exist in just a moment. This askQuestion() method will be where we choose some flags from the array, put them in the buttons, then wait for the user to select the correct one.

Add this new method underneath viewDidLoad():

func askQuestion() {
    button1.setImage(UIImage(named: countries[0]), for: .normal)
    button2.setImage(UIImage(named: countries[1]), for: .normal)
    button3.setImage(UIImage(named: countries[2]), for: .normal)
}

The first line is easy enough: we're declaring a new method called askQuestion(), and it takes no parameters. The next three use UIImage(named:) and read from an array by position, both of which we used in project 1, so that bit isn't new either. However, the rest of those lines is new, and shows off two things:

  • button1.setImage() assigns a UIImage to the button. We have the US flag in there right now, but this will change it when askQuestion() is called.
  • for: .normal The setImage() method takes a second parameter: which state of the button should be changed? We're specifying .normal, which means "the standard state of the button."

That .normal is hiding two more complexities, both of which you need to understand. First, this is being used like a data type called an "enum", short for enumeration. If you imagine that buttons have three states, normal, highlighted and disabled. We could represent those three states as 0, 1 and 2, but it would be hard to program – was 1 disabled, or was it highlighted?

Enums solve this problem by letting us use meaningful names for things. In place of 0 we can write .normal, and in place of 1 we can write .disabled, and so on. This makes code easier to write and easier to read, without having any performance impact. Perfect!

A note for pedants: I said UIControlState “is being used like a data type called an enum” rather than “is an enum, because this particular example is rather murky behind the scenes. In Objective-C – the language UIKit was written in – it’s an enum, but in Swift it gets mapped to a struct that just happens to be used* like an enum, so if you want to be technically correct it’s not a true enum in Swift. At this point in your Swift career there is no difference, but let’s face it: “technically correct” is the best kind of correct.

The other thing .normal is hiding is that period at the start: why is it .normal and not just normal? Well, we're setting the title of a UIButton here, so we need to specify a button state for it. But .normal might apply to any number of other things, so how does Swift know we mean a normal button state?

The actual data type setImage() expects is called UIControlState, and Swift is being clever: it knows to expect a UIControlState value in there, so when we write .normal it understands that to mean "the normal value of UIControlState." You could, if you wanted, write the line out in full as UIControlState.normal, but there isn’t much point.

This is how your code should look at this point.

At this point the game is in a fit state to run, so let’s give it a try.

First, select the iPhone 8 simulator by going to the Product menu and choosing Destination > iPhone 8. Now press Cmd+R now to launch the Simulator and give it a try.

You'll immediately notice two problems

  1. We're showing the Estonian and French flags, both of which have white in them so it's hard to tell whether they are flags or just blocks of color
  2. The "game" isn't much fun, because it's always the same three flags!

The second problem is going to have wait a few minutes, but we can fix the first problem now. One of the many powerful things about views in iOS is that they are backed by what's called a CALayer, which is a Core Animation data type responsible for managing the way your view looks.

Conceptually, CALayer sits beneath all your UIViews (that's the parent of UIButton, UITableView, and so on), so it's like an exposed underbelly giving you lots of options for modifying the appearance of views, as long as you don't mind dealing with a little more complexity. We're going to use one of these appearance options now: borderWidth.

The Estonian flag has a white stripe at the bottom, and because our view controller has a white background that whole stripe is invisible. We can fix that by giving the layer of our buttons a borderWidth of 1, which will draw a one point black line around them. Put these three lines in viewDidLoad() directly before it calls askQuestion():

button1.layer.borderWidth = 1
button2.layer.borderWidth = 1
button3.layer.borderWidth = 1

Remember how points and pixels are different things? In this case, our border will be 1 pixel on non-retina devices, 2 pixels on retina devices, and 3 on retina HD devices. Thanks to the automatic point-to-pixel multiplication, this border will visually appear to have more or less the same thickness on all devices.

By default, the border of CALayer is black, but you can change that if you want by using the UIColor data type. I said that CALayer brings with it a little more complexity, and here's where it starts to be visible: CALayer sits at a lower technical level than UIButton, which means it doesn't understand what a UIColor is. UIButton knows what a UIColor is because they are both at the same technical level, but CALayer is below UIButton, so UIColor is a mystery.

Don't despair, though: CALayer has its own way of setting colors called CGColor, which comes from Apple's Core Graphics framework. This, like CALayer, is at a lower level than UIButton, so the two can talk happily – again, as long as you're happy with the extra complexity.

Even better, UIColor (which sits above CGColor) is able to convert to and from CGColor easily, which means you don't need to worry about the complexity – hurray!

So, so, so: let's put all that together into some code that changes the border color using UIColor and CGColor together. Put these three just below the three borderWidth lines in viewDidLoad():

button1.layer.borderColor = UIColor.lightGray.cgColor
button2.layer.borderColor = UIColor.lightGray.cgColor
button3.layer.borderColor = UIColor.lightGray.cgColor

As you can see, UIColor has a property called lightGray that returns (shock!) a UIColor instance that represents a light gray color. But we can't put a UIColor into the borderColor property because it belongs to a CALayer, which doesn't understand what a UIColor is. So, we add .cgColor to the end of the UIColor to have it automagically converted to a CGColor. Perfect.

If lightGray doesn't interest you, you can create your own color like this:

UIColor(red: 1.0, green: 0.6, blue: 0.2, alpha: 1.0).cgColor

You need to specify four values: red, green, blue and alpha, each of which should range from 0 (none of that color) to 1.0 (all of that color). The code above generates an orange color, then converts it to a CGColor so it can be assigned to a CALayer's borderColor property.

That's enough with the styling, I think. Time to make this into a real game…

Build for watchOS

Take your existing Swift skills to Apple's tiniest platform – check out Hacking with watchOS!

< Previous: Designing your layout   Next: Guess which flag: random numbers >
Click here to visit the Hacking with Swift store >>