Taking Control with Breakpoints

At this point, you might be looking at this and thinking, “There is something wrong with this. My Spidey sense detects Code Smell.” Trust your Spidey sense. This solution is fraught with potential problems for your project.

Even if this approach has uncovered the bug, we’ve sprinkled nasty print statements all over our project. Ideally, we should never include a print command in code that we send to Apple. Although they may help for debugging, print statements do nothing for the end user, and are thus inefficient and slow our app for absolutely no reason. Additionally, any code we add to our project opens up the possibility of breaking something. What’s the point of using something that might break our code in order to figure out how to fix it?

Wouldn’t it be great if we could still print all our commands to the console without having to sift through all our code looking for those sneaky print statements?

Breakpointing Bad

The answer to our conundrum is to use breakpoints. You may have inadvertently already created a breakpoint when you clicked an error icon in the left gutter to try to see what it said. Now we are going to create breakpoints on purpose.

Breakpoints are a feature in the Xcode development environment that lets us freeze our app at a specific point and figure out what our code is doing. They are like a photograph of all the functions that are happening, what threads are running, and what all our variables are set to at a given moment in time. Understanding breakpoints is the key to many of the debugging techniques available to us in Xcode.

Breakpoints are part of the Low-Level Debugger. LLDB is the debugger for Xcode. Many of its functionalities have been built into the user interface, such as the ability to create and edit breakpoints, as we’ll see shortly. It also has many other commands that are not included in the user interface and need to be entered via the Xcode console. By learning these, we can become efficient debuggers…plus, we can do things that look like magic and we can impress our friends and family.

images/debugging/gutter-breakpoint.png

We have already seen the easiest and most common way people create breakpoints in Xcode. Click in the gutter to the left of our code to create a breakpoint on any line. In PodcastEpisodeParser.swift, create a breakpoint on the switch elementName that is the first executable line of parser(didStartElement:qualifiedName:attributes:).

Right now when we create a breakpoint, it is kind of limited. It will just signal the code to pause on this line. That’s helpful enough, as it will let us know the app got that far, which is what we were tempted to use prints for. Fortunately for us, breakpoints can do so much more than that.

images/debugging/right-click-breakpoint.png

Right-click (or Control-click) on the breakpoint to reveal the breakpoint menu, as seen in the figure. As you will see, one of our options is Edit Breakpoint. Let’s go ahead and select that and see what we can do with it.

Take a look at the default options for editing breakpoints (as seen in the next figure). Notice that we have the following options:

  • Add a condition for whether or not to stop on the breakpoint.
  • Ignore the breakpoint a variable number of times.
  • Add an action to perform when the breakpoint is hit.
  • Determine if we want the program to pause or keep running after the program hits the breakpoint.
images/debugging/edit-breakpoint.png

The ability to ignore a breakpoint is particularly useful if we are dealing with a large collection of items. If we were analyzing a collection of ten million keys and values but we only wanted to know what the forty-second value was, we could tell the compiler to ignore the first forty-one values and analyze the one we want to make sure it is “Life, the Universe, and Everything.”

Breakpoint Logging

Rather than burdening our code with lots of print statements, breakpoints offer something easier to remove when we have finished debugging our code. Click the Action popup button. Notice that one of our options is Log Message, as seen in the following figure.

images/debugging/breakpoint-log-message.png

Log Message lets you do exactly that: log a message to Xcode’s debug console. Since we are attaching this behavior to a breakpoint, it is easier to go back later and filter out all of our debugging tools. We can surround expressions with the at (@) character to evaluate them as strings, so logging didStartElement: @elementName@ does the same thing as print("didStartElement: (elementName)").

Better yet, instead of a lot of messy code, this approach leaves us with nice, neat breakpoints, like those in the following diagram. In fact, it gets better: breakpoints are saved only in the local user’s Xcode configuration. So, if we send this project to our colleagues, there will be nothing for them to clean up. And when we want to clean up our own breakpoints, it’s easier to find them with the Breakpoint Navigator (7) and delete them than to search the code for every print statement.

images/debugging/logging-breakpoints.png

The Debugging User Interface

So far, what we’ve accomplished is pretty much what we got from using a bunch of prints: we can tell how far our code got before something went wrong. But we still haven’t narrowed down just where the problem is. Are we seriously going to have to put breakpoints all over it and edit each to add a unique log message?

At this level of debugging detail, we can do better. Go ahead and run the app. The usual parsing routine will call parser(didStartElement:qualifiedName:attributes:) and hit our breakpoint. When this happens, the Mac automatically switches the foreground application from the iOS Simulator to Xcode. The editor shows the app’s source code, and the line we are stopped in is highlighted in green. By default, stopping on a breakpoint also causes two debugging-related panes (shown in the following figure) to appear automatically.

images/debugging/debug-navigator-and-area.png
  • Debug Navigator (6): Shows the app’s usage of CPU time, memory, and other resources. When the app is stopped on a breakpoint, it also shows the state of active threads.

  • Debug Area (Y): As first mentioned in The Xcode Window, this space below the source view can show output from print. When stopped on a breakpoint, it also lets us look at variables and their values. In this figure, the Debug Navigator is on the left, and the Debug Area is on the right.

In the bottom right of the Debug Area are three important icons: a trashcan and two little boxes. The trashcan clears logged text from println or breakpoints that log messages. The two boxes show or hide the two panes of the Debug Area: the left shows a variables view, and the right shows the log messages.

images/debugging/debug-toolbar-buttons.png

At the top of the Debug Area, there’s a toolbar that includes a blue breakpoint icon, along with several other tiny buttons. The breakpoint button turns all breakpoints on or off. The next button to the right is a Play/Pause button, which allows us to continue after hitting a breakpoint.

The next three buttons are the step buttons. The first, Step Over, allows the app to continue to the next statement in the current method and then stops again. Further right, the down and up arrow icons represent Step In and Step Out, respectively. Step In means that we will enter the statement on this line of code and stop on its first line. Usually, this is only useful if the statement is in code we’ve written, as the debugger can’t show us the source for Apple’s framework code (or third parties’). Step Out does the opposite: it lets the app continue until the current method returns, and stops on the first line in the calling method after returning.

Stepping Through Breakpoints

We are going to use the step buttons to solve our problem. Use the Step Over button to advance one line at a time after the breakpoint. A green arrow in the source will show us where we are after each step.

With our app stopped on the switch elementName instruction, we can look ahead to our various cases: the first one handles name and itunes:duration and sets currentEpisodeText to an empty string, so that later calls to parser(foundCharacters:) can fill in the string. The others, for grabbing attributes like enclosure and itunes:image, don’t concern us right now.

So, click the Step Over button, which will execute the logic of the switch. It will stop execution on the first case statement. This is the one we expect it to enter eventually, but click Step Over again and it moves on to the next case. Click Step Over a few more times and it will eventually enter the default case.

Huh, guess that wasn’t the element we wanted. We can click Continue to let the app go back to running until it hits the breakpoint again, this time with a different elementName.

We can keep doing it this way, but after a few trips, we should start to wonder if we’ll ever go into the right case. There’s a pretty good way we can find out: just drag the breakpoint arrow down to the currentElementText = "" line inside that case. Now that we’ve moved the breakpoint, click Continue. The app will start running again, and finally stop on our breakpoint.

images/debugging/breakpoint-on-itunesduration.png

Progress! Or is it? Look down at the variables view on the left side of the debugging pane. It shows all the variables in scope, and elementName is itunes:duration. This is a case we don’t care about; we’re waiting for name (and, as a reminder, this is the bug: we deliberately changed that from the correct element name, title). Hit Continue, and we keep hitting itunes:duration, never name. So frustrating!

images/debugging/debug-navigator-move-up-stack-trace.png

Let’s say it occurs to us at this point that maybe it’s not hitting name, because there is no element called name. How can we investigate this hypothesis further? Over on the far left, the Debug Navigator shows the stack trace—the chain of method and function calls that led us to this point. Line 0, at the top, is the method we’re currently inside: parser(didStartElement:qualifiedName:attributes:). The next few lines are internal to iOS’s frameworks, mostly the XML parser. We can click on those lines, but it will just show us the assembly language for those parts of the code (which is kind of cool, but doesn’t help us now).

But further down the list of calls, we can find the PodcastFeedParser.init(parser:) call that kicked off this parsing. This is in our own code base, so clicking it takes us to that source file. Better yet, the variables view updates to show what variables are in scope at that point, like the dataMb, responseMb, and errorMb that we receive in the URLSession downloadTask closure.

Best of all, we have data, the unwrapped NSData object. We can use the debugger to look at this and figure out why it doesn’t have any name tags. Select data and right-click or Control-click it. A menu pops up with several options. Try “Print Description of ‘data’.”

images/debugging/breakpoint-print-description.png

Over in the console, we’ll see the results:

 Printing description of data:
 ▿ 102856 bytes
  - count : 102856
  ▿ pointer : 0x00007f8ea6851000
  - pointerValue : 140250655821824
 (lldb)

For some data types, printing the object like this would give us a useful description, but with Data, all we get is a byte counter and a memory pointer, which isn’t enough. For this one, we need to get our hands really dirty.

Click over in the console area, where it says (lldb). Did we mention this isn’t just a log viewer, but in fact an interactive console? Because it is. There are a bunch of commands we can enter directly. For example, type po data and press Enter to “print out” the description of data. This gives us the same result as the menu item did.

The cool thing is, we aren’t limited to just the objects as-is. We can actually write simple Swift expressions in the debug console itself. Remember back in Coding the New Class, when we turned the podcast Data into a String so we could log it? We can do the same thing right here. Enter the following command:

 po String(data: data, encoding: .utf8)

That’s the same make-a-String-from-a-Data syntax we used when logging it with print, except now we’re interacting directly with the debugger. Press Enter and, bam!, the contents of the podcast XML fill the console:

 ▿ Optional<String>
 - some : "<?xml version="1.0" encoding="UTF-8"?> <rss
 version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"
 xmlns:cc="http://web.resource.org/cc/"
 xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
 xmlns:media="http://search.yahoo.com/mrss/"
 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> <
 channel> <atom:link href="http://cocoaconf.libsyn.com/rss"
 rel="self" type="application/rss+xml"/> <title>CocoaConf
 Podcast</title> <pubDate>Wed, 16 Nov 2016 18:17:41
 +0000</pubDate> <lastBuildDate>Wed, 16 Nov 2016 18:19:07
 +0000</lastBuildDate> <generator>Libsyn WebEngine
 2.0</generator> <link>http://cocoaconf.com/podcast</link>
 <language>en-us</language> <copyright>

It’s not nicely formatted, and has and escape sequences for newlines and tabs respectively, but we could totally copy-and-paste this into a text editor to clean it up and make sense of it. And at that point, we could look at the episode tags and realize that they use the element title and not name. All thanks to the fact that we can actually write single lines of Swift syntax in the debugger itself. That’s pretty handy.

Of course, now that we’ve realized the error of our ways, go back to PodcastEpisodeParser.swift and change name back to title in the two places where we deliberately made it wrong. Also, with our bug fixed, we can delete the breakpoint, either by right- or Control-clicking it in the source editor, or by selecting it in the Breakpoint Navigator and pressing the Delete key.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset