Swift Metaprogramming: Writing Code that Inspects Itself

Most Swift developers never look beyond the syntax—but what if your code could inspect itself at runtime? This extract from Swift Internals explores Mirror, reflection, and @dynamicMemberLookup: the metaprogramming tools that let you build generic inspectors and clean, chainable APIs over dynamic data. By Aaqib Hussain.

Leave a rating/review
Save for later
Share
Note from the editor: The following is a short extract from Chapter 7 of Swift Internals, to give you a taste of what’s inside the book. We hope you find it useful and, if it piques your interest, consider picking up the full book for a much deeper dive into Swift’s inner workings. Enjoy!

The word “meta” is actually a Greek term that is commonly used as a prefix today. It means “beyond” or “after.” Metaprogramming refers to going “beyond” just writing code that runs. It’s not about the app logic itself — like performing a network request, developing UI, or building business logic — but about the powerful practice of writing code that can generate, analyze, or transform another piece of code. This is where the Swift language becomes a tool to manipulate your codebase.

Why should you care? This is the ultimate weapon against boilerplate code. The repetitive code you copy-paste for equality conformance, JSON decoding, or test mocks is a major source of bugs and is difficult to maintain. Here, metaprogramming acts as a knight in shining armor, enabling you to write a single piece of code that generates other repetitive code, ensuring consistency from a single source of truth. It also powers the creation of expressive, human-readable Domain-Specific Languages (DSLs).

This article explores two runtime metaprogramming techniques in Swift: reflection using Mirror, and dynamic property access using @dynamicMemberLookup.

The Magic Mirror: Runtime Reflection with Mirror

In standard programming, you write code that operates on data. You are aware of the variables, their types, properties, and methods during compilation. But what if you want to see it while your code is running? What if you want to write a generic inspector that can examine any object? Whether it’s a struct User, an enum NetworkError, or even a class you haven’t implemented yet.

This is a common feature also available in languages other than Swift. It’s called Reflection. It refers to a program’s ability to inspect its structure such as types, relationships, and properties at runtime. In Swift, the primary tool to effectively leverage this capability is Mirror.

What is Reflection?

Reflection is a kind of metaprogramming that happens only at runtime. Unlike compilation tools that validate code before execution, reflection examines your app’s objects in memory while they are active.

You can think of it as holding a mirror up to your code. Usually, a function only sees the values it’s passed. With reflection, you can see the structure of those values and answer questions such as:

  • What kind of thing are you? (A struct? A class? A tuple?)
  • What are the names of your properties?
  • What values are currently stored in those properties?

Swift intentionally limits reflection capabilities to preserve performance and type safety. Unlike Objective-C, Swift reflection does not allow method invocation, mutation, or dynamic type creation at runtime. However, Mirror offers a standardized, safe way to peek inside instances when you truly need that dynamic behavior.

How to Use Mirror?

Using a Mirror is quite simple. You create a Mirror to reflect any instance you want to inspect.

Consider the following code:

struct User {
  let name: String
  let age: Int
}

let michael = User(name: "Michael Scott", age: 44)

// Create the mirror
let mirror = Mirror(reflecting: michael)

Once you have the mirror object, one of its most useful properties is children. This is the collection of all visible parts of the reflected subject. Each child is a tuple containing an optional label (the property name) and a value (the stored data).

Iterating over the children collection:

print("Inspecting \(mirror.subjectType):")
for child in mirror.children {
  let propertyName = child.label ?? "unknown"
  print("  - \(propertyName): \(child.value)")
}
// Output:
// Inspecting User:
//   - name: Michael Scott
//   - age: 44

You can also identify the type of an object. It’s an optional enum that can be .struct, .class, .enum, .tuple, .optional, .collection, and more, by using the displayStyle property on the Mirror object. It is important to be aware of the type you’re dealing with when handling values. For example, you might want to format a .class differently than a .tuple in a logging tool.

Practical Use Case: Building a Generic prettyPrint

The best way to use Mirror is to create something useful with it. A common issue in debugging is printing complex objects, resulting in unreadable, jumbled output. You can use Mirror to write a generic function that uses reflection to recursively print any object with clear indentation.

This function doesn’t need prior knowledge of the types it will print. It will use Mirror to figure it out dynamically.

Take a look at the function below:

func prettyPrint(_ value: Any, indent: Int = 0) {
    let mirror = Mirror(reflecting: value)

    // Base case: If the value has no children, just print it directly.
    if mirror.children.isEmpty {
        print(value)
        return
    }

    // Determine if it's a collection to use [] instead of ().
    let isCollection = mirror.displayStyle == .collection
      || mirror.displayStyle == .set
      || mirror.displayStyle == .dictionary
    let open = isCollection ? "[" : "("
    let close = isCollection ? "]" : ")"

    // Print type name (if not a collection) and opening bracket.
    if !isCollection {
        print("\(mirror.subjectType)", terminator: "")
    }
    print(open)

    let childIndent = String(repeating: "  ", count: indent + 1)

    for child in mirror.children {
        // Always print indentation first
        print(childIndent, terminator: "")

        // If it has a label (like struct properties), print it.
        // Arrays usually don't have labels for their elements.
        if let label = child.label {
            print("\(label): ", terminator: "")
        }

        // Recurse for the value
        prettyPrint(child.value, indent: indent + 1)
    }

    // Print closing bracket with parent's indentation.
    let footerIndent = String(repeating: "  ", count: indent)
    print("\(footerIndent)\(close)")
}

Now, you can pass anything to this function:

struct Company {
  let boss: User
  let employees: [User]
}

let dunderMifflin = Company(
  boss: User(name: "Michael", age: 44),
  employees: [
    User(name: "Jim", age: 33),
    User(name: "Dwight", age: 38)
  ]
)

prettyPrint(dunderMifflin)

The prettyPrint function implements recursion and utilizes Mirror. It will traverse deeply into the Company struct, locate the boss property, verify that it is of type User, and continue digging.

It yields the following output:

<code>Company(
  boss: User(
    name: Michael
    age: 44
  )
  employees: [
    User(
      name: Jim
      age: 33
    )
    User(
      name: Dwight
      age: 38
    )
  ]
)</code>

It’s a powerful, versatile implementation built entirely on runtime introspection.

Contributors

Sam Davies

Final Pass Editor

Over 300 content creators. Join our team.