Chapters

Hide chapters

iOS Apprentice

Eighth Edition · iOS 13 · Swift 5.2 · Xcode 11

My Locations

Section 4: 11 chapters
Show chapters Hide chapters

Store Search

Section 5: 13 chapters
Show chapters Hide chapters

5. A Fully Working Game
Written by Joey deVilla

You’ve made a lot of progress on the game, and the to-do list is getting shorter! You have a basic version of the game running, where you can generate and display the target value, and you can also calculate and show the player the number of points they’ve scored in the current round.

It’s now time to make a fully-working game, where the player can play multiple rounds and the game keeps a running score. We’ll also give the player the ability to start a new game.

This chapter covers the following:

  • Improving the pointsForCurrentRound() algorithm: Simplifying how the the number of points awarded to the player is calculated.
  • What’s the score?: Calculate the player’s total score over multiple rounds and display it onscreen.
  • One more round…: Implement updating the round count and displaying the current round on screen.
  • Key points: A quick review of what you learned in this chapter.

Improving the pointsForCurrentRound() algorithm

Let’s do a little more refactoring of pointsForCurrentRound(), the method that calculates how many points to award to the player based on the difference between the target value and where they put the slider. Here’s its code at the moment:

func pointsForCurrentRound() -> Int {
  let difference: Int
  if self.sliderValueRounded > self.target {
    difference = self.sliderValueRounded - self.target
  } else if self.target > self.sliderValueRounded {
    difference = self.target - self.sliderValueRounded
  } else {
    difference = 0
  }
  return 100 - difference
}

Most of the code in this method is devoted to making sure that difference — the difference between the slider value and the target value — is always positive. This is done by making sure that the smaller value is always subtracted from the larger value.

“Absolute” power

There’s a simpler way to do this, and it comes from one of the many math functions built into the Swift Standard Library: The abs() function. Given a number, which can be an Int, a Double or any other Swift data type that represents a number, it returns the absolute value of that number, which is the value of that number, but ignoring the sign.

Here are some examples of abs() in action:

  • abs(5) returns 5
  • abs(-5) returns 5
  • abs(-5.25) returns 5.25

➤ Let’s use abs() to simplify the code in pointsForCurrentRound(). Change its code to the following:

func pointsForCurrentRound() -> Int {
  let difference = abs(self.sliderValueRounded - self.target)
  return 100 - difference
}

Note that you didn’t have to specify difference’s data type. That’s because Swift can infer it from the code on the right side of the = sign: self.sliderValueRounded and self.sliderValueRounded are both Ints, subtracting the latter from the former also yields an Int and the absolute value of that result is also an Int. Based on this, Swift infers that difference is an Int.

➤ Run the app and click Hit me!. It works as before, without any changes that the player will notice, but with much less code:

Good code is simple and readable, and this often translates to less code. If you can get the same result using less code, you get not only the benefits of simplicity and readability, but fewer lines of code makes it less likely to introduce bugs.

Removing a “magic number”

Here’s the current code for pointsForCurrentRound():

func pointsForCurrentRound() -> Int {
  let difference = abs(self.sliderValueRounded - self.target)
  return 100 - difference
}

If you’ve worked on the code recently, it’s probably quite obvious to you what the 100 in return 100 - difference is for. It’s the maximum possible score, which happens when the player positions the slider right at the target value.

However, if you spend some time away from Bullseye’s code and then return to it, you might have forgotten where the 100 comes from. You might also decide to change this value at a later point.

There’s a programming term for numbers like this that appear in code: magic numbers. They’re called magic because they’re just there, without any explanation or context; they just “magically” appear in the code. In programming, we strongly discourage the use of magic numbers, and recommend that you replace them with a constant with a name that explains what the number is for.

➤ Let’s define a new constant, maximumScore, to replace the magic number. Change the code for pointsForCurrentRound() to this:

func pointsForCurrentRound() -> Int {
  let maximumScore = 100
  let difference = abs(self.sliderValueRounded - self.target)
  return maximumScore - difference

Once again, you don’t have to specify maximumScore’s data type. Based on the value of 100 assigned to it, Swift will infer that maximumScore is an Int.

➤ Run the app. Once again, it works as it did before the change.

You’ve replaced a number without context — 100 — with the constant maximumScore, which both holds the value 100 and explains what it’s for. Even with this additional line, you still have a pointsForCurrentRound() that’s less than a third the size of the original.

What’s the score?

Now that you have a lean, mean pointsForCurrentRound() and know how far off the slider is from the target, it’s time to keep track of the player’s score.

The first thing you’ll need is a place to store the score. Think about the nature of the score:

  • It should have a name that makes its use and purpose clear: score.
  • It’s a whole-number value. This means that it should be an Int. It should have an initial value of 0.
  • It’s part of the state of the game. Thus means that it should be marked with the @State keyword.

➤ Add the new variable to the User interface views section of ContentView’s properties, just below the declaration for sliderValueRounded . The section should look like this at the end:

// User interface views
@State var alertIsVisible = false
@State var sliderValue = 50.0
@State var target = Int.random(in: 1...100)
var sliderValueRounded: Int {
  Int(self.sliderValue.rounded())
}
@State var score = 0

Now that there’s a score variable, there needs to be code to add the points that the player earned to it. The player earns points when they tap the Hit me! button, so that seems like a logical place to calculate the total score.

The code for the Hit me! button is in the body variable, in the Button row section:

// Button row
Button(action: {
  print("Points awarded: \(self.pointsForCurrentRound())")
  self.alertIsVisible = true
}) {
  Text("Hit me!")
}
.alert(isPresented: self.$alertIsVisible) {
  Alert(title: Text("Hello there!"),
        message: Text(self.scoringMessage()),
        dismissButton: .default(Text("Awesome!")))
}

The code in the Button view’s action: parameter is executed whenever it’s tapped. Right now, that code is:

print("Points awarded: \(self.pointsForCurrentRound())")
self.alertIsVisible = true

This code does the following:

  • It outputs the points that the player has earned for the current attempt on Xcode’s console. This only happens with the Simulator, or on a connected device that’s running the app from Xcode, and will only be seen by the programmer. The user never sees this.
  • It sets the alertIsVisible property to true, which causes the alert pop-up to appear.

Let’s add to this code. We should add the points that are being awarded to the player for this round to the total score.

➤ Change the code for the Button view so that it looks like the following:

// Button row
Button(action: {
  print("Points awarded: \(self.pointsForCurrentRound())")
  self.alertIsVisible = true
  self.score = self.score + self.pointsForCurrentRound()
}) {
  Text("Hit me!")
}
.alert(isPresented: self.$alertIsVisible) {
  Alert(title: Text("Hello there!"),
        message: Text(self.scoringMessage()),
        dismissButton: .default(Text("Awesome!")))
}

There’s now a place in which to store the score, and there’s a way to add to the score when the player taps Hit me!. It’s now time to display the score.

Since score is a @State variable, it means that Swift constantly watches it for changes, and immediately updates any user interface elements that make use of it when those changes happen. Let’s set up that user interface element.

➤ Scroll down to the part of the body variable marked Score row and change it to the following:

// Score row
HStack {
  Button(action: {}) {
    Text("Start over")
  }
  Spacer()
  Text("Score:")
  Text("\(self.score)")
  Spacer()
  Text("Round:")
  Text("999")
  Spacer()
  Button(action: {}) {
    Text("Info")
  }
}
.padding(.bottom, 20)

Note the change. You’ve replaced this hard-coded score:

Text("999999")

With:

Text("\(self.score)")

➤ Run the app. When it starts, you’ll see something like this:

Note that the player’s score is no longer 999999, but 0, which is the initial value assigned to score. The Text view now displays the score, which is always up to date because score is a @State variable.

➤ Click Hit me! The pop-up will appear:

The pop-up acts as you’d expect, displaying the slider’s value, the target value, and the number of points the player scored.

What’s new is the score. If you look at the bottom of the screen, you’ll see that the score has been updated, with the player’s points have been added to it.

➤ Click Awesome! The pop-up will be dismissed, and you’ll have another change to position the slider. Move the slider, and click Hit me! again:

Once again, the pop-up appears, along with value, the target value, and the number of points the player scored. And once again, if you look at the bottom of the screen, you’ll see that the score has been updated.

There’s just one problem now: The target never changes. We’ll fix that by starting a new round.

One more round…

Once the player has tapped Hit me! and been awarded their points, the game should present the player with a new target. This means coming up with a new random value for target. That part is easy:

self.target = Int.random(in: 1...100)

The trickier part is figuring out where to put this code.

The most obvious place is inside the action: parameter of the Button view. Right now, code in this parameter causes the pop-up to appear and updates the score. It looks like a good place to generate a new target value.

➤ Change the code in body at the start of the Button row section so that it looks like the following:

// Button row
Button(action: {
  print("Button pressed!")
  self.alertIsVisible = true
  self.score = self.score + self.pointsForCurrentRound()
  self.target = Int.random(in: 1...100)
}) {

➤ Run the app and make a note of the target value — don’t do anything else just yet:

In the example above, the target value is 9.

➤ Click Hit me!. 99 times out of 100, you’ll see that the target value has changed!

The target value was 9 before you clicked Hit me!, and changed — both on the main screen and in the pop-up — to 94.

The player was awarded 56 points, and if you do the math:

  • Take the maximum score of 100,
  • the new target value of 94 and the slider position of 50, making a difference of 44,
  • which makes for 56 points, which comes from 100 - 44.

How did this happen?

Asynchronous code execution

You probably know that computers — your iOS device included — can perform several tasks at the same time, either by actually performing tasks simultaneously, or combining careful scheduling with their millisecond speed to make it appear as if they’re multitasking.

Let’s look at the Button code again:

// Button row
Button(action: {
  print("Points awarded: \(self.pointsForCurrentRound())")
  self.alertIsVisible = true
  self.score = self.score + self.pointsForCurrentRound()
  self.target = Int.random(in: 1...100)
}) {

The multitasking starts when alertIsVisible is set to true. The program follows two paths:

  1. The program continues to execute the rest of the code in Button’s action parameter.
  2. Setting the state variable alertIsVisible to true triggers Button’s .presentation() method and causes the alert pop-up to appear.

These two paths of execution happen over a span of milliseconds, and practically simultaneously.

The diagram below might make it easier to understand what’s happening:

The series of events that causes the alert pop-up to appear happens right away, but the code in the action: parameter has already updated the score and generated a new target number before the alert pop-up has even finished drawing itself onscreen.

In programmer-speak, alerts work asynchronously. We’ll talk much more about that in a later chapter, but it means that you should keep in mind that a lot of code that updates the user interface based on changes to state variables often gets executed at the same time. Clearly, we’ll need to take another approach.

➤ Change the code in body at the start of the Button row section so that the only code in the button’s action: parameter sets alertIsVisible to true. The section should end up like this:

// Button row
Button(action: {
  self.alertIsVisible = true
}) {
  Text("Hit me!")
}
.alert(isPresented: self.$alertIsVisible) {
  Alert(title: Text("Hello there!"),
        message: Text(self.scoringMessage()),
        dismissButton: .default(Text("Awesome!"))
  )
}

Finding a better place to start a new round

The problem with our first approach is that it tried to start a new round in response to the player tapping Hit me!, which is before the alert pop-up gets displayed. The new round should start in response to the player dismissing the alert pop-up, which happens when they tap the pop-up’s Awesome! button.

It turns out that the Alert object, which is initialized at the end of the button row section of code…

Alert(title: Text("Hello there!"),
      message: Text(self.scoringMessage()),
      dismissButton: .default(Text("Awesome!")))

…accepts an optional extra parameter after dismissButton:, and that parameter is code to be executed when the alert pop-up is dismissed.

➤ Change the code in bodys Button row section so that it looks like this:

// Button row
Button(action: {
  print("Button pressed!")
  self.alertIsVisible = true
}) {
  Text("Hit me!")
}
.alert(isPresented: self.$alertIsVisible) {
  Alert(title: Text("Hello there!"),
        message: Text(self.scoringMessage()),
        dismissButton: .default(Text("Awesome!")) {
          self.score = self.score + self.pointsForCurrentRound()
          self.target = Int.random(in: 1...100)
        }
  )
}

Note: You may have noticed that objects and methods seem to expect some of their parameters inside parentheses (()) and some of them inside braces ({}). We’ll explain why this is so later in this book, and it will all make sense. For now, just trust in the code that we’re showing you.

➤ Run the app, and, once again, make a note of the target value first:

In the example above, the target value is 73.

➤ Click Hit me!. This time, you’ll see that the target value is still 73, and that the points earned this round are based on that value:

You should also note that the score hasn’t been updated yet — it’s still 0.

➤ Dismiss the pop-up by pressing Awesome!, and make a note of the target value and score:

The target value is new, and the score has been updated. Now it’s time to properly display the current round.

Showing the current round

Just as there’s a designated place to store the score, there also needs to be a place to store the number of the current round.

Think about what this variable should be like:

  • It should have a name that makes its use and purpose clear: round.
  • It’s a whole-number value. This means that it should be an Int. It should have an initial value of 1.
  • It’s part of the state of the game. Thus means that it should be marked with the @State keyword.

➤ Add the new variable to the User interface views section of ContentView’s properties. The section should look like this at the end:

// User interface views
@State var alertIsVisible = false
@State var sliderValue = 50.0
@State var target = Int.random(in: 1...100)
var sliderValueRounded: Int {
  Int(self.sliderValue.rounded())
}
@State var score = 0
@State var round = 1

Now that we have the round variable, we need code to increase its value by 1 — or in programmer-speak; increment it — at the start of a new round. A new round starts when the player dismisses the alert pop-up, so that’s where this code should go.

The code for the Hit me! button is in the body variable, in the button row section:

➤ Change the code at the end of bodys Button row section so that it looks like this:

Alert(title: Text("Hello there!"),
      message: Text(scoringMessage()),
      dismissButton: .default(Text("Awesome!")) {
        self.score = self.score + self.pointsForCurrentRound()
        self.target = Int.random(in: 1...100)
        self.round = self.round + 1
      }
)

There’s now a place in which to store the number of the current round, and that number is incremented when the player dismissed the alert pop-up. It’s now time to display it.

As with score, round is a state variable, it means that Swift constantly watches it for changes, and changes cause any user interface elements that make use of it to be updated. Let’s set up that user interface element.

➤ Scroll to the part of the body variable marked Score row and change it to the following:

// Score row
HStack {
  Button(action: {}) {
    Text("Start over")
  }
  Spacer()
  Text("Score:")
  Text("\(self.score)")
  Spacer()
  Text("Round:")
  Text("\(self.round)")
  Spacer()
  Button(action: {}) {
    Text("Info")
  }
}
.padding(.bottom, 20)

Note the change. You’ve replaced this hard-coded count of rounds:

Text("999")

With:

Text("\(self.round)")

➤ Run the app. When it starts, you’ll see something like this:

Note that the starting round is no longer 999, but 1, which is the initial value assigned to round. The Text view now displays the correct round, which is always up to date because round is a @State variable.

➤ Click Hit me!, and then dismiss the alert pop-up when it appears. You’ll see something like this:

The screen shows that you’re now on round 2.

All the display elements work now!

Key points

You’ve got a mostly-working game; feel free to take a victory lap.

In this chapter, you did the following:

  • You improved pointsForCurrentRound()’s algorithm by using the abs() function from the Swift Standard Library, reducing the number of lines in the method by more than two-thirds.
  • You also made pointsForCurrentRound() more readable by replacing a “magic number” with a constant.
  • Added the ability to store and display the cumulative score and current round.

In the next chapter, you’ll do some more refactoring, tweak the game to improve it, and give the player the ability to start a new game.

You can find the project files for the app up to this point under 05 — Rounds and Score in the Source Code folder. If you get stuck, compare your version of the app with these source files to see if you missed anything.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.