To continue your debugging experiments, you are going to add another bug to your application. Add the code below to ViewController.swift. Notice that you will be using an NSMutableArray, the Objective-C counterpart of Swift’s Array, to make the bug a little harder to find.
@IBAction func buttonTapped(_ sender: UIButton) { print("Method: (#function) in file: (#file) line: (#line) called.") badMethod() } func badMethod() { let array = NSMutableArray() for i in 0..<10 { array.insert(i, at: i) } // Go one step too far emptying the array (notice the range change): for _ in 0...10 { array.remove(at: 0) } }
Build and run the application to confirm that a tap on the button results in the application crashing with an uncaught NSRangeException exception. Use your freshly acquired knowledge to study and interpret the error message as much as possible.
If you used a Swift Array type to create this bug, Xcode would have been able to highlight the line of code that caused the exception. Because you used an NSMutableArray, the code that raised the exception is deep within the Cocoa Touch framework. Frequently this is the case when debugging; problems are not so obvious and you need to do some investigative work.
Assume that you do not know the direct cause of the crash. You just know it happens after you tap the application’s button. A reasonable way to proceed would be to stop the application after the button is tapped and step through the code until you get a clue as to the problem.
Open ViewController.swift. To stop an application at a specified location in the code, you set a
breakpoint. The simplest way to set a breakpoint is to click
on the gutter to the left of the editor pane next to the line where you want execution to stop.
Try it: Click to the left of the line @IBAction func buttonTapped(_ sender: UIButton) {
.
A blue marker indicating the new breakpoint will appear (Figure 9.3).
After a breakpoint is set, you can toggle it by clicking on the blue marker directly. If you click on the marker once, it will become disabled, indicated by a paler shade of blue (Figure 9.4).
Another click re-enables the breakpoint. You can also enable, disable, delete, or edit a breakpoint by Control-clicking on the marker. A contextual menu will appear, as shown in Figure 9.5.
Selecting Reveal in Breakpoint Navigator opens the breakpoint navigator in Xcode’s left pane with a list of all the breakpoints in your application (Figure 9.6). You can also open the breakpoint navigator by clicking its icon in the navigator selector.
Make sure your breakpoint on the buttonTapped(_:) method is set and active after all the clicking you did in the previous section. Run the application and tap on the button.
Your application hits the breakpoint and stops executing, and Xcode takes you to the line of code that would be executed next, which is highlighted in green. It also opens some new information areas (Figure 9.7).
You are familiar with the console and have already seen the debug navigator. The new areas here are the variables view and the debug bar, which together with the console make up the debug area. (If you cannot see the variables view, click on the icon on the bottom-right corner of the debug area.)
The variables view can help you discover the values of variables and constants within the scope of the breakpoint. However, trying to find a particular value can require a fair amount of digging.
Initially, all you will see listed in the variables view are the sender and self arguments passed to the buttonTapped(_:) method. Click on the disclosure triangle for sender, and you will see that it contains a UIKit.UIControl property. Within it there is a _targetActions array that contains the button’s attached target-action pairs.
Open the _targetActions array, open the first item ([0]), and then select the _target property. Tap the space bar while _target is selected, and a Quick Look window will open, showing a preview of the variable (which is an instance of ViewController). The Quick Look is shown in Figure 9.8.
In the same section as the _target, you will see the _selector. Next to it, you will see (SEL) "buttonTapped:". The (SEL) indicates that this is a selector, and "buttonTapped:" is the name of the selector.
In this contrived example, it does not help you much to dig to find the _target and the _action; however, once you start working with larger, more complex applications, it can be especially useful to use the variables view. You do need to know what you are looking for, such as the _target and the _action – but finding the value that you are interested in can be very helpful in tracking down bugs.
Now it is time to start advancing through the code. You can do this using the buttons on the debug bar, shown in Figure 9.9.
The important buttons in the debug bar are:
Continue program execution () – resumes normal execution of the program
Step over () – executes a single line of code without entering any function or method call
Step into () – executes the next line of code, including entering a function or method call
Step out () – continues execution until the current function or method is exited
Click the button until
you highlight the badMethod()
line (do not execute this line). Note that you do not step
into the print() method – because it is an Apple-written method, you know there will be no problems there.
With badMethod()
highlighted, click the
button to step into the badMethod() method, and
continue stepping through the code with until the application crashes. It will take you quite a few clicks,
and it will look like you are going through the same lines of code over and over – in fact, you are, as the code loops over the ranges.
As you step through the code, you can pause to mouseover i and array.remove
to see their values update (Figure 9.10).
Once the application crashes, you have confirmation that the crash occurs within the
badMethod() method. With this knowledge you can now delete or disable the
breakpoint at the func buttonTapped(_ sender: UIButton)
line.
To delete a breakpoint, Control-click it and select Delete Breakpoint. You can also delete a breakpoint by dragging the blue marker out of the gutter, as shown in Figure 9.11.
Occasionally, you want to be notified when a line of code is triggered, but you do not need any additional information or for the application to pause when it hits that line. To accomplish this, you can add a sound to a breakpoint and have it automatically continue execution after being triggered.
Add a new breakpoint at the
array.insert(i, at: i)
line of the badMethod() method. Then
Control-click on the marker and select Edit Breakpoint.... Click on the
Add Action button and select Sound from the pop-up menu. Finally, check the
box to Automatically continue after evaluating actions (Figure 9.12).
You have configured the breakpoint to make an alert sound instead of stopping execution every time it is encountered. Run the application again and tap the button. You should hear a sequence of sounds, and then the application will crash.
It seems the application is safely completing the for
loop, but you need to be sure.
Find and Control-click your breakpoint marker again, selecting Edit Breakpoint...
as before. In the editor pop-up, click the + to the right of the sound action to add a new
action.
From the pop-up, select Log Message. In the Text field, enter
Pass number %H
(%H
is the breakpoint hit count, a reference to the number
of times the breakpoint has been encountered). Finally, make sure the Log message to console radio
button is selected (Figure 9.13).
Run the application again and tap the button. You will hear the sequence of sounds again, and the application will crash as before. But this time, if you watch the console (or scroll up after the application crashes), you will see that the breakpoint was encountered 10 times. This confirms that your code is completing the loop safely.
Delete your current breakpoint and add a new one on the line array.remove(at: 0)
. Edit the
breakpoint to log the pass number and continue automatically, as before (Figure 9.14).
Run the application and tap the button. When it crashes, scroll up in the console and you will see that the second breakpoint was encountered 11 times. That is one time too many, and you have your smoking gun. It also explains the NSRangeException logged on the console as the application crashes. Carefully read the crash log on the console again and make as much sense of it as possible.
Before fixing the problem, take the time to explore a couple more debugging strategies. First, disable or delete any remaining breakpoints in the application.
In these simple examples, you have known just where to look to find the bug in your code, but in real-world development you will often have no idea where in your application a bug is hiding. It would be nice if you could tell which line of code is causing an uncaught exception resulting in a crash.
It would be nice – and with an exception breakpoint, you can do just that. Open the breakpoint navigator and click on the + in the lower-left corner of the window. From the contextual menu, select Exception Breakpoint.... A new exception breakpoint is created and a pop-up appears. Make sure it catches all exceptions on throw, as shown in Figure 9.15.
Run the application and tap the button once again. The application automatically stops and Xcode takes you to the line that directly causes the exception to be raised. Note, however, that there is no console log. That is because the application has not crashed yet. To see the crash and read the cause, click on the button on the debug bar until you see the crash.
This strategy is the one to begin with as you tackle a new bug. In fact, many programmers always keep an exception breakpoint active while developing. Why did we make you wait so long to use it? Because if you had started with an exception breakpoint, you would not have needed to learn about the other debugging strategies, and they have their uses, too. Feel free to remove this breakpoint if you would like; you will not need it again.
You are going to try one final technique: the symbolic breakpoint. These are breakpoints specified not by line number, but by the name of a function or method, referred to as a symbol. Symbolic breakpoints are triggered when the symbol is called – whether the symbol is in your code or in a framework for which you have no code.
Add a new symbolic breakpoint in the breakpoint navigator by clicking the + button on the lower-left corner and, from the contextual menu, selecting Symbolic Breakpoint.... In the pop-up, specify “badMethod” as the symbol, as shown in Figure 9.16. This means that every time badMethod() is called, the application will stop.
Run the application to test the breakpoint. The application should stop at badMethod() after you tap the Tap me! button.
In a real-world app, it is rare that you would use a symbolic breakpoint on a method that you created; you would likely add a normal breakpoint like the ones you saw earlier in this chapter. Symbolic breakpoints are most useful to stop on a method that you did not write, such as a method in one of Apple’s frameworks. For example, you might want to know whenever the method loadView() is triggered for any view controller within the application.
Finally, fix the bug.
func badMethod() { let array = NSMutableArray() for i in 0..<10 { array.insert(i, at: i) }// Go one step too far emptying the array (notice the range change):for _ in 0...10 {for _ in 0..<10 { array.remove(at: 0) } }
A great feature of Xcode’s LLDB debugger is that it has a command-line interface. The console area is not only
used to read messages, but also can be used to type LLDB commands. The debugger command-line interface
is active whenever you see the blue (lldb)
prompt on the console.
Make sure your
symbolic breakpoint on badMethod() is still active, run the application, and tap the button
to break at that point. Look at the console and you will see the (lldb)
prompt (Figure 9.17). Click beside the
prompt, and you can type commands.
One of the most useful LLDB commands is print-object
, abbreviated po
.
This command prints a nice description of any instance. Try it out by typing on the console.
(lldb) po self <Buggy.ViewController: 0x7fae9852bf20>
The response to the command is that self is an instance of
ViewController. Now advance one line of code with the command step
; this will initialize the
array constant reference. Print the reference’s value with po
.
(lldb) step (lldb) po array 0 elements
The response 0 elements
is not very useful, as it does not give you a lot of
information. The print
command, abbreviated p
, can be more verbose. Try it.
(lldb) p array (NSMutableArray) $R3 = 0x00007fae98517c00 "0 values" {}
Frequently, using the console with print
or print-object
to examine variables
is much more convenient than Xcode’s variables view pane.
Another useful LLDB command is expression
, abbreviated expr
. This
command allows you to enter Swift code to modify variables. For example, add some data
to the array, look at the contents, and continue execution.
(lldb) expr array.insert(1, at: 0) (lldb) p array (NSMutableArray) $R5 = 0x00007fae98517c00 "1 value" { [0] = 0xb000000000000013 Int64(1) } (lldb) po array ▿ 1 element - [0] : 1 (lldb) continue
Perhaps more surprisingly, you can also change the UI with LLDB expressions. Try changing the button’s tintColor to red.
(lldb) expr self.view.tintColor = UIColor.red (lldb) continue
There are many LLDB commands. To learn more, enter the
help
command at the (lldb)
prompt.