Leave a rating/review
Bull’s Eye is coming along well, but there’s a problem: right now, the random target never changes. It’s always the same random value we get when we first start the app.
We’ll fix that by generating a new random target value each time the user taps the Hit me button.
Let’s take a look at how we can do this - and in the process, we’ll encounter our first bug in the app, and then we’ll learn how to fix it using the Xcode debugger.
Open Game.swift and add to the bottom of startNewRound(points:):
target = Int.random(in: 1...100)
Open ContentView.swift and add this to HitMeButton’s body, right after alertIsVisible = true:
game.startNewRound(points: game.points(sliderValue: Int(sliderValue)))
Run the app and do the math for the points. Bring up calc and a text editor, something like this:
Target: 6
Slider: 9
Expected Points: 97
Actual points: 80
Why?
New target 29
Slider: 9
Points: 80
When you’re running into an unexpected problem when you’re coding, one of the best ways to figure out what went wrong is to use Xcode’s built in debugger.
The Xcode debugger allows you to set certain lines of code as breakpoints. Breakpoints is a fancy way of saying “hey Xcode - when you hit that line of code, stop what you’re doing so I can take a look at what’s going on with the app.”
When it’s stopped on a line of code, you can then examine the state of various variables and see if it’s what you expect.
The debugger also has four buttons you can use to control the flow of execution.
The first button, continue, says “keep going until you hit the next breakpoint.
The second button, step over, says move onto the next line of code, stepping over any calls to methods.
The third button, step into, says move onto the next line of code, stepping into any methods you call.
And the fourth button, step out, says run the rest of this method, and continue execution back where this method was called.
Let’s give the debugger a shot and see if we can use it to narrow down what’s going on here.
Set a breakpoint on alertIsVisible = true.
Set a breakpoint on the “return Alert” line too.
Run the app, and tap hit me.
Use Step over, then step into a few times, until you get into points.
SiderValue = 50 Target = 65
Hit continue. Show that sliderValue is what you expect, but Game has a new target now. So when it goes to calculate the points, it’s going to be wrong.
Show the breakpoint navigator and how to delte all breakpoints.
Let’s review what happened here, let’s look at the Button
code again:
Button(action: {
alertIsVisible = true
game.startNewRound(points: game.points(sliderValue: Int(sliderValue)))
}) {
Here we have two lines of code that run when the button is tapped - it sets alertIsVisible to true, and then updates the score and target.
Remember that alertIsVisible
is a binding to a @State variable. So when it’s updated, the UI needs to be refreshed so that the UI is consistent with the state.
So that means when alertIsVisible
is set to true, the program does two things:
- First, the program continues to execute the rest of the code in the
Button
action, which calculates the point, updates the score, and sets a new random target. - Shortly thereafter, the entire ContentView refreshes itself to be consistent with the state, hence refreshing the HitMeButton which is a subview, hence making the alert appear.
So here’s the problem. When we go to refresh the UI, we see we need to present an alert. So we calculate the points, which compares the slider value to the target value.
But by this point, the target value has already been updated to a new random value for the new round, rather than staying as the old random value. This causes the calculation to be incorrect.
To fix this, we need to have better control over the timing here. We don’t want to update the total score and target value until AFTER the user has tapped the ‘Awesome’ button on the alert.
Let’s see how we can fix this bug.
Cut this lines:
game.startNewRound(points: game.points(sliderValue: Int(sliderValue)))
Add this variable to the top of alert:
let points = game.points(sliderValue: roundedValue)
Update text to use it:
"You scored \(points) points this round."
Add to dismiss button:
dismissButton: .default(Text("Awesome!")) {
game.startNewRound(points: points)
})
Build, run, and try again - it works!