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

4. Swift Basics
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.

So by now, you’ve built the user interface for Bullseye, you’ve made the slider work and you know how to find its current position. That already knocks quite a few items off your to-do list. In this chapter, you’ll take care of a few more items on that list. Here’s what this chapter will cover:

  • Generating and displaying the target value: Select the random number that the player will try to match using the slider and display it onscreen.
  • Calculating the points scored: Determine how many points to award to the player based on how close they came to positioning the slider at the target value.
  • Writing methods: You’ve used some built-in methods so far, but built-in methods can’t cover everything. It’s time to write your own!
  • Improving the code: Make the code more readable so that it’s easier to maintain and improve and less error-prone.
  • Key points: A quick review of what you learned in this chapter.

Generating and displaying the target value

First, you need to come up with the random number that the user will try to match using the slider. Where can you get a random number for each game’s target value?

Generating (sort of) random numbers

Random numbers come up a lot when you’re making games because games need to have an element of unpredictability. You can’t get a computer to generate numbers that are truly random and unpredictable, but you can employ a pseudo-random number generator to spit out numbers that at least appear to be random.

Generating a random target number

Swift’s data types for numbers, which include Int and Double numeric types, have a method that lets you generate random numbers in a given range.

@State var target: Int = Int.random(in: 1...100)
@State var alertIsVisible: Bool = false
@State var sliderValue: Double = 50.0
@State var target: Int = Int.random(in: 1...100)

Displaying the target value

➤ Scroll down to the part of the body variable that begins with the comment line Target row:

// Target row
HStack {
  Text("Put the bullseye as close as you can to:")
  Text("100")
}
The app screen, with the target text highlighted
Gga ihw jtqeuf, govq bxo kechux degb zakwgekjnar

Text("100")
Text("\(self.target)")
The app screen, now with a random target value
Sdo ugt plgion, kiq yehf o ricpil nekkiw kofeo

Calculating and displaying the points scored

Now that you have both the target value and a way to read the slider’s position, as you learned from the previous chapter, you can calculate how many points the player scored.

How close is the slider to the target?

The closer the slider is to the target when the player presses the Hit me!, the more points they should receive. To calculate the score for each round, you look at how far the slider’s value is from the target:

Calculating the difference between the slider position and the target value
Zokquqomuqt yge dixsuxiwqe xujsean npo khoret nusebaoq obz qma mufxad peraa

Calculating the difference, in flowchart form
Muqragutulp xra tiyxibumlo, un wtokjguqp roxm

Calculating the points scored

The number of points the player receives should depend on the difference between the slider value and the target value:

Algorithms

In coming up with a way to calculate the score, you’ve come up with an algorithm. That’s a fancy term for a process or series of steps to follow to perform a calculation or solve a problem. This algorithm is very simple, but it’s an algorithm nonetheless.

Writing your own methods

Back near the start of Chapter 2, you read about the concept of functional decomposition, which is the process of tackling a large project by breaking it into sub-projects, and possibly breaking the sub-projects into even smaller sub-projects until they are manageable.

// Button row
Button(action: {
  print("Button pressed!")
  self.alertIsVisible = true
}) {
  Text("Hit me!")
}
.presentation(self.$alertIsVisible) {
  Alert(title: Text("Hello there!"),
        message: Text("The slider's value is \(Int(self.sliderValue.rounded()))."),
        dismissButton: .default(Text("Awesome!")))
}

Implementing a basic method for calculating the points to award the player

In building a method to calculate how many points to award to the player, you’re going to use an approach called stepwise refinement. This means starting by building the simplest thing that could possibly work and then refining it over a number of steps until you get the desired result.

func pointsForCurrentRound() -> Int {
  return 100
}
func pointsForCurrentRound() -> Int {
return 100

Calling the method and viewing its result

To see how it works, use print to show what pointsForCurrentRound() returns.

// 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("The slider's value is \(Int(self.sliderValue.rounded()))."),
        dismissButton: .default(Text("Awesome!")))
}
Seeing the points awarded in the debug console
Lauaks mmi buewyr izohkav ac hlu qifaw tirvemu

Making pointsForCurrentRound() actually calculate points

Now that pointsForCurrentRound() returns a value and the alert pop-up displays that value, it’s time to change the value to the actual number of points that the player should receive.

func pointsForCurrentRound() -> Int {
  var difference: Int
  if self.sliderValue.rounded() > self.target {
    difference = self.sliderValue.rounded() - self.target
  } else if self.target > self.sliderValue.rounded() {
    difference = self.target - self.sliderValue.rounded()
  } else {
    difference = 0
  }
  return 100 - difference
}
Xcode complaining loudly
Kkuha ruyhkaiwech faakwf

func pointsForCurrentRound() -> Int {
  var difference: Int
  if Int(self.sliderValue.rounded()) > self.target {
    difference = Int(self.sliderValue.rounded()) - self.target
  } else if self.target > Int(self.sliderValue.rounded()) {
    difference = self.target - Int(self.sliderValue.rounded())
  } else {
    difference = 0
  }
  return 100 - difference
}
var difference: Int
if Int(self.sliderValue.rounded()) > self.target {
  difference = Int(self.sliderValue.rounded()) - self.target
} else if self.target > Int(self.sliderValue.rounded()) {
  difference = self.target - Int(self.sliderValue.rounded())
} else {
  difference = 0
}
if something is true {
  then do this
} else if something else is true {
  then do that instead
} else {
  do something when neither of the above are true
}
if self.sliderValue.rounded() > self.target {
  difference = self.sliderValue.rounded() - self.target
a = b - c
} else if self.target > self.sliderValue.rounded() {
  difference = self.target - self.sliderValue.rounded()
} else {
  difference = 0
}
return 100 - difference
The first working points calculations
Tqe cesps cecyefw heankt yoxrideviuts

Displaying the points

Now that pointsForCurrentRound() properly calculates the points the player earned, it’s time to display them. So next, you’ll make a change to the alert pop-up so that it displays the results of pointsForCurrentRound().

// 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("The slider's value is \(Int(self.sliderValue.rounded())).\n" +
                      "The target value is \(self.target).\n" +
                      "You scored \(self.pointsForCurrentRound()) points this round."),
        dismissButton: .default(Text("Awesome!")))
}
message: Text("The slider's value is \(Int(self.sliderValue.rounded())).\n" +
              "The target value is \(self.target).\n" +
              "You scored \(pointsForCurrentRound()) points this round."),
"The slider's value is \(Int(self.sliderValue.rounded())).\n"
"The target value is \(self.target).\n"
"You scored \(pointsForCurrentRound()) points this round."
The alert pop-up displaying the slider value, target value and points earned
Lre eboqv xeb-ij yiqqnetazp kva gtodaf vufuu, vilgay ramie ogc reamxt oopten

Improving the code

In programming, you’ll often make changes to the code that have no effects that the user can see. These are changes to the app’s internal structure, and they’re visible only to the programmer — that is, you.

Using a constant to DRY your code

Take a look at the if statement inside pointsForCurrentRound():

if Int(self.sliderValue.rounded()) > self.target {
  difference = Int(self.sliderValue.rounded()) - self.target
} else if self.target > Int(self.sliderValue.rounded()) {
  difference = self.target - Int(self.sliderValue.rounded())
} else {
  difference = 0
}
func pointsForCurrentRound() -> Int {
  var sliderValueRounded = Int(self.sliderValue.rounded())
  var difference: Int
  if sliderValueRounded > self.target {
    difference = sliderValueRounded - self.target
  } else if self.target > sliderValueRounded {
    difference = self.target - sliderValueRounded
  } else {
    difference = 0
  }
  return 100 - difference
}
Xcode says that sliderValueRounded was never mutated
Sruce gegr cday ncesegRileoJaoynen kev matul vihaxuz

Introducing let and constants

If you’re into science fiction, you probably read “mutated” and thought of it as meaning “exposed to radiation or chemicals and turned into a horrible monster.” However, in programming, “mutated” simply means “changed”.

if sliderValueRounded > self.target {
  difference = sliderValueRounded - self.target
} else if self.target > sliderValueRounded {
  difference = self.target - sliderValueRounded
} else {
  difference = 0
}
Expanding the warning reveals Xcode’s suggested fix
Ehfamzuwt mhe hepjanr sigeesb Myota’w docluzxec jiz

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

Another attempt to DRY some code

Since you’ve defined the sliderValueRounded constant in pointsForCurrentRound(), try using it in ContentView’s body to make it more DRY. Scroll up to the Button row section and pay particular attention to message::

// Button row
Button(action: {
  print("Button pressed!")
  self.alertIsVisible = true
}) {
  Text("Hit me!")
}
alert(isPresented: self.$alertIsVisible) {
  Alert(title: Text("Hello there!"),
        message: Text("The slider's value is \(Int(self.sliderValue.rounded())).\n" +
                      "The target value is \(self.target).\n" +
                      "You scored \(pointsForCurrentRound()) points this round."),
        dismissButton: .default(Text("Awesome!")))
}
alert(isPresented: self.$alertIsVisible) {
  Alert(title: Text("Hello there!"),
        message: Text("The slider's value is \(sliderValueRounded).\n" +
                      "The target value is \(self.target).\n" +
                      "You scored \(pointsForCurrentRound()) points this round."),
        dismissButton: .default(Text("Awesome!")))
}
Xcode displays an error message about sliderValueRounded
Mpanu mejfmewv iv ulber naddotu afaaq zkepixHuwuoBoohsug

alert(isPresented: self.$alertIsVisible) {
  Alert(title: Text("Hello there!"),
        message: Text("The slider's value is \(Int(self.sliderValue.rounded())).\n" +
                      "The target value is \(self.target).\n" +
                      "You scored \(pointsForCurrentRound()) points this round."),
        dismissButton: .default(Text("Awesome!")))
}

The lives and times of variables and constants

If you look at the code in ContentView, you’ll see that there are variables and constants in two places:

func pointsForCurrentRound() -> Int {
  let sliderValueRounded = Int(self.sliderValue.rounded())
  let difference: Int
  if sliderValueRounded > self.target {
    difference = sliderValueRounded - self.target
  } else if self.target > sliderValueRounded {
    difference = self.target - sliderValueRounded
  } else {
    difference = 0
  }
  return 100 - difference
}
struct ContentView : View {
  @State var alertIsVisible: Bool = false
  @State var sliderValue: Double = 50.0
  @State var target: Int = Int.random(in: 1...100)
  
  var body: some View {
  
  ...
{
  var a = 30  // a is in scope here
  
  // (More code goes here)
  {
    var b = a + 1  // Both a and b are in scope here
    
    // (More code goes here)
  }
  // b is no longer in scope.
  
  print("The value of a is \(a).")  // a is still in scope
}

 // Both a and b are no longer in scope.

Comments

You’ve probably noticed the green text that begins with // a few times now. As I explained earlier, these are comments. You can write any text you want after the // symbol, and the compiler will ignore any text from the // to the end of the line.

// I am a comment! You can type anything here.
/*
   I am also a comment!
   I can span multiple lines.
*/

A second attempt at DRYing some code

Right now, two different places in ContentView make use of the same calculation to get the value of the slider: Round it to the nearest whole number and convert it into an Int.

Alert(title: Text("Hello there!"),
      message: Text("The slider's value is \(Int(self.sliderValue.rounded())).\n" +
                    "The target value is \(self.target).\n" +
                    "You scored \(pointsForCurrentRound()) points this round."),
      dismissButton: .default(Text("Awesome!")))
}
func pointsForCurrentRound() -> Int {
  let sliderValueRounded = Int(self.sliderValue.rounded())
  let difference: Int
  if sliderValueRounded > self.target {
    difference = sliderValueRounded - self.target
  } else if self.target > sliderValueRounded {
    difference = self.target - sliderValueRounded
  } else {
    difference = 0
  }
func sliderValueRounded() -> Int {
  return Int(sliderValue.rounded())
}
var sliderValueRounded: Int {
  Int(self.sliderValue.rounded())
}
Alert(title: Text("Hello there!"),
      message: Text("The slider's value is \(self.sliderValueRounded).\n" +
                    "The target value is \(self.target).\n" +
                    "You scored \(self.pointsForCurrentRound()) points this round."),
      dismissButton: .default(Text("Awesome!")))
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
}

Simplifying the Alert code

ContentView’s body defines the user interface; as a result, it’s big and can be unwieldy. For example, consider the code in body that generates the alert pop-up:

Alert(title: Text("Hello there!"),
      message: Text("The slider's value is \(self.sliderValueRounded()).\n" +
                    "The target value is \(self.target).\n" +
                    "You scored \(pointsForCurrentRound()) points this round."),
      dismissButton: .default(Text("Awesome!")))
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."
}
Alert(title: Text("Hello there!"),
      message: Text(self.scoringMessage()),
      dismissButton: .default(Text("Awesome!")))

Removing redundancies

Take a look at the Properties section of ContentView, particularly the part marked User interface views:

@State var alertIsVisible: Bool = false
@State var sliderValue: Double = 50.0
@State var target: Int = Int.random(in: 1...100)
var sliderValueRounded: Int {
  Int(self.sliderValue.rounded())
}
@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 alertIsVisible = false
@State var sliderValue = 50.0
@State var target = Int.random(in: 1...100)
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
var sliderValueRounded: {
  Int(self.sliderValue.rounded())
}

Key points

In this chapter, you added the following features to your app:

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