Xcode offers developers a mature development environment with a powerful debugger. Under the hood, Xcode's debugging tools take advantage of LLDB, the debugger of the LLVM project. It isn't necessary to have a deep understanding of LLDB or LLVM to make use of Xcode's debugging tools, but it certainly doesn't hurt to become familiar with LLDB and LLVM.
In this episode, we explore the debugging tools Xcode ships with by debugging Cloudy, the application we build in Mastering MVVM With Swift. Download the application if you want to follow along.
You can only debug an application if you run it in the simulator or on a device. When you run an application, an instance of the application is instantiated. We commonly refer to the instance as a process and it's the process we debug during development. The terms application and process are very often used interchangeably. That's fine as long as you understand the difference.
When you run an application in Xcode, the debugger is automatically started and attached to the process of the application. Click the Run button in the top left or press Command + R. From the moment the application is up and running, we can start inspecting the process and, if necessary, debug it.
The first change you'll notice when the application is running and Xcode's debugger is attached to the application's process, is the Debug Area at the bottom. What you see depends on the configuration of your Xcode installation, but you should see, at the very least, the debug bar appear at the bottom of the window.
If there's nothing to report or debug, this change is subtle. We take a closer look at the debug bar in the episode on debugging with breakpoints.
We can show or hide the variables view and the console by clicking the middle view control at the top right or by clicking the leftmost button of the debug bar.
The variables view is empty at the moment because the application isn't paused. This becomes clear later in this episode. The console shows us the output generated by Xcode and the running application. The coordinates that are printed to the console, for example, come from a print statement in the project.
Print or log statements are incredibly useful for debugging. It's an easy and a straightforward form of debugging that I use all the time.
Pausing the Application
The second button of the debug bar allows us to enable or disable the breakpoints defined in the project or workspace. We revisit breakpoints later in this series.
The pause button pauses or suspends the application's process. This isn't something you do very often. It's more useful to pause the application by setting a breakpoint at a specific location or when a certain condition is met. Notice that the pause button turns into a play button when the application is paused. This allows us to resume the application's process.
You can ignore the three buttons on the right of the pause button. We revisit those when we discuss breakpoints in more detail.
Debugging a View Hierarchy
The next button is more interesting. If we click this button, the application is paused and Xcode shows us an exploded version of the application's user interface. This is the view debugger that ships with Xcode.
It shows us the views of the application as well as the view controllers responsible for managing the views. View debugging is very useful for debugging issues related to the user interface. We explore this aspect of debugging in a later episode in more detail.
When you're debugging a view hierarchy, the application is paused and a snapshot of its current state is used for debugging. We can exit the view debugger by clicking the resume button in the debug bar.
A few years ago, Apple added another type of debugging to Xcode, debugging the memory graph of the application's process. This can be very useful to detect memory issues, such as retain cycles or memory leaks.
If you click this button, the application is paused and the debugger captures the memory graph of the process. We don't see anything interesting in this example. In a later episode, I show you how to leverage this tool to detect retain cycles and other memory issues.
Simulating Location Changes
Developers usually spend their time writing code indoors behind a desk, which makes debugging issues related to location services challenging. It becomes a little bit easier thanks to Xcode's ability to simulate location changes.
This button allows you to simulate the location for the current debug session. You can also provide a GPX file to simulate several location changes. That can be useful to simulate, for example, a user going for a walk or a run. The simulator also has support for simulating location changes.
Adding a Breakpoint
We take a closer look at breakpoints in a later episode of this series, but I'd like to give you a quick peek at the Breakpoint Navigator. You can add a breakpoint by clicking the gutter on the left in any source file. The breakpoint shows up as a blue arrow.
If we run the application again, Xcode suspends the application the moment it hits the breakpoint. Notice that the variables view is now populated with information. We zoom in on this aspect later in this series.
The console allows us to interact with LLDB. The
po command stands for print object and it prints the object to the console.
A project or workspace can have dozens of breakpoints. You can find an overview in Xcode's Breakpoint Navigator on the left. This makes it easy to enable and disable breakpoints, or to jump to the location of a specific breakpoint.
The source editor also shows you information when a breakpoint is hit. If you hover over a local variable, for example, you can inspect its value. While this is interesting, the variables view is more convenient and that's what you use most often.
The application is still suspended because we hit a breakpoint. We can discover more information about the application's current state by inspecting the contents of the Debug Navigator on the right.
You can find the debug gauges at the top and the process view at the bottom. The debug gauges show you which resources the application is using. This is more useful if the application is running.
The process view is more interesting at the moment. It shows us the backtrace of the application organized by thread. The application uses several threads to do its work and it's currently suspended on thread 1, the main thread.
Every line is a stack frame. The current stack frame is highlighted and corresponds with the location of the breakpoint we set. Don't worry if you're not familiar with stack frames. We discuss stack frames in more detail later.
There's one last thing I want to show you in this episode. At the start of this episode, I mentioned that the debugger is automatically attached to the process of the running application. That's the default option. It's also possible to attach the debugger to an existing process.
You may be wondering why that's useful. This can be interesting if you spot a problem in your application while your device isn't attached to your machine or launched by Xcode. Many bugs are spotted when you're not in front of your computer. Right?
Let me show you this feature with a build of Cloudy running in the simulator. We launch the application and choose Attach to Process from Xcode's Debug menu.
Xcode helps us by showing us a list of likely targets at the top. When we choose the process we're interested in, the Stop button is enabled indicating that the debugger is successfully attached to the process.
But let's take it one step further. Xcode 9 added support for wireless debugging. This means that you can build and run your application on a physical device without the need for a wire. That's great and it means that developers can also attach the debugger to an application that's running on a device. Keep in mind that you can only debug your own applications.
The application I'm attaching the debugger to is an instance of Cloudy that runs on a device that isn't physically connected to my development machine. It's a very convenient addition if you ask me. It makes debugging random bugs, bugs that are hard to reproduce, a little bit easier.
You may have noticed that Xcode's user interface changes as you run and debug an application. Xcode shows you what's most useful to you based on the current conditions and circumstances. You can customize this to some extent by fiddling with Xcode's preferences and behaviors. That's something we explore later.
We are slowly learning more about Xcode's debugging tools and you should now have an overview of the tools Xcode offers us to debug applications. In the next episode, we take a deep dive into debugging with breakpoints. Breakpoints are flexible and powerful. Debugging with breakpoints is a technique you'll find yourself using time and time again.