iOS. Apple. Indies. Plus Things.

Swift's print(), debugPrint(), String(reflecting:), Mirror(reflecting:) and dump()

// Written by Jordan Morgan // Sep 26th, 2022 // Read it in about 9 minutes // RE: Swift

It’s truly incredible to think of all the advancements Cupertino & Friends™️ have brought forth in the name of debugging over the years. Need to explode an app’s hierarchy and pick it apart on the fly? No problem1:

Xcode's view debugging.

Or, maybe you’ve got to diagnose some slow code, debug CoreML, optimize app launch times or figure out the source of a hitch? Bam, Instruments:

Instrument's template page.

Heck - even retain counts have a hard time hiding out if you know how to fire up good ol’ mem graph:

Xcode's memory graph debugging tool.

And yet - in light of those amazing tools, I often find myself doing something like this to diagnose layout issues in my code:

view.backgroundColor = .purple

There’s nothing quite like having a giant purple view look at you right in the face to figure out the true size of a view. And, the code version of that?

The humble print(). While it’s used often across the Apple ecosystem, the ethnography of print debugging expands far beyond it. As such, today we’ll look at some other alternatives you might be unaware of:

Swift.debugPrint(), String(reflecting:), Mirror(reflecting:) and dump().

Size Up

At first blush, in the case of debugPrint() there’s not really anything eye catching about its signature. In fact, its arguments mimic that of print() to the letter:

// Ol' faithful
func print(
    _ items: Any...,
    separator: String = " ",
    terminator: String = "\n"

// And its close cousin
func debugPrint(
    _ items: Any...,
    separator: String = " ",
    terminator: String = "\n"

Going further, if you were to test them out in LLDB, depending on what you’re printing - you may not find any difference at all:

let numbers: [Int] = [0,1,2]

// Resulting in...
[0, 1, 2]
[0, 1, 2]

Even Apple proclaims that print() “Writes the textual representations of the given items…” while debugPrint() “Writes the textual representations of the given items most suitable for debugging” (emphasis my own). Hmmm, what exactly is suitable for debugging then?

Moving on, if you check out dump - it immediately gives off a different vibe:

@discardableResult func dump<T>(
    _ value: T,
    name: String? = nil,
    indent: Int = 0,
    maxDepth: Int = .max,
    maxItems: Int = .max
) -> T

The same is true, to a lesser extent, for String(reflecting:) and Mirror(reflecting:):

init<Subject>(reflecting subject: Subject)

init(reflecting subject: Any)

Okay…so…what’s suitable for debugging? What gives? And why does debugPrint() even exist if its so similar to print()? What about the other three options? To understand that, it helps to know about reflection, and how Swift’s Mirror struct works.

Mirror, Mirror on the Console

The concept of reflection in programming is primarily referring to the practice of inspecting types. Whether you’re in a statically typed language or a dynamic one, reflection can help solve all sorts of interesting problems. Not more than five or so years ago before Swift rose to its de facto standard for iOS development, many Objective-C developers used the concept of reflection to figure out stuff like what methods a class would implement, or even add new ones altogether2!

But, we’re talking Swift - not Objective-C. And that brings us back to Mirror, which helps Swift perform somewhat similar functions.

When you mirror a type in Swift, you can see things like its stored properties, its enumerations cases and more. Mirror extends into other parts of Swift, all building on the concept of reflecting types. There are functions specifically built for print debugging purposes from the get-go, complete with display customization options to tweak how output is rendered, making them well suited for print-based debugging. Putting it all together, reflection is quite simple and not really all that mysterious even though we often think of it that way.

With that said, for the rest of the post consider the following struct:

struct VideoGame: Identifiable {
    enum Genre: Int {
        case Adventure, Sports, Shooter
    var id = UUID()
    var genre: Genre
    var name: String
    var releaseDate: Date

Now, with a bit of background on Mirror and reflection in general, let’s see what all of these different printing methods produce.

The Great Print() Off

Let’s get to it and use all five of these methods on the following instance and see what we get:

let originalReleaseDate = Date(timeIntervalSince1970: 1195250594)
let massEffect = VideoGame(genre: .Adventure,
                           name: "Mass Effect",
                           releaseDate: originalReleaseDate)

Without further ado…

print(String(reflecting: massEffect))
print(Mirror(reflecting: massEffect))

What would you expect to happen here? Let’s see:

// Print
VideoGame(id: 56B4811E-0306-4974-A2D8-AA010311FC07, genre: SwiftUI_Playgrounds.VideoGame.Genre.Adventure, name: "Mass Effect", releaseDate: 2007-11-16 22:03:14 +0000)

// debugPrint
SwiftUI_Playgrounds.VideoGame(id: 56B4811E-0306-4974-A2D8-AA010311FC07, genre: SwiftUI_Playgrounds.VideoGame.Genre.Adventure, name: "Mass Effect", releaseDate: 2007-11-16 22:03:14 +0000)

// Reflection
SwiftUI_Playgrounds.VideoGame(id: 56B4811E-0306-4974-A2D8-AA010311FC07, genre: SwiftUI_Playgrounds.VideoGame.Genre.Adventure, name: "Mass Effect", releaseDate: 2007-11-16 22:03:14 +0000)

// Mirror
Mirror for VideoGame

// dump
   id: 56B4811E-0306-4974-A2D8-AA010311FC07
    - uuid: "56B4811E-0306-4974-A2D8-AA010311FC07"
  - genre: SwiftUI_Playgrounds.VideoGame.Genre.Adventure
  - name: "Mass Effect"
   releaseDate: 2007-11-16 22:03:14 +0000
    - timeIntervalSinceReferenceDate: 216943394.0

Some are pretty much the same, but there are some interesting differences. Let’s pick em’ apart.

For print()...
We’re getting pretty much what we expect. Under the hood, it’ll call String(item) and in most cases, it’s perfect for debugging code in LLDB. For our struct type, we see its properties and their values.

For debugPrint()...
Things look a little bit different. Its output is strikingly similar to the print(), but it has a fully qualified print out. In this case, I’m running code in a little project I dump in stuff for my blog posts - SwiftUI Playgrounds. In short, it has more accurate type information.

This is where a good ol’ fashioned print() versus debugPrint() really differentiate. The former gives you what you probably expect, while the formatter does that and provides more type information. That’s why these look just a teeny bit different:

let name = "Jordan"

print(name) --> Jordan 
debugPrint(name) --> "Jordan", representing the actual type

let rangeExample = 1...10

print(rangeExample) --> 1...10 // Just the value, not the type 
debugPrint(rangeExample) --> ClosedRange(1...10), includes the type and value

In short, I’ve found it helps to think of it this way. One gives me the values, and the other gives me the type and the values.

For String(reflecting:)
This one seems boring, but it’s actually quite interesting. I haven’t found in the official docs that the following statement is true, but in my experience, it seems true: debugPrint() is basically calling String(reflecting:) under the hood. In our case, you’ll notice that their output is identical.

Even if that’s the case, debugPrint() is more than just syntactical sugar and there are reasons to use it over simply using String(reflecting:). With all of the arguments it supports (remember, the same ones that print() does) you can tweak output much to your liking while getting the same information that String(reflecting:) brings to the table.

So, instead of having to write this:

print(String(reflecting:"Jordan"), String(reflecting:"Jansyn"), separator: " -- ")

// "Jordan" -- "Jansyn"

You can simlpy opt for debugPrint()

debugPrint("Jordan", "Jansyn", separator: " -- ")

// "Jordan" -- "Jansyn"

…and get the same result.

In short, you’ve got the niceties of text formatting along with type information. It does, however, bring about the question of “How exactly does String(reflecting:) figure out what to print?”

If you look at the docs for init(reflecting:) you’ll see a similar refrain we read earlier, emphasis again my own: “Creates a string with a detailed representation of the given value, suitable for debugging.”

There’s that phrase again, suitable for debugging. Seriously, what the __ is suitable for debugging!?

The answer lies in the type’s protocol conformance. This function follows a little flow chart of sorts to figure out what exactly is “suitable for debugging”, and it goes like this:

Step 1:
Does the type conform to CustomDebugStringConvertible? If it does, then you’ll get the type’s debugDescription. Swift supplies this for its own types, which is one reason you may not have reached for it yourself. This is what Swift considers “most useful for debugging.”

If you don’t like what Swift comes up with for a type, you’re free to tweak it to whatever you want. For example, for the VideoGame, maybe you just want the raw value of its enumeration and the name for some reason. Perhaps those are the only two interesting fields you’re trying to fix a bug centered around. Rolling your own CustomDebugStringConvertible would get you there:

extension VideoGame: CustomDebugStringConvertible {
    var debugDescription: String {
        return "Genre: \(self.genre.rawValue), name: \(name)"

// Prints out as Genre: 0, name: Mass Effect

Step 2:
However, if the type doesn’t conform to that - then String(reflecting:) dips down into CustomStringConvertible, which you’ve likely used in your Swift adventures. It’s particularly valuable for enum types, take our Genre enum from earlier, whose raw value is an Int:

enum Genre: Int {
    case Adventure, Sports, Shooter

print(VideoGame.Genre.Adventure) --> "VideoGame.Genre.Adventure"

// Add CustomDebugStringConvertible conformance
enum Genre: Int, CustomStringConvertible {
    var description: String {
        switch self {
        case .Adventure: return "Adventure"
        case .Sports: return "Sports"
        case .Shooter: return "Shooter"
    case Adventure, Sports, Shooter

print(VideoGame.Genre.Adventure) --> "Adventure"

So, we can start to see why something may be more “suitable for debugging” now. However, there is still one more stop that String(reflecting:) will take if neither of those protocols are used - and that stop is to…

Step 3:
TextOutputStreamable. This little guy is suited towards text-streaming ops, unsurprisingly, and as such - String, Character, and Unicode.Scalar all conform to it out of the box.

Bonus Step:
However, if String(reflecting:) encounters none of those, then you get a grab-bag result of who knows what. An “unspecified result” determined by the Swift standard library gets returned.

For Mirror(reflecting:)
This one is quite easy to understand, but can be incredibly useful. In short, you’re getting an instance’s type information. That’s why we simply got “Hey, this is a VideoGame” from it:

Mirror for VideoGame

If you aren’t sure of instance’s type - simply put, reach for Mirror(reflecting:).

For dump()
Finally, we’ve got dump() - one of the more interesting options. It’s similar to the mirror API, and in fact - it uses mirroring to produce whatever you pass in for value to create a standard textual representation of it. For a refresher, this is what it produced:

   id: 56B4811E-0306-4974-A2D8-AA010311FC07
    - uuid: "56B4811E-0306-4974-A2D8-AA010311FC07"
  - genre: SwiftUI_Playgrounds.VideoGame.Genre.Adventure
  - name: "Mass Effect"
   releaseDate: 2007-11-16 22:03:14 +0000
    - timeIntervalSinceReferenceDate: 216943394.0

Using dump you can print out entire class hierarchies and type information. In short, it gives you all the freakin’ information it can find about whatever you pass to it. For example, if you tacked on a Rating struct to our VideoGame and dumped it - it would break apart Rating and show its values too:

// Added as a property to VideoGame...
struct Rating {
    let average: Double
    let weighted: Double

let originalReleaseDate = Date(timeIntervalSince1970: 1195250594)
let massEffect = VideoGame(rating: .init(average: 9, weighted: 8.5),
                           genre: .Adventure,
                           name: "Mass Effect",
                           releaseDate: originalReleaseDate)


// Rating is shown as well
   rating: SwiftUI_Playgrounds.Rating
    - average: 9.0
    - weighted: 8.5
   id: 0DB0A374-031C-4211-8A00-C9BDCD6A59AF
    - uuid: "0DB0A374-031C-4211-8A00-C9BDCD6A59AF"
  - genre: SwiftUI_Playgrounds.VideoGame.Genre.Adventure
  - name: "Mass Effect"
   releaseDate: 2007-11-16 22:03:14 +0000
    - timeIntervalSinceReferenceDate: 216943394.0

But, using its formatting options, you can tweak indentation, the depth of the print out according to the value’s nested types and properties or even label it:

dump(massEffect, name: "Best Role Playing Game", indent: 4, maxDepth: 1)

// Resulting in 4 spaces of indentation, and just top level information of the types
     Best Role Playing Game: SwiftUI_Playgrounds.VideoGame
       rating: SwiftUI_Playgrounds.Rating
       id: 6814EDC9-73E4-4F72-9111-8F7CBEAC3712
      - genre: SwiftUI_Playgrounds.VideoGame.Genre.Adventure
      - name: "Mass Effect"
       releaseDate: 2007-11-16 22:03:14 +0000

Here, since out maxDepth was 1, complex types like Rating no longer displayed its property values - just that it was a Rating instance. This is great, because when you use dump() on something like UIView….you’ll find a boatload of type information. Maybe you want to pare it down a bit, ya know?

And with that, our little TL;DR of some different ways you can go about your print debugging adventures has concluded.

Final Thoughts

Which one of these you’d want to use is all about context. For the most part, print() is likely just fine. But, if you need more and want to know why they work the way they do and what their differences are - well, now you do!

I think with stuff like this, it’s simply about improving your Swift lexicon. Maybe you won’t use most of these in your day to day, but when the times comes that you need it - well hey, you know it exists now 😃.

Until next time ✌️

  1. Crazy enough, that’s been around since Xcode 6. More than half of Xcode’s lifespan. 

  2. In you want to see this in action, try calling private API. I just wrote about that fairly recently here


Spot an issue, anything to add?

Reach Out.