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 or LLVM.

The LLDB Website

The LLVM Website

Debugging Thunderstorm

In this video, we explore the debugging tools Xcode ships with by debugging Thunderstorm, the app we build in Mastering MVVM with SwiftUI. Download the source files if you want to follow along.

You can only debug an app if you run it in the simulator or on a device. When you run an app, an instance of the app is instantiated. We commonly refer to the instance as a process and it is the process we debug during development. The terms app and process are often used interchangeably. That is fine as long as you understand the difference.

When you run an app in Xcode, the debugger is automatically started and attached to the process of the app. Click the Run button at the top or press Command + R. From the moment the app is up and running, we can start inspecting the process and, if necessary, debug it.

Debug Area

The first change you notice when the app is running and Xcode's debugger is attached to the app'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.

Debug Area With Debug Bar

If there is nothing to report or debug, then this change is subtle. We take a closer look at the debug bar in the video on debugging with breakpoints.

Debug Area With Debug Bar

We can show or hide the variables view and the console by clicking the rightmost button of the debug bar.

Debug Area With Debug Bar

The variables view is empty at the moment because the app's process isn't paused. This becomes clear later in this video. The console displays the output generated by Xcode and the running app. The message "Start Locations View Model" that is printed to the console, for example, comes from a print statement in LocationsViewModel.swift.

Reveal Variables View and Console

func start() {
    print("Start Locations View Model")

    ...
}

Print and log statements are incredibly useful for debugging. They are an easy and straightforward form of debugging that I use all the time.

Pausing the App's Process

The first 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 course.

The pause button pauses or suspends the app's process. That isn't something you do very often. It is more useful to pause the app 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 app is paused. The play button resumes the app'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 that button, the app's process is paused and Xcode shows us an exploded version of the app's user interface. That is the view debugger that ships with Xcode.

Debugging User Interface Issues With Xcode's View Debugger

It shows us the views of the app as well as the view controllers responsible for managing the views. View debugging is useful for debugging issues related to the user interface. We explore that aspect of debugging in a later video.

When you are debugging a view hierarchy, the app is paused and a snapshot of its current state is used for debugging. We can exit the view debugger by clicking the continue button in the debug bar.

Debugging Memory

A few years ago, Apple added another type of debugging to Xcode, debugging the memory graph of the app's process. That can be useful to detect memory issues, such as retain cycles or memory leaks.

Debugging Memory Issues With Xcode's View Debugger

If you click the Debug Memory Graph button, the app's process is paused and the debugger captures the memory graph of the app's process. We don't see anything interesting in this example. In a later video, I show you how to leverage the memory graph debugger to detect retain cycles and other memory issues.

Overriding the Environment

Previews and view modifiers make it trivial to preview SwiftUI views in various environment configurations, but it is sometimes useful to change the environment configuration at runtime. The Environment Overrides button gives you access to a small selection of environment variables, including appearance, Dynamic Type, and several accessibility settings.

Overriding the Environment

The Environment Overrides menu is convenient to debug issues or test a build before releasing it.

Simulating Location Changes

Developers usually spend their time writing code indoors behind a desk, which makes debugging issues related to location services challenging. That task becomes easier thanks to Xcode's ability to simulate location changes.

Simulating Location Changes

The Simulate Location button allows us to simulate the location for the current debug session. You can also provide a GPX (GPS Exchange Format) file to simulate several location changes. That can be useful to simulate, for example, a user going for a run or a walk. The simulator also has support for simulating location changes.

Adding a Breakpoint

We take a closer look at breakpoints in a later video of this course, but I would like to give you a quick peek at the Breakpoint Navigator. You can add a breakpoint to any source file by clicking the source editor's gutter on the left. The breakpoint shows up as a blue arrow.

Adding a Breakpoint

If we run the app again, Xcode suspends the app's process 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 course.

Debugging With Breakpoints

We can interact with LLDB through Xcode's console. The po command stands for print object. It prints the object to the console.

Interacting With LLDB

A project or workspace can have dozens of breakpoints. You can find an overview in Xcode's Breakpoint Navigator on the left. The Breakpoint Navigator makes it easy to manage breakpoints or to jump to the location of a breakpoint.

Exploring the Breakpoint Navigator

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 is what you use most often.

Debug Navigator

The app is still suspended because we hit a breakpoint. We can discover more information about the app's current state by inspecting the contents of the Debug Navigator on the left.

Exploring the Debug Navigator

You can find the debug gauges at the top and the process view at the bottom. The debug gauges show you which resources the app's process is using. That information is more useful if the app is running, not when the app is paused.

Debug Gauges

The process view is more interesting at the moment. It shows us the backtrace of the app's process organized by thread. The app's process uses several threads to do its work and it is currently suspended on thread 1, the main thread.

Backtrace

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 are unfamiliar with stack frames. We discuss stack frames in more detail later in this course.

Debugging Options

There is one last thing I want to show you in this video. At the start of this video, I mentioned that the debugger is automatically attached to the process of the running app. That is the default option. It is also possible to attach the debugger to an existing process.

You may be wondering why that is useful. That can be interesting if you spot a problem in your app while your device isn't attached to your machine or launched by Xcode. Many bugs are spotted when you are not in front of your computer. Right?

Let me show you this feature with a build of Thunderstorm running in the simulator. We launch the app and choose Attach to Process from Xcode's Debug menu.

Attaching Xcode to a Running Application

Xcode helps us by showing us a list of likely targets at the top. When we select the process we are interested in, the Stop button is enabled indicating that the debugger is successfully attached to the process.

Let's take it one step further. A few years ago, Xcode added support for wireless debugging. That means you can build and run your app on a physical device without the need for a wire. That is convenient and it means developers can also attach the debugger to a process that is running on a device. Keep in mind that you can only debug your own apps.

The app I am attaching the debugger to is an instance of Thunderstorm that runs on a device that isn't physically connected to my development machine. It makes debugging random bugs, bugs that are hard to reproduce, a little bit easier.

What's Next?

You may have noticed that Xcode's user interface changes as you run and debug an app. Xcode shows you what is 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 is something we explore later in this course.

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 apps. In the next video, we take a deep dive into debugging with breakpoints. Breakpoints are flexible and powerful. Debugging with breakpoints is a technique you find yourself using time and time again.