Using @DebugDescription in Xcode 16
Debugging can be tricky, especially with custom types. Clear and informative debug output is essential for understanding the behavior of your code.
That's where the CustomDebugStringConvertible
protocol and @DebugDescription
macro come in. In this article, we'll take a look at how to work with this protocol and how to use this new macro in Xcode 16 to make debugging even easier. š
Using CustomDebugStringConvertible
The CustomDebugStringConvertible
protocol allows you to customize the debug description of your custom types, providing more detailed and readable debug output.
When you conform to this protocol, you implement a computed property debugDescription
that returns a String
. This string is used when you print the object in a debug context, such as when using print()
or inspecting variables in Xcode's debug console:
struct Book: CustomDebugStringConvertible {
let title: String
let author: String
let pageCount: Int
var debugDescription: String {
// Ace the iOS Interview - Aryaman Sharda [330]
"\(title) - \(author) [\(pageCount)]"
}
}
This is especially helpful when dealing with complex custom types since a custom formatted output is often more useful than the default one.
Before implementing the CustomDebugStringConvertible
protocol, our output looks like this:
let book = Book(
title: "Ace the iOS Interview",
author: "Aryaman Sharda",
pageCount: 330
)
print(book)
Book(title: "Ace the iOS Interview", author: "Aryaman Sharda", pageCount: 330)
With this conformance in place, our debugger output now looks like this:
struct Book: CustomDebugStringConvertible {
let title: String
let author: String
let pageCount: Int
var debugDescription: String {
// Ace the iOS Interview - Aryaman Sharda [330]
"\(title) - \(author) [\(pageCount)]"
}
}
print(book)
Ace the iOS Interview - Aryaman Sharda [330]
(lldb) po book
āæ Ace the iOS Interview - Aryaman Sharda [330]
- title : "Ace the iOS Interview"
- author : "Aryaman Sharda"
- pageCount : 330
This is clearly a noticeable improvement, but there's still one small issue....
I'd much rather inspect my variables in Xcode's Variable Inspector instead of adding print
statements in my code or typing po book
to utilize our new custom debugging format.
What if we could change how our variables appear here directly? What if we could see the debugDescription
at the top-level without having to expand the book
variable? Fortunately, the @DebugDescription
macro allows us to do just that.
@DebugDescription
By simply annotating our type with the new DebugDescription
macro, we can now use our debugDescription
in Xcode's Variable Inspector and crash logs:
@DebugDescription
struct Book: CustomDebugStringConvertible {
let title: String
let author: String
let pageCount: Int
var debugDescription: String {
// Ace the iOS Interview - Aryaman Sharda [330]
"\(title) - \(author) [\(pageCount)]"
}
}
It's definitely a nice quality of life improvement, but how does it all work? And what if I can't use Xcode 16 yet?
How It Works
In order to display a custom debug description, LLDB - the debugger used in Xcode - needs to evaluate the code that generates this description. In other words, it needs to actually execute the debugDescription
computed property.
This process is called "expression evaluation". LLDB usually performs this evaluation only when you explicitly ask for it, commonly using the po
(print object) command. Outside of these explicit commands, LLDB avoids the overhead of expression evaluation which can often be complex and slow (or just simply unavailable).
Luckily, we can avoid the need for expression evaluation altogether by defining an LLDB Type Summary. This tells LLDB how to display your type without needing to run any extra code.
For example, in the debugger, we can manually add a Type Summary for Range
with the following command:
type summary add --summary-string "${var.lowerBound}..<${var.upperBound}" "Range<MyModule.MyString.Index>"
Since the format is pre-defined, LLDB doesn't need to evaluate any expressions or execute any code to display the summary. It simply replaces the placeholders with the actual values of the properties. This means that LLDB will always be able to display the debug output quickly and reliably, regardless of the state of the program or the availability of expression evaluation.
So, simply put, when we annotate our type with DebugDescription
, it's just creating a LLDB Type Summary for it under the hood and then, at compile time, bundling these summaries with the binary.
If you're interested in reading more about the proposal and evolution of this macro, check out the discussion in the Swift forums:
Macro Alternatives
For those who can't use Xcode 16 yet, an alternative is to use LLDB Type Summaries and configure them in your .lldbinit
file. The .lldbinit
file is a configuration file that Xcode automatically loads when you start a debugging session. It allows you to define custom scripts and commands to control how types are displayed in the debugger.
By writing Type Summaries in the .lldbinit
file, you can customize the debug output for your most important models, providing meaningful and formatted information during debugging - even without the new macro.
You can also share the .lldbinit
configuration with your team which would allow everyone to benefit from the same enhanced debugging experience. Then, whenever your team is in a position to use Xcode 16, you can migrate to using the new macro.
You can find instructions on setting up the .lldbinit
file here:
If you're interested in more articles about iOS Development & Swift, check out my YouTube channel or follow me on Twitter.
And, if you're an indie iOS developer, make sure to check out my newsletter! Each issue features a new indie developer, so feel free to submit your iOS apps.