The Swift Reflection API and what you can do with it
In this guide we'll examine the Swift reflection API, see how fast it is, and will try to show use cases where it can be applied successfully.
Introduction
Even though Swift stresses strong types, compile time safety and static dispatch, it still offers a Reflection mechanism as part of the standard library.
Reflection means that you can ask Swift at runtime questions about types. I.e. you can tell Swift "what are the methods that this class implements"
This might not sound useful, but in reality is allows to do some really
clever tricks: For example, you could write a function that takes any
struct
, lists all the properties (var username, var age
, etc)
and writes this information into Core Data.
Reflection in Swift is read-only
, so you can't write any properties.
However, it is still quite powerful. This guide will explain reflection
and also show how it can be used in a practical way (the aforementioned
struct to Core Data example).
The best understanding of the topic can be achieved by having a look at the API to see what it offers us.
Mirrors
Swift's reflection capabilities are based around a struct
called
Mirror. You create a mirror for a particular subject
and the
mirror will then let you query it.
Before we look at the API, let's define a simple data structure that we can experiment on.
import Foundation
public class Store {
let storesToDisk: Bool = true
}
public class BookmarkStore: Store {
let itemCount: Int = 10
}
public struct Bookmark {
enum Group {
case tech
case news
}
private let store = {
return BookmarkStore()
}()
let title: String?
let url: URL
let keywords: [String]
let group: Group
}
let aBookmark = Bookmark(title: \"Appventure\", url: URL(string: \"appventure.me\")!, keywords: [\"Swift\", \"iOS\", \"OSX\"], group: .tech)
So, we have a Bookmark
. Bookmarks can have titles, urls, keywords, and bookmarks can belong to a Group
. There is also a BookmarkStore
and a more general Store
. So, how do we query this data structure at runtime?
Creating a Mirror
The easiest way of creating a mirror is the reflecting
initializer:
public init(reflecting: Any)
Lets use it with our Bookmark
struct
:
let myMirror = Mirror(reflecting: aBookmark)
print(myMirror)
// prints : Mirror for Bookmark
So this creates a Mirror for Bookmark
. As you can see, the type of the
subject is Any
. This is the most general type in Swift. Anything under
the Swift Sun is at least of type Any
1. So this makes the mirror
compatible with struct
, class
, enum
, Tuple
, Array
,
Dictionary
, Set
, etc.
There are three additional initializers in the Mirror struct, however those are mostly used for circumstances where you'd want to provide your own, custom mirror.
In particular, Any
is an empty protocol and everything implicitly conforms to this protocol
What is in a Mirror
The Mirror struct
contains several types
to help you identify the
information you'd like to query.
The first one is the DisplayStyle
enum
which tells you the type of
the subject:
public enum DisplayStyle {
case `struct`
case `class`
case `enum`
case tuple
case optional
case collection
case dictionary
case set
}
Those are the supported types of the reflection API. As we saw earlier,
reflection only requires an Any
type, and there're many things in the
Swift standard library that are of type Any
but aren't listed in the
DisplayStyle
enum above. What happens when you try to reflect over one
of those, say a closure?
let closure = { (a: Int) -> Int in return a * 2 }
let aMirror = Mirror(reflecting: closure)
In this case, you'd get a mirror, but the DisplayStyle
would be nil
1
There's also a typealias
for the child elements of a Mirror
:
public typealias Child = (label: String?, value: Any)
So each child consists out of an optional label and a value of
type Any
. Why would the label be an Optional
? If you think about it,
it makes sense, not all of the structures that are supported by
reflection have children with names. A struct
has the property's name
as the label, but a Collection only has indexes, not names. Tuples are a
little bit special. In Swift values in tuple could have optional labels.
Doesn't matter if value in tupple is labeled or not, in reflection
tuple will have labels ".0", ".1" and so on.
Next up is the AncestorRepresentation
enum
:
public enum AncestorRepresentation {
/// Generate a default mirror for all ancestor classes. This is the
/// default behavior.
case generated
/// Use the nearest ancestor's implementation of `customMirror()` to
/// create a mirror for that ancestor.
case customized(@escaping () -> Mirror)
/// Suppress the representation of all ancestor classes. The
/// resulting `Mirror`'s `superclassMirror()` is `nil`.
case suppressed
}
This enum
is used to define how superclasses of the reflected subject
should be reflected. I.e. this is only used for subjects of type
class
. The default (as you can see) is that Swift generates an
additional mirror for each superclass. However, if you need more
flexibility here, you can use the AncestorRepresentation enum
to
define how superclasses are being mirrored.
How to use a Mirror
So we have our myMirror
instance variable that reflects our Bookmark
. What do we do with it?
These are the available properties / methods on a Mirror
:
let children: Children
: The child elements of our subjectdisplayStyle: Mirror.DisplayStyle?
: The display style of the subjectlet subjectType: Any.Type
: The type of the subjectfunc superclassMirror() -> Mirror?
: The mirror of the subject's superclass
In the next step, we will analyze each of these.
displayStyle
This is easy. It will just return a case of the DisplayStyle
enum
.
If you're trying to reflect over an unsupported type, you'll get an
empty Optional
back (as explained above).
print (aMirror.displayStyle)
// prints: Optional(Swift.Mirror.DisplayStyle.struct)
children
This returns a AnyCollection<Child>
with all the children that
the subject contains. Children are not limited to entries in an Array
or Dictionary
. All properties of a struct
or class
, for example,
are also children returned by this property. The protocol
AnyCollection
means that this is a type erased Collection
.
for case let (label?, value) in myMirror.children {
print (label, value)
}
//prints:
//: store main.BookmarkStore
//: title Optional(\"Appventure\")
//: url appventure.me
//: keywords [\"Swift\", \"iOS\", \"OSX\"]
//: group tech
SubjectType
This is the type of the subject:
print(aMirror.subjectType)
//prints : Bookmark
print(Mirror(reflecting: 5).subjectType)
//prints : Int
print(Mirror(reflecting: \"test\").subjectType)
//prints : String
print(Mirror(reflecting: Null()).subjectType)
//print : Null
However, the Swift documentation has the following to say:
This type may differ from the subject's dynamic type when
self
is thesuperclassMirror()
of another mirror.
SuperclassMirror
This is the mirror of the superclass of our subject. If the subject is
not a class, this will be an empty Optional
. If this is a class-based
type, you'll get a new Mirror
:
// try our struct
print(Mirror(reflecting: aBookmark).superclassMirror())
// prints: nil
// try a class
print(Mirror(reflecting: myBookmark.store).superclassMirror())
// prints: Optional(Mirror for Store)
Structs to Core Data
Imagine we're working at the newest, hot, tech startup: Books Bunny. We offer an Artificial Intelligence with a browser plugin that automatically analyses all the sites that the user visits and automatically bookmarks the relevant urls.
Our server backend is
obviously written in Swift. Since we have millions of site visits active
in our system at a time, we'd like to use structs
for the analysis
part of each site that a user visits. However, if our AI decides that
this is worthy of a bookmark, we'd like to use CoreData to store this
type in a database.
Now, we don't want to write custom serialization to Core Data code
whenever we introduce a new struct
. Rather, we'd like to develop this
in a way so that we can utilize it for all future structs
we develop.
So, how do we do that?
Structs to Core Data
Remember, we have a struct
and want to automatically convert this to
NSManagedObject
(Core Data).
If we want to support different structs
or even types, we can
implement this as a protocol and then make sure our desired types
conform to it. So which functionality should our imaginary protocol
offer?
- First, it should allow us to define the name of the Core Data Entity that we want to create
- Second, it should have a way to tell it to convert itself to an
NSManagedObject
Our protocol
could look something like this:
protocol StructDecoder {
// The name of our Core Data Entity
static var EntityName: String { get }
// Return an NSManagedObject with our properties set
func toCoreData(context: NSManagedObjectContext) throws -> NSManagedObject
}
The toCoreData
method uses exception handling to
throw an error, if the conversion fails. There're several possible
error cases, which are outlined in the ErrorType
enum
below:
enum SerializationError: ErrorType {
// We only support structs
case structRequired
// The entity does not exist in the Core Data Model
case unknownEntity(name: String)
// The provided type cannot be stored in core data
case unsupportedSubType(label: String?)
}
We have three error cases that our conversion has to look out for. The
first one is that we're trying to apply it to something that is not a
struct
. The second is that the entity
we're trying to create does
not exist in our Core Data Model. The third is that we're trying to
write something into Core Data which can not be stored there (i.e. an
enum
).
Let's create a struct and add protocol conformance:
Bookmark struct
struct Bookmark {
let title: String
let url: URL
let pagerank: Int
let created: Date
}
Next, we'd like to implement the toCoreData
method.
Protocol Extension
We could, of course, write this anew for each struct
, but that's a
lot of work. Structs do not support inheritance, so we can't use a base
class. However, we can use a protocol extension
to extend to all
conforming structs
:
extension StructDecoder {
func toCoreData(context: NSManagedObjectContext) throws -> NSManagedObject {
}
}
As this extension is being applied to our conforming structs
, this
method will be called in the structs context. Thus, within the
extension, self
refers to the struct
which we'd like to analyze.
So, the first step for us is to create an NSManagedObject
into which
we can then write the values from our Bookmark struct
. How do we do
that?
A Bit of Core Data
Core Data is a tad verbose, so in order to create an object, we need the following steps:
- Get the name of the entity which we'd like to create (as a string)
- Take the
NSManagedObjectContext
, and create anNSEntityDescription
for our entity - Create an
NSManagedObject
with this information.
When we implement this, we have:
// Get the name of the Core Data Entity
let entityName = type(of: self).EntityName
// Create the Entity Description
// The entity may not exist, so we're using a 'guard let' to throw
// an error in case it does not exist in our core data model
guard let desc = NSEntityDescription.entityForName(entityName, inManagedObjectContext: context)
else { throw unknownEntity(name: entityName) }
// Create the NSManagedObject
let managedObject = NSManagedObject(entity: desc, insertIntoManagedObjectContext: context)
Implementing the Reflection
Next up, we'd like to use the Reflection API to read our bookmarks
properties and write it into our NSManagedObject
instance.
// Create a Mirror
let mirror = Mirror(reflecting: self)
// Make sure we're analyzing a struct
guard mirror.displayStyle == .struct else { throw SerializationError.structRequired }
We're making sure that this is indeed a struct
by testing the
displayStyle
property.
So now we have a Mirror
that allows us to read properties, and we have
a NSManagedObject
which we can set properties on. As the mirror offers
a way to read all children, we can iterate over them and set the values.
So let's do that.
for case let (label?, value) in mirror.children {
managedObject.setValue(value, forKey: label)
}
Now, the only thing left to do is return our NSManagedObject
. The
complete code looks like this:
extension StructDecoder {
func toCoreData(context: NSManagedObjectContext) throws -> NSManagedObject {
let entityName = type(of:self).EntityName
// Create the Entity Description
guard let desc = NSEntityDescription.entityForName(entityName, inManagedObjectContext: context)
else { throw UnknownEntity(name: entityName) }
// Create the NSManagedObject
let managedObject = NSManagedObject(entity: desc, insertIntoManagedObjectContext: context)
// Create a Mirror
let mirror = Mirror(reflecting: self)
// Make sure we're analyzing a struct
guard mirror.displayStyle == .Struct else { throw SerializationError.structRequired }
for case let (label?, anyValue) in mirror.children {
managedObject.setValue(anyValue, forKey: label)
}
return managedObject
}
}
That's it. We're converting our struct
to NSManagedObject
.
Performance
So we just wrote some code that converts struct
types via reflection
at runtime to Core Data
types.
How fast is this? Can this be used well in production? I did some testing:
Create 2000 NSManagedObjects
Native, here, means creating an NSManagedObject
and setting the
property values via setValueForKey
. If you create a NSManagedObject
subclass within Core Data and set the values directly on the properties
(without the dynamic setValueForKey
overhead) this is probably even
faster.
So, as you can see, using reflection slows the whole process of creating
NSManagedObjects
down by about 3.5x. This is fine when you're
using this for a limited amount of items, or when you don't have to
care about speed. However, when you need to reflect over a huge amount
of structs
, this will probably kill your app's performance.
Custom Mirrors
As we already discussed earlier, there're other options creating a
Mirror. This is useful, for example, if you need to customize just how
much of your subject can be seen with a mirror. The Mirror Struct
has additional initializers for this.
This is especially useful in two cases:
- If reflection is a core part of your code, so that you have more control
- If you write a library and you expect that consumers will use reflection on it and you'd rather surpress that.
The way you use it is via the CustomReflectable
protocol. This protocol only has one requirement: var customMirror: Mirror
. If you implement it, you can create your own Mirror
and return that instead of the custom Mirror(reflecting:)
one.
Collections
In order to
The first special init
is tailor-made for collections:
init<Subject, C>(_ subject: Subject, children: C, displayStyle: Mirror.DisplayStyle? = nil, ancestorRepresentation: Mirror.AncestorRepresentation = .generated) where C : Collection, C.Element == Mirror.Child
Compared to the init(reflecting:)
initializer above, this one allows
us to define much more details about the reflection process.
- It only works for collections
- We can set the subject to be reflected and the children of the subject (the collection contents)
Classes or Structs
The second can be used for a class
or a struct
.
init<Subject>(_ subject: Subject, children: KeyValuePairs<String, Any>, displayStyle: Mirror.DisplayStyle? = nil, ancestorRepresentation: Mirror.AncestorRepresentation = .generated)
Interesting to note, here, is that you provide the children (i.e.
properties) of your subject as a KeyValuePairs<String, Any>
which is a bit like
a dictionary only that it can be used directly as function parameters.
Conclusion
So, where does this leave us? What are good use cases for this?
Obviously, if you're working a lot of NSManagedObject
's, this will
considerably slow down your code base. Also if you only have one or two
structs
, it is easier, more performant, and less magical if you simply
write a serialization technique yourself with the domain knowledge of
your individual struct
.
Rather, the reflection technique showcased here can be used if you have many, complicated structs, and you'd like to store some of those sometimes.
Examples would be:
- Setting Favorites
- Storing Bookmarks
- Staring Items
- Keeping the last selection
- Storing the ast open item across restarts
- Temporary storage of items during specific processes.
Apart from that, of course, you can also use reflection for other use cases:
- Iterate over tuples
- Analyze classes
- Runtime analysis of object conformance
- Converting to / from JSON (or other types)
- Generated detailed logging / debugging information automatically (i.e. for externally generated objects)
More Information
The source documentation of the Reflection API is very detailed. I'd encourage everyone to have a look at that as well.
Also, there's a much more exhaustive implementation of the techniques showcased here in the CoreValue project on GitHub which allows you to easily encode and decode from / to Structs to CoreData.
Similar Articles |
---|