Breakpoints are indispensable for debugging problems in a software project. Debugging an app with breakpoints can seem complex at first, but it isn't difficult once you understand what is going on. While the underlying concept of debugging with breakpoints is simple, you can make it as complex as you want to fit your needs.
What Is a Breakpoint?
Many developers use breakpoints to debug problems, but a surprising number doesn't understand what they are and how they work under the hood. Let me start by answering the question what is a breakpoint?
A breakpoint gives you the ability to pause or interrupt an app's process based on a set of predefined conditions. The debugger breaks the app's process when it hits a breakpoint hence the name.
Breakpoints are set in the debugger. Developers that are unfamiliar with breakpoints wrongly assume that breakpoints need to be removed or disabled in release builds. That isn't correct, though. The breakpoints you set in the debugger are not part of the builds the compiler creates.
You can only debug an app with breakpoints if the app is attached to the debugger. A user who downloads your app from the App Store or TestFlight won't run into the breakpoints you set in development because the app isn't attached to the debugger. And, as I mentioned earlier, breakpoints are set in the debugger, they are not included in the builds you upload to App Store Connect.
Breakpoint Types
In the previous video, we added a breakpoint in LocationsViewModel.swift. The debugger automatically pauses or suspends the app's process when the breakpoint is hit.
The breakpoint we added is a file and line breakpoint. Xcode supports several types of breakpoints. Open the Breakpoint Navigator on the left and click the + button at the bottom left to see what other types of breakpoints we can add for debugging.
Swift Error Breakpoint
The Swift Error Breakpoint pauses execution when a Swift error is thrown. Error handling in Swift is in some ways similar to exception handling in Objective-C.
Exception Breakpoint
And that brings us to the Exception Breakpoint. This type of breakpoint is useful if you work with Objective-C and want to break the app's process when an exception is thrown. An exception breakpoint can be useful even if your app is entirely written in Swift. Remember that the Cocoa frameworks that power iOS, tvOS, macOS, and watchOS apps are mostly written in Objective-C.
Symbolic Breakpoint
A Symbolic Breakpoint allows you to pause the app's process when a particular symbol is hit. This type of breakpoint can be useful for debugging problems that involve code you don't have direct access to. A symbolic breakpoint suspends the app when a particular method or function is invoked.
Let me give an example to make this more clear. Symbolic breakpoints are helpful if you need to debug an issue that involves one of Apple's frameworks. Because you don't have access to the source code of Apple's frameworks, it isn't possible to add a file and line breakpoint.
Runtime Issue Breakpoint
Xcode uses sanitizers to detect runtime issues. A runtime issue is a problem the debugger detects while your app is running, for example, not updating the user interface on the main thread or unsafely updating variables from different threads. Runtime issues show up in the Issue Navigator while your app is running.
Runtime issues may lead to unexpected behavior and they can result in your app crashing. The problem is that such crashes are often difficult to debug. You can make debugging much less painful by enabling one of Xcode's sanitizers and add a runtime issue breakpoint. The debugger pauses the app's process when it runs into a runtime issue, making it easier to pinpoint the root cause.
Constraint Error Breakpoint
As the name suggests, a constraint error breakpoint is useful for debugging issues related to Auto Layout. This type of breakpoint pauses the app's process if Auto Layout throws an error.
Test Failure Breakpoint
Breakpoints can also be helpful to debug issues related to your test suite. Use the test failure breakpoint to pause the app's process if a test fails. While this may not sound interesting, later in this course, I show you a trick that takes advantage of this type of breakpoint.
Let's start with the most common type of breakpoint, the file and line breakpoint.
File and Line Breakpoints
As the name implies, a file and line breakpoint is associated with a specific location in the codebase. The breakpoint we added in the previous video is tied to line 56 of LocationsViewModel.swift. Let's run the app and see what happens.
When the debugger hits the file and line breakpoint we defined in LocationsViewModel.swift, it suspends the app's process. What you see and hear depends on the configuration of your Xcode installation. We revisit this topic in a later video.
What is most interesting is the content of the Debug Navigator on the left. Xcode shows us the backtrace of the stack frames. A backtrace or stack trace is a listing of the function calls the moment the app's process was suspended.
The topmost stack frame corresponds with the position of the file and line breakpoint we set in LocationsViewModel.swift. It points to the start()
method of the LocationsViewModel
class. The next stack frame is the getter of the computed body
property of the LocationsView
struct that triggered the start()
method and so on.
The Debug Navigator shows us that the start()
method was invoked on the main thread. We can also explore the backtraces of other threads.
Some backtraces can be very complex and a bit of help is welcome. You can filter the backtrace by entering a keyword in the text field at the bottom and you can toggle one of the filters on the right to ignore stack frames that are not useful to debug the issue.
Xcode automatically shows you what it thinks is most useful to you. Stack frames that don't contain debug symbols are shown by default for me, but those stack frames are often not helpful. You can hide stack frames that don't contain debug symbols by selecting the leftmost button. It can sometimes be useful to take a peek at the entire stack frame to see which aspects of your codebase or a framework are involved.
If you select the middle button, only crashed threads and threads with debug symbols are shown. It's a filter I rarely use. If you select the rightmost button, only running blocks are shown in the queues view.
Different Ways to View Process
And that brings us to a concept that is often skipped or misunderstood, threads and queues. You can view the app's process in a few different ways by clicking the button in the top right of the Debug Navigator. We covered the last two options earlier in this course, View UI Hierarchy and View Memory Graph Hierarchy.
By default, the Debug Navigator shows the app's process by thread. It lists the threads the app's process uses to do its work. That is the view we have been using so far. It also offer the option to show the app's process by queue. The Debug Navigator lists the queues the app's process uses. Each queue is a Grand Central Dispatch queue that is used to perform work.
The queues view is useful for debugging concurrent or parallel operations. I mostly use the threads view, but the queues view can be helpful to shed light on an issue that isn't obvious when you only focus on the threads view.
I bring this up to better understand the function of the rightmost button at the bottom of the Debug Navigator. If you select the button, which is the default, only running blocks are shown in the queues view. If we deselect it, the Debug Navigator becomes a bit noisy. Let's take a look at a global dispatch queue.
It displays the running block, if any, and completed blocks, if any. The running block is prefixed with a green symbol while completed blocks are prefixed with a gray symbol. Toggle the rightmost button a few times to better understand the difference. Even though the queues view is a bit noisier when completed blocks are shown, they can be useful to get a more complete view of the app's process.
What's Next?
In the next video, you learn how to step through your code using step over, step into, and step out. We use a breakpoint to suspend the app's process and use the debug bar to step through the code.