Reflection in Swift
Reflection is a common programming language feature that enables us to inspect, and work with, the members of a type — dynamically, at runtime. That may seem at odds with Swift’s heavy focus on compile-time validation, and while Swift’s implementation of reflection certainly is more limited than what you might see in other languages, it still has been there since day one.
Swift’s version of reflection enables us to iterate over, and read the values of, all the stored properties that a type has — whether that’s a struct, a class, or any other type — enabling a sort of meta programming that can enable us to write code that actually interacts with the code itself.
This week, let’s take a look at when reflection can come in handy, and how it can let us automate tasks by working with our code in a more dynamic fashion.
The use case
While there are many programming language features that can seem interesting “on paper”, or act as theoretical exercises, the big question is — what’s the actual use case? Iterating over the properties of a type may seem like a fun thing to do, but how can it be useful in practice? Let’s start by taking a look at an example.
Let’s say that an app we’re working on uses a UserSession
type to keep track of a logged in user’s session. It enables us to read and write user-specific data, such as the user’s credentials, favorite items, and settings — through a series of storage classes, like this:
class UserSession {
let credentials: CredentialsStorage
let favorites: FavoritesStorage
let settings: SettingsStorage
}
Since we want to persist all of the above data, so that the user doesn’t lose any information when quitting the app, the tricky part becomes how to reset all of that persistence once the user logs out? An initial implementation might be to have a logOut
method on our session that calls reset()
on each storage object:
extension UserSession {
func logOut() {
credentials.reset()
favorites.reset()
settings.reset()
}
}
While the above works, it’s a little bit fragile — since when introducing a new storage object, we won’t get any compile time indication of whether we’ve remembered to reset its state. While we could try to make sure that all user-specific settings are stored within the same folder on disk — and then write a unit test that verifies that it was emptied once the user has logged out — it’s far from a guarantee that our reset will be thorough and complete.
In this case in particular, this is also something we really need to make sure to get right — as otherwise we might leak user-specific data between accounts, which could have quite severe implications. So let’s see if we can use reflection to solve this problem for us, in a way that’s automatic and solid.
The first thing we’ll do is to make sure that we can reference all of our storage objects through one single API. In this case, we’re only interested in the reset()
method — so let’s start by extracting that into a Resettable
protocol that we then make all of our storage types conform to:
protocol Resettable {
func reset()
}
extension CredentialsStorage: Resettable {}
extension FavoritesStorage: Resettable {}
extension SettingsStorage: Resettable {}
Now that we have a standard, uniform way to interact with all of our resettable objects — it’s time to start reflecting!
Mirror, mirror, on the wall
Reflection in Swift is made available through the Mirror
API, which ships as part of the standard library. Using it is actually quite simple — the only thing we need to do is to create a mirror with the target we wish to reflect, and then use that mirror’s children
property to iterate over all of that target’s stored properties. Each child will contain a label
(the name of the property), and a value
(its value — of type Any
), looking like this:
let mirror = Mirror(reflecting: self)
for child in mirror.children {
print("Property name:", child.label)
print("Property value:", child.value)
}
Using the above, we can replace our logOut
code from before, that manually called reset()
on all storage objects, and instead simply iterate over all of our user session’s properties — and once we find a Resettable
value, we’ll reset it:
extension UserSession {
func logOut() {
let mirror = Mirror(reflecting: self)
for child in mirror.children {
if let resettable = child.value as? Resettable {
resettable.reset()
}
}
}
}
Now, if we ever add a new Resettable
property to UserSession
, it will automatically be reset once the user logs out. Pretty cool! 😎
The above technique is not only useful for resetting persistence, but can be used whenever we need to iterate over an unknown quantity of properties that all share the same API, and perform the same action on them. To make it easier to do so, we could introduce a small extension on Mirror
, that lets us qualify each child using a specific type, and then get access to its value in a type-safe manner — like this:
extension Mirror {
static func reflectProperties<T>(
of target: Any,
matchingType type: T.Type = T.self,
using closure: (T) -> Void
) {
let mirror = Mirror(reflecting: target)
for child in mirror.children {
(child.value as? T).map(closure)
}
}
}
Above we’re using a default argument for matchingType
, since in many scenarios Swift’s type inference will be able to automatically infer the type we’re looking for. However, when that’s not possible, we also give the API user the option to specify the type explicitly through that argument.
Using the above extension, we can now clean up our code from before, and have it use a nice closure-based syntax for iterating over and resetting all of its children — and we could then use that same extension for other use cases as well, such as preloading objects from a database, or warming up a series of caches:
extension UserSession {
func logOut() {
Mirror.reflectProperties(of: self) {
(child: Resettable) in
child.reset()
}
}
}
extension DataController {
func preload() {
Mirror.reflectProperties(of: self) {
(child: Preloadable) in
child.preload()
}
}
}
extension CacheController {
func warmUp() {
Mirror.reflectProperties(of: self) {
(child: Cache) in
child.warmUp()
}
}
}
The beauty of using reflection this way is that it requires no modification of the type we’re reflecting. We don’t need to store an array of properties, conform to a specific protocol, or make our code harder to work with. While above we did choose to implement our reflection using extensions on the target itself, that’s definitely not a requirement. For example, we could’ve just as easily written our log out code as a completely separate function:
func tearDown(_ session: UserSession) {
Mirror.reflectProperties(of: session) {
(child: Resettable) in
child.reset()
}
}
That gives us a ton of flexibility, while still automating our operations in a really nice way 👍.
Recursive reflections
So far, we’ve only been reflecting one level down — but sometimes we want to perform a series of operations on a hierarchy of objects. For example, let’s say that our SettingsStorage
type from above contains a storage object of its own — perhaps to store the user’s preferred languages:
class SettingsStorage {
let preferredLanguages: PreferredLanguagesStorage
}
With our current reflection implementation, the above, nested PreferredLanguagesStorage
object won’t be reset as part of our automatic user session tear down process — but the good news is that we can easily fix that by making our reflection code recursive. We’ll make it an optional feature, since we might not always want to iterate through all properties recursively, but when enabled (by passing recursively: true
to our Mirror
extension API), we’ll make sure to call our function recursively on all children that were found — like this:
extension Mirror {
static func reflectProperties<T>(
of target: Any,
matchingType type: T.Type = T.self,
recursively: Bool = false,
using closure: (T) -> Void
) {
let mirror = Mirror(reflecting: target)
for child in mirror.children {
(child.value as? T).map(closure)
if recursively {
// To enable recursive reflection, all we have to do
// is to call our own method again, using the value
// of each child, and using the same closure.
Mirror.reflectProperties(
of: child.value,
recursively: true,
using: closure
)
}
}
}
}
With the above in place, we can now update our logOut
method from before, to now recursively reset all of the session’s children — including the newly added PreferredLanguagesStorage
:
extension UserSession {
func logOut() {
Mirror.reflectProperties(of: self, recursively: true) {
(child: Resettable) in
child.reset()
}
}
}
Depending on the use case, we might choose to make recursive reflection opt-out instead of opt-in, but since these actions can potentially be destructive (like in the case of resetting persistence) — keeping things opt-in, like we did above, might be a good idea.
Limitations and alternatives
While reflection can be incredibly useful in Swift, its current implementation is still somewhat limited. In the examples above, we’ve only been dealing with read-only access and reference types — for good reason, because Mirror
currently doesn’t support writing any values, only reading them.
That’s not a problem when mutating objects and other reference types, but it does prevent us from doing things like assigning a default value to all properties of a struct, or dynamically constructing an object from a piece of data. While there are ways around those limitations, such as using the Objective-C runtime or manipulating unsafe pointers directly — it’s not something that’s natively supported through Swift’s top-level APIs.
A common alternative to those use cases that reflection doesn’t yet cover is to use compile time code generation — to inspect properties of our code and then generate new code based on those. That’s something that the Swift standard library code base even makes use of, and something we’ll be looking into more in detail in future articles.
Conclusion
Reflection can be a powerful tool in order to work with our code in a more dynamic fashion — for example to be able to automatically perform the same action on all properties of an object, to generate descriptions and string representations, or even to do automatic JSON encoding.
While we’ll most likely see an expansion of the current Mirror
API to support more use cases in the future — specifically to support mutations in a type-safe way — there’s already several tasks that can be performed using the reflection API as it exists today.
However, just like with any dynamic or meta programming, it’s always important to carefully consider when to use it — and perhaps even more importantly — when not to use it. While automation is nice — especially for use cases like the ones from this article, when we’re able to get a better guarantee that our code will remain correct over time, it can be quite easy to make things harder to understand or debug by introducing too much reflection and other dynamic language features.
What do you think? Do you currently use any reflection in your projects, or is it something you’ll try out? Let me know — along with your questions, comments and feedback — on Twitter @johnsundell.
Thanks for reading! 🚀