Implementation of Reflection in Swift
Reflection is a set of functions that allows a program to inspect and modify its own structure or even how it works at runtime. I’ll start with a real life use case.
The output is going to be like this:
Although reflection is relatively slow, the advantage is obvious: you can write highly dynamic code that minimizes interface and still achieves functionalities that’s desired. For example, with
LFModel, you don’t have to tell what properties there would be in the data model while parsing. The library loops through all the keys/values and processes different data types itself.
In this post, I’ll be focusing on the implementation of reflection in
Swift, and hopefully it will help you get a better understanding of the dynamic feature of
Swift, so that it can be used in your own framework.
Creating a native object based on a Dictionary
To create a native object from a given
Dictionary (or a
JSON object that can be deserialized to native
Dictionary), we can use
for (key, value) in dict to loop through it, and assign the keys/values to an object using
setValue(value, forKey:key). However, assigning a value to a non-existent key will cause an error, thus we need to check if the object
But here’s something funny happening. Please try to guess what will happen and run the following code in a
Int can be bridged to
TestObject does not respond to
setValue(42, forKey:("int1")) will result an error. So it shows that an
NSObject responds to:
Intwith a default value
NSNumber?, a subclass of
String?, a native
TestObject is an
NSObject, all the method calls are identical in
Objective-C. However, to do things the other way around, some
Swift only mechanism will be introduced.
Getting all properties from a native object
Objective-C, to get all properties from a
NSObject, we can use Objective-C runtime APIs
class_copyPropertyList to get the property list, and then use
property_getName to get the names of all the items.
keys: ["int10", "str10", "str11"] as the results in
int11 is still missing, so with this approach you’ll always need to give types like
Int a default value.
Furthermore, if we inherit from
TestObject but not
class ChildObject: TestObject, we’re going to see the same result. In real life it’s very common to create things like a
StudentObject based on a
UserObject, and we do need to know
student1.name, in which name is a property of
UserObject if I’m allowed to be Captain Obvious here. Anyway, to support things like this, we need to do some little tweaks:
And now we’re getting
keys: ["int10", "str10", "str11", "int0", "int3", "str"] - guess we are used to the absence of
int11 already. Basically what we did this time was trying to loop through the object and its superclasses to get all the properties of each other, until the superclass is
Mirror in Swift 2
If you’re not quite comfortable with the code above since it’s too
objc, it’s perfectly fine: actually
Swift has its own reflection mechanism, and it’s significantly changed since
reflect() and represents the structure of a native object. Try to add the code below after the definition of
ChildObject and its instance
And what do we get this time?
Yay! Not only the code is much simpler, we also have all the
Int families back in. You can read more about
Mirror in The Swift Reflection API and what you can do with it, but from the code above we already get what we want: getting all properties from a native object.
Getting the class of a property from its name
You may notice the commented
UserModel. It can be interpret to “after everything is initialized, find the key
friends, and reload it as either a
UserModel or an array of
UserModel, based on the type of
friends”. Without this line
friends will be set as the original
Array, which looks like
[["id": 43, "name": "Leo"]]. So suppose the function
reload is already there, how to implement the
init so the
reloads happen automatically?
Firstly let’s see what we need. In
NSClassFromString is used to get the class of the object from a string.
If you inspect
type, which in our case is
NSStringFromClass(UserModel), it’s something like
LFramework_Example.UserModel, as we can see a
Swift class is like “bundle name + class name”. So we can loop through
child in Mirror(reflecting:self).type.children and find the
child.label is equal to either
child.value.dynamicType is going to be:
So if we get rid of the
Array<> part, and append it after bundle name
bundle.infoDictionary[kCFBundleNameKey], we’ll be able to use the class name string to do the
reload. After the following code is added inside
LFModel, we don’t need to call the
However, the code above is just a proof of concept to show how to get the class of a property from its name. We don’t have to do it if we pass the class instead of the class name to
reload, and not to mention it highly relies on the implementation of how
NSStringFromClass works in
Swift, which might be changed in future. I would highly recommend to use the better maintained
EVObject instead of my
LFModel, which is used in this tutorial just because its implementation is much simpler to understand.
In this post, we’ve discussed the tricks of assigning keys/values to a native object and the other way around, i.e. getting properties from a native object in both the old
Objective-C runtime way and the new
Mirror way, as well as getting the class of a property from its name. In the beginning these tricks are used to convert a
Dictionary into an object.
The downside of reflection is always about performance, and it makes it harder to perform static analytics, so it’s more often used in libraries and not the actual business logic, expect you’re 100% sure what you’re doing, which might not be right if you take a look at the code 1 year later. And particularly in
Swift, it’s a relatively new and fast evolving language, and the new version is not always going to be backward compatible. For example, as we mentioned in
reflect(), and there’s no guarantee that
NSStringFromClass is always going to work in the same way.
Despite of all the disadvantages, using
reflection carefully results highly dynamic code, simplifies interface, and allows you to think out of the box.
All the code above can be found in the
refactor/framework branch of LSwift.