I’ve never made much use of advanced Swift features like reflection. 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?