Dynamic Features in Swift
In this tutorial, you’ll learn to use dynamic features in Swift to write clean code, create code clarity and resolve unforeseen issues quickly. By Mike Finney.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Dynamic Features in Swift
25 mins
- Getting Started
- Reflecting on Mirror and Debug Output
- CustomDebugStringConvertible
- Dump
- Swift Mirror
- Creating a Mirror-Powered Dog Log
- Creating a Mirror
- CustomReflectable
- Wrapping Up Mirror and Debug Output
- Key Paths
- Understanding Dynamic Member Lookup
- Introducing @dynamicMemberLookup
- Adding dynamicMemberLookup to the Dog
- Overloading subscript(dynamicMember:)
- Compiler and Code Completion Gone to the Dogs
- Friendly Dog Catcher
- Where to Go From Here?
Creating a Mirror
It’s time to create a log function that uses a mirror. To start, add the following code right under ☆ Create log function here:
func log(itemToMirror: Any) {
let mirror = Mirror(reflecting: itemToMirror)
debugPrint("Type: 🐶 \(type(of: itemToMirror)) 🐶 ")
}
This creates the mirror for the passed-in item. A mirror lets you iterate over the parts of an instance.
Add the following code to the end of the log(itemToMirror:)
:
for case let (label?, value) in mirror.children {
debugPrint("⭐ \(label): \(value) ⭐")
}
This accesses the children
property of the mirror, gets each label-value pair, then prints them out to the console. The label-value pair is type-aliased as Mirror.Child
. For a DogCatcherNet
instance, the code iterates over the properties of a net object.
To clarify, a child of the instance being inspected has nothing to do with a superclass or subclass hierarchy. The children accessible through a mirror are just the parts of the instance being inspected.
Now, it’s time to call your new log method. Add the following code right under ☆ Log out the net and a Date object here:
log(itemToMirror: net)
log(itemToMirror: Date())
Run the playground. You’ll see at the bottom of the console output some doggone great output:
"Type: 🐶 DogCatcherNet 🐶 "
"⭐ customerReviewStars: two ⭐"
"⭐ weightInPounds: 2.6 ⭐"
"Type: 🐶 Date 🐶 "
"⭐ timeIntervalSinceReferenceDate: 551150080.774974 ⭐"
This shows all the properties’ names and values. The names appear as they do in your code. For example, customerReviewStars
is literally how the property name is spelled in code.
CustomReflectable
What if you wanted more of a dog and pony show in which the property names are displayed more clearly as well? What if you didn’t want some of the properties displayed? What if you wanted items displayed that are not technically part of the type? You’d use CustomReflectable
.
CustomReflectable
provides the hook with which you can specify what parts of a type instance are shown by using a custom Mirror
. To conform to CustomReflectable
, a type must define the customMirror
property.
After speaking with several dog catcher programmers, you’ve discovered that spitting out the weightInPounds
of the net has not helped with debugging. However, the customerReviewStars
information is extremely helpful and they’d like the label for customerReviewStars
to appear as “Customer Review Stars.” Now, it’s time to make DogCatcherNet
conform to CustomReflectable
.
Add the following code right under ☆ Add Conformance to CustomReflectable for DogCatcherNet here:
extension DogCatcherNet: CustomReflectable {
public var customMirror: Mirror {
return Mirror(DogCatcherNet.self,
children: ["Customer Review Stars": customerReviewStars,
],
displayStyle: .class, ancestorRepresentation: .generated)
}
}
Run the playground and see the following output:
"Type: 🐶 DogCatcherNet 🐶 "
"⭐ Customer Review Stars: two ⭐"
Where’s the Dog?
The whole point of the net is to handle having a dog. When the net is populated with a dog, there must be a way to pull out information about the dog in the net. Specifically, you need the dog’s name and age.
The playground page already has a Dog
class. It’s time to connect Dog
with DogCatcherNet
. In the spot labeled as ☆ Add Optional called dog of type Dog here, add the following property to DogCatcherNet
:
var dog: Dog?
With the dog property added to the DogCatcherNet
, it’s time to add the dog to the customMirror
for the DogCatcherNet
. Add the following dictionary entries right after the line children: ["Customer Review Stars": customerReviewStars,
:
"dog": dog ?? "",
"Dog name": dog?.name ?? "No name"
This will output the dog using its default debug description and dog’s name, respectively labeled “dog” and “Dog name.”
Time to gently put a dog into the net. Right under ☆ Uncomment assigning the dog, uncomment that line so the cute little dog is put into the net:
net.dog = Dog() // ☆ Uncomment out assigning the dog
Run the playground and see the following:
"Type: 🐶 DogCatcherNet 🐶 "
"⭐ Customer Review Stars: two ⭐"
"⭐ dog: __lldb_expr_23.Dog ⭐"
"⭐ Dog name: Abby ⭐"
Mirror Convenience
It’s pretty nice to be able to see everything. However, there are those times when you just want to pluck out a part from a mirror. To do that, you use descendant(_:_:)
. Add the following code to the end of the playground page to create a mirror and use descendant(_:_:)
to pluck out the name and age:
let netMirror = Mirror(reflecting: net)
print ("The dog in the net is \(netMirror.descendant("dog", "name") ?? "nonexistent")")
print ("The age of the dog is \(netMirror.descendant("dog", "age") ?? "nonexistent")")
Run the playground and see at the bottom of the console output:
The dog in the net is Bernie
The age of the dog is 2
That’s doggone dynamic introspection there. It can be quite useful for debugging your own types! Having deeply explored Mirror
, you’re done with DogMirror.xcplaygroundpage.
Wrapping Up Mirror and Debug Output
There are many ways to track, like a bloodhound, what’s going on in a program. CustomDebugStringConvertible
, dump
and Mirror
let you see more clearly what you are hunting for. Swift’s introspection power is highly useful — especially as you start building bigger and more complex applications!
Key Paths
On the subject of tracking what’s going on in a program, Swift has something wonderful called key paths. For capturing an event such as when a value has changed in a third-party library object, look to KeyPath
‘s observe
for help.
In Swift, key paths are strongly typed paths whose types are checked at compile time. In Objective-C, they were only strings. The tutorial What’s New in Swift 4? does a great job covering the concepts in the Key-Value Coding section.
There are several different types of KeyPath
. Commonly discussed types include KeyPath, WritableKeyPath and ReferenceWritableKeyPath. Here’s a summary of the different ones:
-
KeyPath
: Specifies a root type on a specific value type. -
WritableKeyPath
: A KeyPath whose value you can also write to. It doesn’t work with classes. -
ReferenceWritableKeyPath
: A WritableKeyPath used for classes since classes are reference types.
A practical example of using a key path is in observing or capturing when a value changes on an object.
When you encounter a bug involving a third-party object, there is immense power in knowing when the state of that object changes. Beyond debugging, sometimes it just makes sense to hook up your custom code to respond when a value changes in a third-party object such as Apple’s UIImageView object. In Design Patterns on iOS using Swift – Part 2/2, you can learn more about the observer pattern in the section titled Key-Value Observing (KVO).
However, there’s a use case here related to the kennels that fits right into our doggy world. Without this KVO power, how would dog catchers easily know when the kennels are available to put more dogs inside? Although many dog catchers would just love to take home each and every lost dog they find, it’s just not practical.
So dog catchers who just want to help dogs find their way home need to know when the kennels are available to place dogs into. The first step to making this possible is creating a key path. Open the KennelsKeyPath page and, right after ☆ Add KeyPath here, add this:
let keyPath = \Kennels.available
This is how you create a KeyPath
. You use a backslash on the type, followed by a chain of dot-separated properties — in this case, one property deep. To use the KeyPath
to observe changes to the available
property, add the following code after ☆ Add observe method call here:
kennels.observe(keyPath) { kennels, change in
if (kennels.available) {
print("kennels are available")
}
}
Click run and see the following message output to the console:
Kennels are available.
This approach can also be great for figuring out when a value has changed. Imagine being able to debug state changes to third-party objects! Nailing down when an item of interest changes can really keep you from barking up the wrong tree.
You’re done with the KennelsKeyPath page!