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)]"
    }
}
The debugDescription is available here so I no longer need to expand book to see the relevant variables or use print(book) or po book.

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:

Pitch: Debug Description macro
In addition to the macro, can we please also expose a straightforward way to simply manually specify the summary string that we want lldb to use, alongside each type? @_lldbFormatter(”${var.id}: ${var.name}”) struct Student: CustomStringConvertible { var name: String var id: Int var description: String { // potentially more complicated code than what `@DebugDescription` // would be able to handle } } I understand macros are very awesome, but it isn’t always appropriate to rely…

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:

Debugging Tips
What are your favorite Swift debugging tips?

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.

Ace The iOS Interview
The best investment for landing your dream iOS jobHey there! My name is Aryaman Sharda and I started making iOS apps way back in 2015. Since then, I’ve worked for a variety of companies like Porsche, Turo, and Scoop Technologies just to name a few. Over the years, I’ve mentored junior engineers, bui…
Indie Watch
Indie Watch is an exclusive weekly hand-curated newsletter showcasing the best iOS, macOS, watchOS, and tvOS apps from developers worldwide.
Universal Link & Apple App Site Association Testing Tool
Easily verify and test Universal GetUniversal.link is a free tool for verifying and testing Apple App Site Association (AASA) files. Ensure your Universal Links are configured correctly with easy link creation, real-time testing, and team collaboration features. Save the website as a bookmark for quick access on devices and simulators. Simplify your AASA file troubleshooting today!