Reflection in Swift: How Mirror works

Reflection in Swift allows us to use the Mirror API to inspect and manipulate arbitrary values at runtime. Even though Swift puts a lot of emphasis on static typing, we can get the flexibility to gain more control over types than you might expect.

I’ve been planning to explore the Mirror API for a long time, but I always had a hard time coming up with a use-case for it. Though, as you might expect now, I did find an excellent example in the end in which I needed to know the name of the enum case without any associated types. It turned out that the Mirror API was what I needed.

What is Reflection?

Reflection in Swift comes down to printing out metadata information about a given value. Let’s take a look at an example of a struct representing an article and print out its metadata using the Mirror API:

struct Article {
    let title: String
    let author: String

    func publish() {
        // ..
    }
}

let article = Article(title: "Reflection in Swift", author: "Antoine van der Lee")
let mirror = Mirror(reflecting: article)
mirror.children.forEach { child in
    print("Found child '\(child.label ?? "")' with value '\(child.value)'")
}

// Prints:
// Found child 'title' with value 'Reflection in Swift'
// Found child 'author' with value 'Antoine van der Lee'

The console displays both labels and values without knowing anything about their types. The label is a copy of the property name where the value contains an Any type. It’s also worth pointing out that we don’t get any information about the publish() method as it’s not a child of Article since it doesn’t represent storage.

The above example shows us how the Mirror API works, resulting in reflecting the given value. You can traverse an object graph by performing the same reflecting logic on children of children.

If you’re curious how this works under the hood, I encourage you to read this article on the Swift blog.

Using Mirror on common types

The Mirror API, by default, tries its best to come up with a list of children based on the given value. Reflecting on an enum will have a different outcome than reflecting on a struct as we did before. I’ve created a few code examples representing common types to provide you with a clear overview.

Reflecting a class

Using the Mirror API on a class results in a reflection of all stored properties:

final class ArticlePublisher {
    let blogTitle: String = "SwiftLee"
}
let publisher = ArticlePublisher()
Mirror(reflecting: publisher).children.forEach { child in
    print("Found child '\(child.label ?? "")' with value '\(child.value)'")
}
// Prints:
// Found child 'blogTitle' with value 'SwiftLee'

Reflecting a struct

A struct is handled the same as class and prints out its stored properties as children:

struct Article {
    let title: String
    let author: String
}

let article = Article(title: "Reflection in Swift", author: "Antoine van der Lee")
Mirror(reflecting: article).children.forEach { child in
    print("Found child '\(child.label ?? "")' with value '\(child.value)'")
}

Reflecting a tuple

A tuple prints out its values in sequence using the dot-index annotation:

let tuple = ("Antoine", "van der Lee")
Mirror(reflecting: tuple).children.forEach { child in
    print("Found child '\(child.label ?? "")' with value '\(child.value)'")
}
// Prints:
// Found child '.0' with value 'Antoine'
// Found child '.1' with value 'van der Lee'

Reflecting an enum

Enums are a special kind of reflection since they will adjust accordingly based on associated values. Let’s take the following enum as an example:

enum ArticleState {
    case scheduled(Date)
    case published
}

Once we reflect the scheduled state, we will get a child returned for the given associated date value:

let scheduledState = ArticleState.scheduled(Date())
Mirror(reflecting: scheduledState).children.forEach { child in
    print("Found child '\(child.label ?? "")' with value '\(child.value)'")
}
// Prints:
// Found child 'scheduled' with value '2021-12-21 08:16:48 +0000'

However, if we use the same mirror code on the published state, there will be no children to print:

let publishedState = ArticleState.published
Mirror(reflecting: publishedState).children.forEach { child in
    print("Found child '\(child.label ?? "")' with value '\(child.value)'")
}
// Prints:
// -

The published enum case does not have any associated values and, therefore, no children. If you’re looking for a way to print out the case name in all scenarios, you can make use of the following code example instead:

// Define a new protocol.
protocol CaseNameReflectable {
    var caseName: String { get }
}

// Add a default method implementation.
extension CaseNameReflectable {
    var caseName: String {
        let mirror = Mirror(reflecting: self)
        guard let caseName = mirror.children.first?.label else {
            return "\(self)"
        }
        return caseName
    }
}

// Make your enum conform to the protocol.
extension ArticleState: CaseNameReflectable { }

// Print out the case names:
print(publishedState.caseName) // Prints: 'published'
print(scheduledState.caseName) // Prints: 'scheduled'

Note that this won’t print out the associated values anymore. We can create another convenient method for fetching the associated values:

// Define a new protocol.
protocol AssociatedValuesReflectable { }

// Add a default method implementation.
extension AssociatedValuesReflectable {
    var associatedValues: [String: String] {
        var values = [String: String]()
        guard let associated = Mirror(reflecting: self).children.first else {
            return values
        }

        let children = Mirror(reflecting: associated.value).children
        for case let item in children {
            if let label = item.label {
                values[label] = String(describing: item.value)
            }
        }
        return values
    }
}

// Make your enum conform to the protocol.
extension ArticleState: AssociatedValuesReflectable { }

// Print out the associated values:
print(publishedState.associatedValues) // Prints: '[:]'
print(scheduledState.associatedValues) // Prints: '["timeIntervalSinceReferenceDate": "661768488.826595"]'

The published enum case returns an empty dictionary, whereas the scheduled enum case returns a dictionary with the given date value. Though, since we defined our enum case without a label, the dictionary isn’t readable. To improve this, we can adjust the enum case to contain a label, and we’ll get a better dictionary output:

enum ArticleState {
    case scheduled(date: Date)
    case published
}

let scheduledState = ArticleState.scheduled(date: Date())
print(scheduledState.associatedValues) // Prints: '["date": "2021-12-21 08:35:29 +0000"]

Providing a custom Mirror description using CustomReflectable

The CustomReflectable protocol allows changing the Mirror description, which can be helpful in cases the default introspection result is not fulfilling. Some of the default types in Swift inherit from this protocol to provide custom reflection results, like an Array exposing its elements as unlabeled children:

let array = ["Bernie", "Jaap", "Lady"]
Mirror(reflecting: array).children.forEach { child in
    print("Found child '\(child.label ?? "")' with value '\(child.value)'")
}
// Prints:
// Found child '' with value 'Bernie'
// Found child '' with value 'Jaap'
// Found child '' with value 'Lady'

The array example also explains why we must deal with an optional label.

You can use the CustomReflectable protocol as follows:

extension Article: CustomReflectable {
    public var customMirror: Mirror {
        return Mirror(self, children: ["title": title])
    }
}
let articleExample = Article(title: "Reflection in Swift", author: "Antoine van der Lee")
Mirror(reflecting: articleExample).children.forEach { child in
    print("Found child '\(child.label ?? "")' with value '\(child.value)'")
}

// Before custom reflection:
// Found child 'title' with value 'Reflection in Swift'
// Found child 'author' with value 'Antoine van der Lee'
//
// After custom reflection:
// Found child 'title' with value 'Reflection in Swift'

In this example, we’re ensuring only to return the title in the reflection.

Practical examples of using the Mirror API

Now that we know how reflection in Swift works, it’s time to look into a practical example of using the Mirror API. The whole reason I started writing this article is that I finally found a reason to use the Mirroring API.

At WeTransfer, we’re making use of Datadog to collect non-fatal errors. We define errors as enum cases in which some of them have associated values. Datadog groups errors based on their unique message. The associated value of errors results in unique error messages while the underlying error enum case is the same:

Reflection in Swift helped us to group these errors by removing the associated value from the error message.
Reflection in Swift helped us group these errors by removing the associated value from the error message.

The above list of errors shows three different versions of the same hard link creation failed error. The file name in the associated value made each error unique, so Datadog didn’t group those errors.

Grouping errors based on enum case names

The solution for us was to write a custom Mirror extension on Swift’s Error type, which will give us a message containing the enum case name only. I shared the code to extract the enum case and its associated values earlier in this article, resulting in the following outcome in Datadog:

Using the Mirror API we now grouped similar errors by ignoring the associated value.
Using the Mirror API we now grouped similar errors by ignoring the associated value.

As you can see, Datadog grouped all hard link creation failed errors, resulting in a clean overview of non-fatal errors. The associated values are still important to investigate further, which is why we added those as custom properties of the tracked error:

You can display associated values collected through reflection as custom attributes.

Overall, the Mirror API makes it possible for us to group similar errors. The result is an easier-to-digest overview of non-fatal errors allowing us to determine better which errors occur the most.

Conclusion

The Mirror API allows reflecting any type to gather more information about the metadata of a value. You can use reflection to extract labels and values from children at runtime. Finding a practical use case might be challenging, but pulling the enum case names to group errors can be a good example.

If you like to learn more tips on Swift, check out the swift category page. Feel free to contact me or tweet me on Twitter if you have any additional suggestions or feedback.

Thanks!