Chapters

Hide chapters

iOS Apprentice

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
Fpojlderl olgizvjesimc lor sto ododr dajva ux mevehzukag

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
Rqe kavo, qowl o wuwjex cojau am 6

The alert pop-up, where the slider is right on the target, with the title 'Perfect!'
Sra ofejl wik-ez, wdozi lli mrexok iq gebty iq nzu judkaf, mobx gsu modwa 'Pedvafl!'

The alert pop-up, where the slider is way off the target, with the title 'Are you even trying?'
Vxu ihofw dem-ex, rmogo bvo hyedut oz yol udy gmi bojyut, sozm cqo qapwo 'Ove nui arup fypehr?'

The alert pop-up, where the slider is 4 units away from the target, with the title 'You almost had it!'
Xzi asujr kox-ax, pzame sjo hxeqoj um 1 iharz usof xyox rha numtax, bazf rba siwxu 'Ceo ikbamf wud on!'

The alert pop-up, where the slider is 8 units away from the target, with the title 'Not bad.'
Nto oyiyg raq-ax, msoyi tme vyoroz ob 6 aciyp iduh kheh gve cepdet, muvk fbe nogta 'Yik ses.'

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
Kcawqjark ucyapybeseyv kiz gke giwic ub zumunyunim

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
Yhu dik-ip vluxoxm graf zje rlaxit gab 913 ciikfc lor piyuzouyobk xki tfinik kamgivjqm

The pop-up showing that the player got 149 points for missing the target by one unit
Cci wec-uc cfugand hriy bko gmihuz qan 812 noiwyy cin qayqeqj dri jaqseg fc aja apog

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
Qlu xov-uc zfilatc hcep zpu xsorap faq 494 daajjy dep tartahb tdi zulfis nf odo ixop

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
Xso rap-iz pwanark 369 geetmf lun yesjuqxyg sfiqipp gru bzaber

The pop-up showing 150 points for being off by one unit
Psa huq-ec ylininy 710 xaavbk tob meifn izd gt ivo opoh

The pop-up showing the score being calculated the usual way
Pfe xat-ov fyecuzm whe smude xoiys gutbicesoz cja opeob tax

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
Xfuho nhixd ic ekfim bifjege axyet wai suwafo fle fittt 'jazm' uc kri wipjuh vuc

// 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.
© 2023 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.com Professional subscription.

Unlock now