Using Swift Reflection

I’ve never made much use of Swift’s reflection features. Here’s a couple of situations where I found it useful.

Checking Properties Are Valid

In my first example, I had a large struct with 18 properties, most of which were strings. The user can change the properties in a form and I wanted to check each property was valid before allowing the user to save the form.

struct Record {
  var locked: Bool = false
  var name1: String
  var name2: String
  var name3: String
  var name4: String
  var name5: String
  ...
  var updated_at: Date = Date()
}

For the struct to be valid, each of the string properties has to be non-blank. I’m using a small extension on String to test for blank strings:

extension String {
  public var isBlank: Bool {
    allSatisfy { $0.isWhitespace }
  }
}

Testing each property in turn is a lot of typing:

extension Record {
  var isValid: Bool {
      !name1.isBlank &&
      !name2.isBlank &&
      !name3.isBlank &&
      !name4.isBlank &&
      !name5.isBlank &&
      ...
  }
}

My properties don’t have such uniform names so it’s easy for me to miss one. It’s also easy to forget to update it if I add a new property to the struct.

Swift Reflection

We can get access to the stored properties of a Swift type like my Record struct by using the Mirror API:

let record = Record(name1: "aaa", name2: "bbb",
  name3: "ccc", name4: "ddd", name5: "eee")
let mirror = Mirror(reflecting: record)

The children property of the mirror gives us access to the structure of the reflected instance. We can use that to iterate over the label and value of each property:

for child in mirror.children {
  let label = child.label ?? "-"
  print("\(label) \(child.value)")
}

locked false
name1 aaa
name2 bbb
name3 ccc
name4 ddd
name5 eee
updated_at 2023-04-24 14:54:57 +0000

Note that the label is optional as it’s not guaranteed to be available at runtime.

We can use this Mirror API to rewrite my isValid check:

extension Record {
  var isValid: Bool {
    let mirror = Mirror(reflecting: self)
    return mirror.children.compactMap {
      $0.value as? String
    }
    .allSatisfy { !$0.isBlank }
  }
}

The compactMap tries to cast each value to a string, discarding any failures. I can then test the array of strings are all non-blank. This version removes the risk of me missing a property and doesn’t need updating if I add or remove properties in the future.

let record = Record(name1: "aaa", name2: "  ",
  name3: "ccc", name4: "ddd", name5: "eee")
record.isValid  // false

Unit Testing

I’m a little wary of using the Mirror API for production code but it does have uses for unit testing. I have another method on my record structure that normalizes each of the string properties by trimming any leading or trailing whitespace.

The Mirror API saves us some error-prone typing when asserting that we correctly trimmed each string property (my struct has many more properties than this example):

func testNormalizeStrings() {
  let record = Record(name1: " xyz", name2: "xyz ",
    name3: " xyz ", name4: "  xyz", name5: "xyz  ")
  let normal = record.normalize()
  
  let mirror = Mirror(reflecting: normal)
  mirror.children.forEach { child in
    if let value = child.value as? String {
      XCTAssertEqual(value, "xyz")
    }
  }
}

What do you think? Have you found any other practical uses for the Mirror API?