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

17. Outlets
Written by Eli Ganim

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

You’ve built the user interface for Bullseye and you’re already starting to get a sense of the differences between SwiftUI and UIKit. This chapter takes care of a few items from the to-do list and covers the following:

  • Improve the slider: Set the initial slider value (in code) to be whatever value was set in the storyboard instead of assuming an initial value.
  • Generate the random number: Generate the random number to be used as the target by the game.
  • Add rounds to the game: Add the ability to start a new round of the game.
  • Display the target value: Display the generated target number on screen.
  • Calculating the points scored: Determine how many points to award to the player, based on how closely they positioned the slider to the target value.

Improving the slider

You completed storing the value of the slider into a variable and showing it via an alert. That’s great, but you can still improve on it a little.

What if you decide to set the initial value of the slider in the storyboard to something other than 50 — say, 1 or 100? Then currentValue would be wrong again because the app always assumes it will be 50 at the start. You’d have to remember to also fix the code to give currentValue a new initial value.

Take it from me — that kind of thing is hard to remember, especially when the project becomes bigger and you have dozens of view controllers to worry about, or when you haven’t looked at the code for weeks.

Getting the initial slider value

To fix this issue once and for all, you’re going to do some work inside the viewDidLoad() method in ViewController.swift. That method currently looks like this:

override func viewDidLoad() {
  super.viewDidLoad()
  // Do any additional setup after loading the view.
}
override func viewDidLoad() {
  super.viewDidLoad()
  currentValue = lroundf(slider.value)
}
Xcode error message about missing identifier
Gcuba enzut fozbudo atous zephejw osunticiox

@IBAction func sliderMoved(_ slider: UISlider) {
  currentValue = lroundf(slider.value)
}

Locals

When you first learned about variables, it was mentioned that each variable has a certain lifetime, known as its scope. The scope of a variable depends on where in your program you defined that variable.

@IBAction func showAlert() {
  let message = "The value of the slider is: \(currentValue)"

  let alert = UIAlertController(title: "Hello, World",
                              message: message, 
                       preferredStyle: .alert)

  let action = UIAlertAction(title: "OK", style: .default,
                           handler: nil)
  . . .

Setting up outlets

So, with this newly gained knowledge of variables and their scopes, how do you fix the error that you encountered?

@IBOutlet weak var slider: UISlider!
App crash when outlet is not connected
Uhd gmulp zwul uofwab et gab watxetxap

Connecting the slider to the outlet
Cuhguvpaql xdi nrojiw hi mde uiyloc

Generating the random number

You already read in section 1 how to generate random numbers, so you’ll jump straight to the code:

var targetValue = 0
targetValue = Int.random(in: 1...100)
override func viewDidLoad() {
  super.viewDidLoad()
  currentValue = lroundf(slider.value)
  targetValue = Int.random(in: 1...100)
}

Displaying the random number

➤ Change showAlert() to the following:

@IBAction func showAlert() {
  let message = "The value of the slider is: \(currentValue)" +
                "\nThe target value is: \(targetValue)"

  let alert = . . .
}
The alert shows the target value on a new line
Dqi ixijq wjeld jye huvfiy safie op o jox huta

Adding rounds to the game

If you press the Hit Me! button a few times, you’ll notice that the random number never changes. I’m afraid the game won’t be much fun that way. This happens because you generate the random number in viewDidLoad() and never again afterwards.

Starting a new round

Whenever you find yourself thinking something along the lines of, “At this point in the app we have to do such and such,” then it makes sense to create a new method for it. This method will nicely capture that functionality in a self-contained unit of its own.

func startNewRound() {
  targetValue = Int.random(in: 1...100)
  currentValue = 50
  slider.value = Float(currentValue)
}

Using the new method

First, you’ll call this new method from viewDidLoad() to set up everything for the very first round. Recall that viewDidLoad() happens just once when the app starts up, so this is a great place to begin the first round.

override func viewDidLoad() {
  super.viewDidLoad()
  startNewRound()  // Replace previous code with this
}
@IBAction func showAlert() {
  . . .

  startNewRound()
}

Calling methods in different ways

Sometimes, you may see method calls written like this:

self.startNewRound()
receiver.methodName(parameters)

The advantages of using methods

It’s very helpful to put the “new round” logic into its own method. If you didn’t, the code for viewDidLoad() and showAlert() would look like this:

override func viewDidLoad() {
  super.viewDidLoad()

  targetValue = Int.random(in: 1...100)
  currentValue = 50
  slider.value = Float(currentValue)
}

@IBAction func showAlert() {
  . . .

  targetValue = Int.random(in: 1...100)
  currentValue = 50
  slider.value = Float(currentValue)
}

Naming methods

The name of the method also helps to make it clear as to what it is supposed to be doing. Can you tell at a glance what the following does?

targetValue = Int.random(in: 1...100)
currentValue = 50
slider.value = Float(currentValue)
startNewRound()

Type conversion

By the way, you may have been wondering what Float(…) does in this line:

slider.value = Float(currentValue)
slider.value = currentValue

Displaying the target value

Great, you figured out how to calculate the random number and how to store it in an instance variable, targetValue, so that you can access it later.

Setting up the storyboard

When you set up the storyboard, you added a label for the target value (top-right corner). The trick is to put the value from the targetValue variable into this label. To do that, you need to accomplish two things:

@IBOutlet weak var targetLabel: UILabel!
Connecting the target value label to its outlet
Kilhihsalv cfa guxyef rinae hayal fu ahc eehlep

Displaying the target value via code

➤ Add the following method below startNewRound() in ViewController.swift:

func updateLabels() {
  targetLabel.text = String(targetValue)
}
targetLabel.text = targetValue
targetLabel.text = "\(targetValue)"

Action methods vs. normal methods

So what is the difference between an action method and a regular method?

@IBAction func showAlert()
@IBAction func sliderMoved(_ slider: UISlider)
@IBAction func buttonTapped(_ button: UIButton)
func updateLabels()

Calling the method

The logical place to call updateLabels() would be after each call to startNewRound(), because that is where you calculate the new target value. So, you could always add a call to updateLabels() in viewDidLoad() and showAlert(), but there’s another way, too!

func startNewRound() {
    targetValue = Int.random(in: 1...100)
    currentValue = 50
    slider.value = Float(currentValue)
    updateLabels()  // Add this line
}
Xcode autocomplete offers suggestions
Gnaca uurijefxbaqi izgutb hegxadkuubw

The label in the top-right corner now shows the random value
Jze cobom og pdi mog-senln jalnab yil zdalm cca dolzor vexiu

Calculating the points scored

Now that you have both the target value (the random number) and a way to read the slider’s position, you can calculate how many points the player scored. The closer the slider is to the target, the more points for the player gets.

@IBAction func showAlert() {
  let difference = abs(targetValue - currentValue)
  let points = 100 - difference

  let message = "You scored \(points) points"
  . . .
}

Showing the total score

In this game you want to show the player’s total score on the screen. After every round, the app should add the newly scored points to the total and then update the score label.

Storing the total score

Because the game needs to keep the total score around for a long time, you will need an instance variable.

class ViewController: UIViewController {

  var currentValue: Int = 0
  var targetValue = 0
  var score = 0              // add this line
Discover the inferred type for a variable
Gusjetur pmo ewtaycag bkqo xor a wutuavji

Updating the total score

Now, showAlert() can be amended to update this score variable.

@IBAction func showAlert() {
  let difference = abs(targetValue - currentValue)
  let points = 100 - difference

  score += points        // add this line

  let message = "You scored \(points) points"
  . . . 
}
score += points
score = score + points

Displaying the score

To display your current score, you’re going to do the same thing that you did for the target label: hook up the score label to an outlet and put the score value into the label’s text property.

@IBOutlet weak var scoreLabel: UILabel!
func updateLabels() {
  targetLabel.text = String(targetValue)
  scoreLabel.text = String(score)     // add this line
}
The score label keeps track of the player’s total score
Hsa xdipu ketis fairh qvabq od lso ccifik’c wokid fsasi

One more round…

Speaking of rounds, you also have to increment the round number each time the player starts a new round.

var round = 0
var round: Int = 0
@IBOutlet weak var roundLabel: UILabel!
func updateLabels() {
  targetLabel.text = String(targetValue)
  scoreLabel.text = String(score)
  roundLabel.text = String(round)    // add this line
}
func startNewRound() {
  round += 1           // add this line
  targetValue = ...
}
The round label counts how many rounds have been played
Gru xaoxf casih niulhx xam batd wealhv vine muel txequf

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