We've finally fixed the bug that prevented us from even launching the app. Now we can PLAY! 🤗
The purpose of this course is to learn testing, and since we've only just begun this course, you can probably guess there will be more surprises in the app's functionality...
Well, let's play anyway! 😅
Launch the app and click on the grid to emulate the game. We know that the game should be over when one of the following conditions are met:
Winning the game:
3 of the same cards appear horizontally in one of the 3 rows.
3 of the same cards appear vertically in one of the 3 columns.
3 of the same cards appear diagonally from the left top corner to the bottom right corner.
3 of the same cards appear diagonally from the left bottom corner to the top right corner.
A draw:
No cards are aligned, but all the spots are taken.
Go ahead and emulate those circumstances and observe the results! Here's the outcome:

We can see there's a mistake: there's no game over when we have no spots left on the board! There's clearly a bug in the app's functionality (even though the app is not crashing).
Business logic errors
Bugs of this type are called business logic bugs - algorithm errors. These are definitely harder to discover and eliminate. They involve a more detailed debugging process.
Business logic errors can occur for two main reasons:
By design: when algorithm creators believe the logic is correct but it turns out to produce an incorrect business result.
Accidentally: when, while programming, you believe you interpreted the designed business logic correctly, but due to a mistake in interpretation, the app also turns out to produce an incorrect business result.
In either case, the program from a coding perspective functions well and there are no coding mistakes - no programming bugs. 😇
It's good news. At least we don't have runtime errors and our app doesn't crash! On the other hand, this type of error is harder to identify and fix.
Dealing with this type of bugs involve a more detailed debugging process.
Exploring strategies for solving business logic errors
One of the challenges you'll face at this point is that you don't know the app code well since you weren't the one who wrote it in the first place. And we can't use the exception breakpoint technique to take you to the line of code in question because potential errors don't crash the app.
So, how to find a needle in a haystack? 😳
Don't panic! We'll break down the process in 3 phases and soon following this process will become second nature! Here they are:
Reproduce the sequence of actions in the application that lead to the bug.
Add a breakpoint manually.
Navigate the code execution to find the bug.
Let's do it! 💪
1. Reproduce the sequence of actions
To reproduce the bug we need to emulate a game having all the spots taken but without a winning sequence. To make it simple, lets fill the spot in sequence: First column, last column, middle column just following the cells in order.
Everything seems to work well until we fill the last spot. We'd expect the game to be over in a draw, but instead, the app allow us to continue to play. 😕
Let's investigate!
First we need to determine which function is called on each card press. For that, we'll refer to the storyboard and can quickly establish that all the buttons on the board are connected to the same method: cardButtonPress
.
Congrats! You've narrowed down your search quite a bit!
2. Add a breakpoint manually
Next, we're going to add a breakpoint to pause our app execution to be able to investigate what's going on in detail.
Identify the line
We've identified the function where the mistake is likely hiding. Two aspects remain unclear though:
The same method is called when we put any of the cards on the board and in some cases we expect the game to continue and in others to be over.
It's unclear which line exactly is causing the trouble.
Here's the code of the function:
@IBAction func cardButtonPress(_ sender: UIButton) {
if let index = board.index(of: sender) {
let cardTitle = game.advance(atIndex: index)
markBoardCard(withTitle: cardTitle, at: index)
if game.isOver {
if let winningIndices = game.winningIndices {
for i in 0 ..< winningIndices.count {
board[winningIndices[i]].backgroundColor = winnerColor
}
Timer.scheduledTimer(withTimeInterval: 1, repeats: false, block: {_ in
self.presentWinner()
})
}
else {
presentWinner()
}
}
else {
setNextPlayer()
}
}
}
Well, we could start at the start and begin at the beginning: the very first line of cardButtonPress
method. However, we know how to read the code, right?! So, let's flex our brain muscle! 🤔
In the beginning of the function, we are attempting to recognize a button object in the board array that was pressed and then mark the corresponding spot accordingly. This seems to be working alright.
The next section is an if/else statement that checks if the game is over or not and proceeds accordingly.
Aha! The 'Game Over' criteria seem to be faulty! And this is the line of code that's responsible for the outcome:
if game.isOver {
// ...
This is where we'll add a breakpoint!
Add a breakpoint
Adding a breakpoint is simple - click on the number of the line. The new breakpoint will appear in blue:
3. Navigate the code execution
Navigation commands
Now that our breakpoint is created, we can restart the app and press a card - the very first cell, for example. And here it is: the execution is paused at the line of our new breakpoint and we are taken right there:
This line is a good guess for a starting point to navigate the execution further. For that, we'll use the buttons located at the top of the Debug area:
In particular, we'll be using the first 5 of them:
This option allows us to toggle the manual breakpoints - either enable (blue) or disable (grey) them. When disabled, the breakpoints will be ignored during the execution. The app will not be paused just like if we never added any breakpoint.
This command resumes the execution from the current line of code to the next breakpoint in the app or until the end of the logic before a user's interaction is required. For example, if you have another breakpoint on a button press, that button needs to be pressed by a user in order to reach that breakpoint.
Using this option moves execution to the next line of code. If the current command calls a method, the full method will be executed without going into the details of that method.
This option allows to step-in a block of code such as a method, function, property, etc. With this we are taken down the call stack.
With this action we take up the call stack.
Well, now that we know our options, let's navigate the code!
Diving in
The if
statement is analyzing the isOver
property of the game
instance.
To show our intention to go into the details, press the Step in button (). This will take us to where we are instantiating our game object:
var game = Game()
We don't need to go into the details of this process, so we now can step over it (). And it appears like we are taken back to the original line. Now press the Step in button again:
var isOver: Bool {
return board.isFull || winner != nil
}
We are now taken to the isOver calculated property of the Game class. Looks like we are getting closer!
This property analyzes two other calculated properties - the isFull
property of a board instance and its own property, winner
. Based on our experience, the winner is being calculated correctly. This leaves us to conclude that the issue must be within the isFull property calculation.
Let's explore further by stepping into the isFull property:
var isFull: Bool {
return cards.filter { $0 == nil }.count == cards.count
}
This property contains only one line of code. The intention here is to compare the number of taken spots on the board to the total number of cards. In order to understand what's going on, let's inspect what this line generates. We can do that by printing the future value of the taken spots in the console:
(lldb)printcards.filter{$0==nil}.count (Int)$R1=8 (lldb)
At this point, we've just started the game and pressed on the first spot. We've already passed the code that marked the spot, so this calculation should have the value 1 but it printed 8! 😱
We've got to look closer... Notice that instead of identifying the taken spots as not nil, we are performing a comparison to nil.
Well, looks like making the following adjustment should fit the problem:
var isFull: Bool {
return cards.filter { $0 != nil }.count == cards.count
}
Now we need to retest it to make sure it generates the correct value. Remember, we had to go through a number of lines of code from the breakpoint we setup to get here? So, it may be not a bad idea to put a new breakpoint right here and delete the old one:
So that now, when we relaunch the app and press one of the cards, we are taken directly to the line we need. This time - while stopping at this line - if we print the new version in the Console, we'll get a very different result:
(lldb) print cards.filter { $0 != nil }.count (Int) $R1 = 1 (lldb)
This implementation prints 1 - which is what's expected!
Now let's disable the breakpoint and continue playing until we fill all the spots and see if this time we can reach the Game Over state.

Excellent job! 🙌
Let's Recap!
Some errors do not crash the application, but still cause incorrect behavior of the app. They are called business logic errors.
Breakpoints can be added manually by clicking on a line number in Xcode.
From a breakpoint, you can navigate through the code execution to go to the next breakpoint, move to the next line, or move up or down in the call stack.