Chapters

Hide chapters

iOS Apprentice

Eighth Edition · iOS 13 · Swift 5.2 · Xcode 11

Getting Started with SwiftUI

Section 1: 8 chapters
Show chapters Hide chapters

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

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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.

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

Removing a “magic number”

Here’s the current code for pointsForCurrentRound():

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

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.

// 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
// 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!")))
}
print("Points awarded: \(self.pointsForCurrentRound())")
self.alertIsVisible = true
// 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!")))
}
// 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)
Text("999999")
Text("\(self.score)")

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)
// Button row
Button(action: {
  print("Button pressed!")
  self.alertIsVisible = true
  self.score = self.score + self.pointsForCurrentRound()
  self.target = Int.random(in: 1...100)
}) {

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.

// 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)
}) {

// 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.

Alert(title: Text("Hello there!"),
      message: Text(self.scoringMessage()),
      dismissButton: .default(Text("Awesome!")))
// 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)
        }
  )
}

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.

// 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
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
      }
)
// 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)
Text("999")
Text("\(self.round)")

Key points

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

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.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now