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

Made with Slides.com