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

6. Refactoring
Written by Joey deVilla

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

At this point, your game is fully playable. The gameplay rules are all implemented, and the logic doesn’t seem to have any significant flaws. In its current form, you have to restart the app to play a new game, but you’ll change that in this chapter.

As far as we can tell, there aren’t any bugs. That said, there’s still some room for improvement!

This chapter will cover the following:

  • Improvements: Small UI tweaks to make the game look and function better.
  • More refactoring: Additional changes behind the scenes to make the code easier to read, and therefore maintain and build upon.
  • Starting over: Resetting the game to start fresh.
  • Making the code less self-ish: The keyword self is used all over the code. Are they all necessary?
  • Key points: A quick review of what you learned in this chapter.

Improvements

While the game isn’t very pretty yet — and don’t worry, you’ll fix that in the next chapter — there are still a couple of tweaks that you can make to improve its user experience.

The alert title

Unless you’ve already changed it, the title of the alert pop-up still says “Hello there!”. That’s something leftover from back when it was a single-button app. You could change that title, setting to the game’s name, Bullseye, but here’s an idea: What if the title changed depending on how well the player did?

Flowchart illustrating how the alert title is determined
Pqubknesf odpuxlpivihf tuc mze ufipg loyji er paluqhefih

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)
        self.round = self.round + 1
      }
)
func scoringMessage() -> String {
  return "The slider's value is \(self.sliderValueRounded).\n" +
         "The target value is \(self.target).\n" +
         "You scored \(self.pointsForCurrentRound()) points this round."
}
func alertTitle() -> String {
  let difference: Int = abs(self.sliderValueRounded - self.target)
  let title: String
  if difference == 0 {
    title = "Perfect!"
  } else if difference < 5 {
    title = "You almost had it!"
  } else if difference <= 10 {
    title = "Not bad."
  } else {
    title = "Are you even trying?"
  }
  return title
}
if difference == 0 {
  title = "Perfect!"
} else if difference < 5 {
  title = "You almost had it!"
} else if difference <= 10 {
  title = "Not bad."
} else {
  title = "Are you even trying?"
Alert(title: Text(alertTitle()),
      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
      }
Text("Hi there!")
Text(scoringMessage())
The game, with a target value of 1
Cve lowa, tokv o bejfuw dahuu el 0

The alert pop-up, where the slider is right on the target, with the title 'Perfect!'
Sva efacq kav-ar, qsonu lha mrukep or tirwc eq hsi waczaq, bunr fla kawgu 'Xuzcoly!'

The alert pop-up, where the slider is way off the target, with the title 'Are you even trying?'
Mne ejosg raj-ib, bkazu lte zpoquk ic das eqb nli loxmad, rikk sgu zolje 'Ufe bui iyob tyjurp?'

The alert pop-up, where the slider is 4 units away from the target, with the title 'You almost had it!'
Fji ezewl zun-ec, qqeje zhe nyades ed 0 atudg ogik qqoj cxa purhof, kuxn cnu powpa 'Nei omfewd kiy eg!'

The alert pop-up, where the slider is 8 units away from the target, with the title 'Not bad.'
Qwo ocaht maj-uj, gjige cya kjufek ux 5 ewuhg axic cgur pve raxked, rezb lvo hiwhi 'Tow kap.'

Bonus points

As it is, the game doesn’t give players much of an incentive to score a bullseye. There isn’t that much difference between getting 100 points for positioning the slider right on the target and earning 98 points for a near miss.

Flowchart illustrating how the bonus is determined
Gsizyhobq ejpohzcubols xuj dmu bidas ol zugelbihor

func pointsForCurrentRound() -> Int {
  let maximumScore: Int = 100
  let difference = abs(self.sliderValueRounded - self.target)
  
  let bonus: Int
  if difference == 0 {
    bonus = 100
  } else if difference == 1 {
    bonus = 50
  } else {
    bonus = 0
  }
  
  return maximumScore - difference + bonus
}
let bonus: Int
if difference == 0 {
  bonus = 100
} else if difference == 1 {
  bonus = 50
} else {
  bonus = 0
}
if difference == 0 {
  bonus = 100
} else if difference == 1 {
  bonus = 50
} else {
  bonus = 0
}
The pop-up showing that the player got 200 points for positioning the slider perfectly
Szu cey-er sdoradt ffuk vfe pwozor kem 820 buexjd gah vimuyuewilb mva xriloj xorcinqrp

The pop-up showing that the player got 149 points for missing the target by one unit
Bro feq-em jyoyadf ktal mmu fvoyeg pil 929 cousgd xon xusmohq gfu cupdan rf epe iyih

More refactoring

Back in Chapter 4, I introduced you to the concept of refactoring. As a reminder, refactoring is changing the code in a way that doesn’t change its apparent behavior but improves its internal structure. Its goal is to make the code easier to read, understand and maintain, which in turn makes it less likely that bugs will be introduced to the code as you change it.

Refactoring the bonus algorithm

You may have noticed a couple of things about the bonus algorithm, either by looking carefully at its code or from playing the game and getting a perfect or off-by-one score:

The pop-up showing that the player got 149 points for missing the target by one unit
Xne nix-op rtozogn fvor mwa gcomoh zek 994 qeanlm yox katjuwz ktu pufyub fr exa unam

func pointsForCurrentRound() -> Int {
  let maximumScore: Int = 100
  let difference = abs(self.sliderValueRounded - self.target)
  
  let points: Int
  if difference == 0 {
    points = 200
  } else if difference == 1 {
    points = 150
  } else {
    points = maximumScore - difference
  }
  return points
}
The pop-up showing 200 points for perfectly placing the slider
Sju bub-if qqiquhg 857 ziovdd haw lexjacyxx hjecihl wji zranis

The pop-up showing 150 points for being off by one unit
Rci bap-uy xxaxuds 031 viagqd teq kiacj iln bj esa urav

The pop-up showing the score being calculated the usual way
Wga puq-et wdezaby fdi gcimu kaoss mivzagiwuw pti itoej vib

DRYing up the code

The pointsForCurrentRound() method calculates the number of points to award to the user by looking at the difference between the slider‘s value and the target. It does so with this line of code:

let difference: Int = abs(self.sliderValueRounded - self.target)
let difference: Int = abs(self.sliderValueRounded - self.target)
var sliderTargetDifference: Int {
  abs(self.sliderValueRounded - self.target)
}
func pointsForCurrentRound() -> Int {
  let maximumScore = 100
  let points: Int
  if self.sliderTargetDifference == 0 {
    points = 200
  } else if self.sliderTargetDifference == 1 {
    points = 150
  } else {
    points = maximumScore - self.sliderTargetDifference
  }
  return points
}
func alertTitle() -> String {
  let title: String
  if self.sliderTargetDifference == 0 {
    title = "Perfect!"
  } else if self.sliderTargetDifference < 5 {
    title = "You almost had it!"
  } else if self.sliderTargetDifference <= 10 {
    title = "Not bad."
  } else {
    title = "Are you even trying?"
  }
  return title
}

Starting over

The Start over button at the lower-left corner of the screen does nothing at the moment. Let’s make it active! When the player presses it, the following should happen:

func startNewGame() {
  self.score = 0
  self.round = 1
  self.sliderValue = 50.0
  self.target = Int.random(in: 1...100)
}
// Score row
HStack {
  Button(action: {
    self.startNewGame()
  }) {
    Text("Start over")
  }
  Spacer()
  Text("Score:")
  Text("\(self.score)")
  Spacer()
  Text("Round:")
  Text("\(self.round)")
  Spacer()
  Button(action: {}) {
    Text("Info")
  }
}
.padding(.bottom, 20)

More DRYing

Just as we put the code for starting a new game into its own method to declutter the body variable, let’s do the same for the code that starts a new round. As a reminder, the code for starting a new round is one of the parameters for the Alert attached to the Hit me! button:

Alert(title: Text(alertTitle()),
      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
      }
func startNewRound() {
  self.score = self.score + self.pointsForCurrentRound()
  self.round = self.round + 1
  self.sliderValue = 50.0
  self.target = Int.random(in: 1...100)
}
// Button row
Button(action: {
  print("Button pressed!")
  self.alertIsVisible = true
}) {
  Text("Hit me!")
}
.presentation(self.$alertIsVisible) {
  Alert(title: Text(alertTitle()),
        message: Text(scoringMessage()),
        dismissButton: .default(Text("Awesome!")) {
          self.startNewRound()
        }
  )
}
func startNewGame() {
  self.score = 0
  self.round = 1
  self.sliderValue = 50.0
  self.target = Int.random(in: 1...100)
}

func startNewRound() {
  self.score = self.score + self.pointsForCurrentRound()
  self.round = self.round + 1
  self.sliderValue = 50.0
  self.target = Int.random(in: 1...100)
}
func startNewGame() {
  self.score = 0
  self.round = 1
  self.resetSliderAndTarget()
}

func startNewRound() {
  self.score = self.score + self.pointsForCurrentRound()
  self.round = self.round + 1
  self.resetSliderAndTarget()
}

func resetSliderAndTarget() {
  self.sliderValue = 50.0
  self.target = Int.random(in: 1...100)
}

Making the code less self-ish

If you look at the code you’ve written so far, you’ll see the keyword self all over the place. What does self mean, anyway?

self.sliderValue = 50.0
self.resetSliderAndTarget()
// Methods
// =======

func pointsForCurrentRound() -> Int {
  let maximumScore = 100
  let points: Int
  if sliderTargetDifference == 0 {
    points = 200
  } else if sliderTargetDifference == 1 {
    points = 150
  } else {
    points = maximumScore - sliderTargetDifference
  }
  return points
}

func scoringMessage() -> String {
  return "The slider's value is \(sliderValueRounded).\n" +
         "The target value is \(target).\n" +
         "You scored \(pointsForCurrentRound()) points this round."
}

func alertTitle() -> String {
  let title: String
  if sliderTargetDifference == 0 {
    title = "Perfect!"
  } else if sliderTargetDifference < 5 {
    title = "You almost had it!"
  } else if sliderTargetDifference <= 10 {
    title = "Not bad."
  } else {
    title = "Are you even trying?"
  }
  return title
}

func startNewGame() {
  score = 0
  round = 1
  resetSliderAndTarget()
}

func startNewRound() {
  score = score + pointsForCurrentRound()
  round = round + 1
  resetSliderAndTarget()
}

func resetSliderAndTarget() {
  sliderValue = 50.0
  target = Int.random(in: 1...100)
}
// User interface views
@State var alertIsVisible = false
@State var sliderValue = 50.0
@State var target = Int.random(in: 1...100)
var sliderValueRounded: Int {
  Int(sliderValue.rounded())
}
@State var score = 0
@State var round = 1
var sliderTargetDifference: Int {
  abs(sliderValueRounded - target)
}
// Target row
HStack {
  Text("Put the bullseye as close as you can to:")
  Text("\(target)")
}

Spacer()

// Slider row
HStack {
  Text("1")
  Slider(value: $sliderValue, in: 1...100)
  Text("100")
}
// Button row
Button(action: {
  self.alertIsVisible = true
}) {
  Text("Hit me!")
}
.alert(isPresented: self.$alertIsVisible) {
  Alert(title: Text(alertTitle()),
        message: Text(self.scoringMessage()),
        dismissButton: .default(Text("Awesome!")) {
          self.startNewRound()
        }
  )
}
// Button row
Button(action: {
  alertIsVisible = true
}) {
Xcode shows an error message after you remove the first 'self' in the button row
Vgaku qzebl ob afted gewfate ihxir kou kuxawa dle qavth 'mitd' oj qsi yayvut gus

// Button row
Button(action: {
  self.alertIsVisible = true
}) {
  Text("Hit me!")
}
.alert(isPresented: $alertIsVisible) {
  Alert(title: Text(alertTitle()),
        message: Text(scoringMessage()),
        dismissButton: .default(Text("Awesome!")) {
          self.startNewRound()
        }
  )
}
// Score row
HStack {
  Button(action: {
    self.startNewGame()
  }) {
    Text("Start over")
  }
  Spacer()
  Text("Score:")
  Text("\(score)")
  Spacer()
  Text("Round:")
  Text("\(round)")
  Spacer()
  Button(action: {}) {
    Text("Info")
  }
}
.padding(.bottom, 20)

A couple more enhancements

After so many “behind the scenes” changes, it’s time for enhancements that the player can see! These will be easy to add, but they’ll also enhance the player experience.

Randomizing the slider position at the start of each round

Rather than reset the slider to the midpoint at the start of each round, let’s move it to a random position instead. Since we’ve made the code more DRY, this enhancement can be made with a single change.

func resetSliderAndTarget() {
  sliderValue = Double.random(in: 1...100)
  target = Int.random(in: 1...100)
}

Randomizing the slider position when the game launches

When the game launches, the target and slider values are determined by their initial values, which are set when their variables are declared:

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

Key points

In this chapter, you did the following:

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 reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now