Smoke and Mirror()
@attheodo
A short story on reflection with Swift
Reflection?
Reflection?
Type Introspection
Examine the type and properties of an object at runtime
let vc = UIViewController()
print(vc is UIViewController) // true
print(vc.view is UIView) // true
Reflection
If Type Introspection allows you to inspect an object's attributes at runtime, then Reflection is what allows you to manipulate those attributes at runtime.
More concretely, Reflection is the ability of a computer program to examine and modify its structure and behaviour at runtime.
Reflection?
Interlude: GraphQL Union Types
Imagine we implement some kind of search in our GraphQL schema:
query {
search(text: "cat") {
???
}
}
If we knew we only search for animals:
query {
search(text: "cat") {
animal {
animal_type
}
}
}
But... we want our search query to bring everything related to our keyword.... 🤔
Reflection?
Interlude: GraphQL Union Types
Union types 👌
query {
search(text: "cat") {
... on Animal {
animal_type
}
... on Nicknames {
nickname
}
... on Superheroes {
name
}
}
}
Reflection?
The Story begins...
Reflection?
GraphQL Query
fragment NotificationData on NotificationTypes {
... on IRecord {
id
created_at
}
... on INotification {
message
category
resolved
read
}
... on NewMatchInCurrentlyJoinedAlbumNotification {
matched_photos(slice: { offset: 0, limit: 1}) {
...PhotoDetails
}
albumMembership {
...AlbumMembershipDetails
}
}
... on UsersJoinedAlbumNotification {
new_members {
id
}
albumMembership {
...AlbumMembershipDetails
}
}
... on UsersUploadedToAlbumNotification {
photos(slice: { offset: 0, limit: 1 }) {
uploader {
id
}
}
albumMembership {
...AlbumMembershipDetails
}
}
}
implement
Reflection?
The generated NotificationData Object
public struct NotificationData: GraphQLNamedFragment {
public let asNewMatchInCurrentlyJoinedAlbumNotification: AsNewMatchInCurrentlyJoinedAlbumNotification?
public let asUsersJoinedAlbumNotification: AsUsersJoinedAlbumNotification?
public let asUsersUploadedToAlbumNotification: AsUsersUploadedToAlbumNotification?
public struct AsNewMatchInCurrentlyJoinedAlbumNotification: GraphQLConditionalFragment {
public let id: String
public let createdAt: String?
public let message: String?
public let category: NotificationCategories?
public let resolved: Bool?
public let read: Bool?
[... rest of the properties in the fragment ...]
}
public struct AsUsersJoinedAlbumNotification: GraphQLConditionalFragment {
public let id: String
public let createdAt: String?
public let message: String?
public let category: NotificationCategories?
public let resolved: Bool?
public let read: Bool?
[... rest of the properties in the fragment ...]
}
public struct AsUsersUploadedToAlbumNotification: GraphQLConditionalFragment {
public let id: String
public let createdAt: String?
public let message: String?
public let category: NotificationCategories?
public let resolved: Bool?
public let read: Bool?
[... rest of the properties in the fragment ...]
}
}
Reflection?
The Scaffold
class NotificationTableViewCell: UITableViewCell {
open var notification: NotificationData? {
didSet {
setDisplayValues()
}
}
}
class NewUploadsNotificationCell: NotificationTableViewCell
class UsersJoinedAlbumNotificationCell: NotificationTableViewCell
class InboundInvitationNotificationCell: NotificationTableViewCell
class NewMatchesNotificationCell: NotificationTableViewCell
Reflection?
Dealing with NotificationData
We wanted to access the common properties id, message, resolved, read in a convenient way
extension NotificationData {
public func getNotificationId() -> String? {
if let id = self.asUsersJoinedAlbumNotification?.id {
return id
}
if let id = self.asNewMatchInCurrentlyJoinedAlbumNotification?.id {
return id
}
if let id = self.asUsersUploadedToAlbumNotification?.albumMembership?.id {
return id
}
if let id = self.asInboundAlbumInvitationNotification?.id {
return id
}
return nil
}
}
😱
Reflection?
Problems with that approach
-
Repetition
-
Nasty if statements for each common property
-
-
Not Scalable
-
Every time a new notification type is created we need to revise each method in the extension to add a new if-statement to cover it
-
-
Ugly. Too damn ugly.
Â
Reflection?
🤔
"Use the bird Luke..."
Reflection?
Reflection in Swift
Swift reflection is based around a struct called Mirror. Your create a mirror for a particular subject, and then the mirror allows you to query it.
init(reflecting: Any)
Compatible with struct, class, enum, Tuple, Array, Dictionary etc.
let children: Mirror.Children
var customMirror: Mirror
var description: String
let displayStyle: Mirror.DisplayStyle?
let subjectType: Any.Type
var superclass:Mirror: Mirror?
Reflection?
Querying NotificationData using Mirrors
extension NotificationData {
public func objectForKey(key: String) -> AnyObject? {
let notificationUnionTypes = [
"asNewMatchInCurrentlyJoinedAlbumNotification",
"asUsersJoinedAlbumNotification",
"asUsersUploadedToAlbumNotification",
"asInboundAlbumInvitationNotification"
]
let mirror = Mirror(reflecting: self)
for case let (label?, value) in mirror.children {
if notificationUnionTypes.contains(label) {
let unionTypeMirror = Mirror(reflecting: value)
for case let (label?, value) in unionTypeMirror.children {
let objectMirror = Mirror(reflecting: value)
for case let (label?, value) in objectMirror.children {
if key == label {
return value as AnyObject
}
}
}
}
}
return nil
}
}
Reflection?
Querying NotificationData using Mirrors
/// Returns the notification id
public func getId() -> String? {
return self.objectForKey(key: "id") as? String
}
/// Returns the notification category
public func getCategory() -> NotificationCategories? {
if let category = self.objectForKey(key: "category") as? NotificationCategories {
return category
} else {
return nil
}
}
/// Returns whether the notification is read or not
public func isRead() -> Bool {
if let isRead = self.objectForKey(key: "read") as? Bool {
return isRead
} else {
return false
}
}
/// Returns whether a notificatio has been resolved (user cleared its associated action)
public func isResolved() -> Bool {
if let isResolved = self.objectForKey(key: "resolved") as? Bool {
return isResolved
} else {
return false
}
}
Reflection?
Querying NotificationData using Mirrors
Reflection?
The Bad
-
We lost compile-time type safety.
-
Mirrors have some performance penalties.
-
Not very trivial to drill down too much into objects
Reflection?
More on Mirror()
-
https://appventure.me/2015/10/24/swift-reflection-api-what-you-can-do/
Questions?
@attheodo
http://attheo.do
Smoke & Mirror()
By Thanos Theodoridis
Smoke & Mirror()
- 367