Manual mapping of value types

Reflection in Swift

Alexander Cyon
Make It New
Published in
5 min readOct 4, 2016

--

Reflection in Swift is easy using the struct Mirror, with it we can inspect the names and types of properties in an instance of a struct or an instance of a class (soon you’ll see why it was important to bold the words).

Imagine we have a struct called Book which looks like this:

struct Book {
let title: String
let author: String?
let published: Date
let numberOfPages: Int
let chapterCount: Int?
let genres: [String]
}

Then we can create an instance of Book, let’s create Harry Potter!

let book = Book(title: "Harry Potter", author: "J.K. Rowling", published: Date(), numberOfPages: 450, chapterCount: 19, genres: ["Fantasy", "Best books ever"])

And then it is super easy to look at the properties if the Harry Potter book instance using the Mirror struct.

Mirror

let bookMirror = Mirror(reflecting: book)for (name, value) in bookMirror.children {
guard let name = name else { continue }
print("\(name): \(type(of: value)) = '\(value)'")
}
// which prints:
// title: String = 'Harry Potter'
// author: Optional<String> = 'Optional("J.K. Rowling")'
// published: Date = '2016-10-02 19:17:43 +0000'
// numberOfPages: Int = '450'
// chapterCount: Optional<Int> = 'Optional(19)'
// genres: Array<String> = '["Fantasy", "Best books ever"]'

It is super simple to iterate through the properties of the harryPotter book instance using the property called children on the mirror.

In the print above we also see which properties are Optional. Please note that Mirror works exactly as well if Book would have been a class instead of a struct.

But what if we do not have and instance?

let bookClassMirror = Mirror(reflecting: Book.self)print(bookClassMirror.children.count)// which prints:
// ‘0’

So… Mirror does not work passing the type of Book. But there is a solution to this — Objective C to the rescue!

SwiftReflection

I have written an open source project currently called SwiftReflection, and it is available at GitHub. The project gives support for reflection if you don’t have an instance of a class. Here’s how it works.

You need to change Book into a class and let it inherit from NSObject. This is a strong requirement, I know! But if you are working with Core Data all your models will be inheriting from NSObject any way.

import ObjectiveC.runtime
class Book: NSObject {
let title: String
let author: String?
let published: Date
let numberOfPages: Int
let chapterCount: Int?
let genres: [String]
init(title: String, author: String?, published: Date, numberOfPages: Int, chapterCount: Int?, genres: [String] = []) {
self.title = title
self.author = author
self.published = published
self.numberOfPages = numberOfPages
self.chapterCount = chapterCount
self.genres = genres
}
}

We need to import ObjectiveC.runtime in order to use the method class_copyPropertyList like this:

func getTypesOfProperties(in clazz: NSObject.Type) -> Dictionary<String, Any>? {
var count = UInt32()
guard let properties = class_copyPropertyList(clazz, &count) else { return nil }
var types: Dictionary<String, Any> = [:]
for i in 0..<Int(count) {
guard
let property: objc_property_t = properties[i],
let name = getNameOf(property: property)
else { continue }
let type = getTypeOf(property: property)
types[name] = type
}
free(properties)
return types
}

So class_copyPropertyList returns an array of the peculiar objc_property_t, which allows us to retrieve the name and the type — but not without a little bit of work. The name is retrieved using the method getNameOf which I’ve written and it looks like this:

func getNameOf(property: objc_property_t) -> String? {
guard
let name: NSString = NSString(utf8String: property_getName(property))
else { return nil }
return name as String
}

Just a small wrapper using the method property_getName which takes an objc_property_t as a parameter. It is also nice to not have to work with that ugly NSString initialiser. The method getTypeOf is however a little bit more verbose.

func getTypeOf(property: objc_property_t) -> Any {
guard let attributesAsNSString: NSString = NSString(utf8String: property_getAttributes(property)) else { return Any.self }
let attributes = attributesAsNSString as String
let slices = attributes.components(separatedBy: "\"")
guard slices.count > 1 else { return valueType(withAttributes: attributes) }
let objectClassName = slices[1]
let objectClass = NSClassFromString(objectClassName) as! NSObject.Type
return objectClass
}

In order to understand what this method does and why we need to be looking what the method property_getAttributes returns (after being unwrapped):

T@"NSString",N,R,Vtitle
T@"NSString",N,R,Vauthor
T@"NSDate",N,R,Vpublished
Tq,N,R,VnumberOfPages
T@"NSArray",N,R,Vgenres

So we can see that title is an NSString, published is an NSDate, but numberOfPages is a what… q?

Take another look at the method getTypeOf, it extracts “NSString” from “T@”NSString”,N,R,Vtitle”

But how should we interpret “q” for the property numberOfPages which is an Int? The difference is that Int is a value type where String inside an NSObject subclass is just a typealias for NSString which is class inheriting from NSObject, a.k.a as a reference type. Also note that in the Objective C domain there are no things as optionals, so that is why title and author both are NSStrings, even though the author property was declared as an Optional String. So apparently value types are shown as just a char instead of a whole descriptive string.

I created a class containing all different kinds of value types I could come up with and using that you created a Dictionary mapping the char to the type:

let valueTypesMap: Dictionary<String, Any> = [
"c" : Int8.self,
"s" : Int16.self,
"i" : Int32.self,
"q" : Int.self, //also: Int64, NSInteger, only true on 64 bit platforms
"S" : UInt16.self,
"I" : UInt32.self,
"Q" : UInt.self, //also UInt64, only true on 64 bit platforms
"B" : Bool.self,
"d" : Double.self,
"f" : Float.self,
"{" : Decimal.self
]

Then we can write the following method used inside the getTypeOf method:

func valueType(withAttributes attributes: String) -> Any {
guard let letter = attributes.substring(from: 1, to: 2), let type = valueTypesMap[letter] else { return Any.self }
return type
}

Which makes use of the Dictionary shown above.

Now we can use the method getTypesOfProperties on the Book class:

if let types = getTypesOfProperties(in: Book.self) {
for (name, type) in types {
print(“‘\(name)’ is ‘\(type)’”)
}
}
// which prints:
// ‘published’ is ‘NSDate’
// ‘title’ is ‘NSString’
// ‘numberOfPages’ is ‘Int’
// ‘author’ is ‘NSString’
// ‘genres’ is ‘NSArray’

But what happened to happened to our optional property let chapterCount: Int? ?

Unfortunately the method class_copyPropertyList can’t (or won’t?) find properties of optional value types.

So what have we learned?

Mirror

Good: Easy to use, works with any class, struct and with optional value types!

Bad: Does not work without an instance of struct/class

SwiftReflection

Good: Works without an instance

Bad: Requires a class inheriting from NSObject. Doesn’t work with optional value types.

--

--

Cryptocurrency and DLT evangelist. Freelancing IT consultant and app developer in love with Swift.