Skip to content

Instantly share code, notes, and snippets.

@zaneclaes
Last active February 22, 2016 20:49
Show Gist options
  • Save zaneclaes/ade6baa241e7b91c9bb8 to your computer and use it in GitHub Desktop.
Save zaneclaes/ade6baa241e7b91c9bb8 to your computer and use it in GitHub Desktop.
Automatically detect changes to an object in Swift
//
// GameStatic.swift
// ForeverMaze
//
// Created by Zane Claes on 11/20/15.
// Copyright © 2015 inZania LLC. All rights reserved.
//
import SpriteKit
import Firebase
import PromiseKit
import CocoaLumberjack
private var KVOContext = 0
func getProperties(obj: AnyObject!, filter: ((String, String) -> (Bool))!) -> [String] {
return getClassProperties(object_getClass(obj), filter: filter)
}
func getClassProperties(klass: AnyClass!, filter: ((String, String) -> (Bool))!) -> [String] {
let superclass:AnyClass! = class_getSuperclass(klass)
var dynamicProperties = superclass == nil ? [String]() : getClassProperties(superclass, filter: filter)
guard klass != NSObject.self else {
return dynamicProperties
}
var propertyCount = UInt32(0)
let properties = class_copyPropertyList(klass, &propertyCount)
for var i = 0; i < Int(propertyCount); i++ {
let property = properties[i]
let propertyName = String(CString: property_getName(property), encoding: NSUTF8StringEncoding)!
// n.b., the `attributes` array should tell us if the property is dynamic
// Sadly it seems broken in Swift
// c.f., https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html
// We should have been able to look for `,D,` to indicate @dynamic
let attributes = String(CString: property_getAttributes(property), encoding: NSUTF8StringEncoding)!
if (filter(propertyName, attributes)) {
// Readonly property
continue
}
dynamicProperties.append(propertyName)
}
free(properties)
return dynamicProperties
}
class DynamicObject : NSObject {
let (loading, loadFulfill, loadReject) = Promise<GameStatic!>.pendingPromise()
let connection: Firebase!
var snapshot: FDataSnapshot!
var writing = Array<String>()
private var properties:Array<String>
init (firebasePath: String!) {
self.connection = firebasePath == nil ? nil : Firebase(url: firebasePath)
self.properties = []
super.init()
guard self.connection != nil else {
onLoaded(nil)
return
}
load()
}
func onLoaded(snapshot: FDataSnapshot!) {
loadFulfill(self)
}
//
// When we detect that a value is changed remotely, check if it differs from the
// local value. If so, trigger a method so subclasses can respond to the value change.
//
func load() {
self.connection.observeEventType(.Value, withBlock: { snapshot in
// Assign the initial variables from the snapshot:
self.snapshot = snapshot
if !self.loading.resolved {
for property in self.firebaseProperties {
// Assign the initial variable from the snapshot:
if snapshot.hasChild(property) {
let val = snapshot.childSnapshotForPath(property).value
self.setValue(val, forKey: property)
}
// Begin KVO:
self.addProperty(property)
}
self.onLoaded(snapshot)
}
else {
for property in self.firebaseProperties {
if self.writing.indexOf(property) != nil {
continue
}
let oldValue = self.valueForKey(property)!
if snapshot.hasChild(property) {
let newValue = snapshot.childSnapshotForPath(property).value!
if !newValue.isEqual(oldValue) {
DDLogDebug("\(self) [Remote Value Changed]: \(property) \(oldValue) -> \(newValue)")
self.setValue(newValue, forKey: property)
self.onPropertyChangedRemotely(property, oldValue: oldValue)
}
}
else if self.removeValue(property) {
self.onPropertyChangedRemotely(property, oldValue: oldValue)
}
}
}
})
// Basic timeout...
after(30).then { () -> Void in
if !self.loading.resolved {
DDLogWarn("[TIMEOUT] [FIREBASE-READ] \(self.connection.description)")
self.onLoaded(nil)
}
}
}
var isLoading:Bool {
return !self.loading.fulfilled
}
// This is a dangerous scenario. The server does not have a representation of this object. We're setting it to nil locally.
// However, some values may not be nil, and this could cause a crash. Or, if the key is an array, we may want to simply empty
// it out rather than nilling it.
func removeValue(property: String) -> Bool {
let val = self.valueForKey(property)
guard val != nil else {
return false
}
let type = "\(self.valueForKey(property)!.dynamicType)"
var newValue:AnyObject? = nil
if type.rangeOfString("NSArray") != nil {
newValue = []
}
else if type.rangeOfString("Number") != nil {
newValue = 0
}
else if type.rangeOfString("Bool") != nil {
newValue = false
}
if newValue == nil || !val!.isEqual(newValue) {
self.setValue(newValue, forKey: property)
return true
}
else {
return false
}
}
// Called when a remote change for a property is detected. Can be overwritten by children.
func onPropertyChangedRemotely(property: String, oldValue: AnyObject) {
}
// Properties which are explicitly not observed; can be overwritten in a subclass
var localProperties:[String] {
return ["snapshot","sprite","loaded","writing"]
}
var firebaseProperties:[String] {
return getProperties(self, filter: { (name, attributes) -> (Bool) in
if self.localProperties.contains(name) {
return true
}
// Not read-only implies writablitiy, or Q implies private (I think?)
return attributes.rangeOfString(",R") != nil && attributes.rangeOfString("Q,") == nil
})
}
func addProperty(property: String!) {
guard !properties.contains(property) else {
return
}
properties.append(property)
self.addObserver(self, forKeyPath: property, options: [.New, .Old], context: &KVOContext)
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if let property = keyPath,
newValue = change![NSKeyValueChangeNewKey],
oldValue = change![NSKeyValueChangeOldKey] {
if !newValue.isEqual(oldValue) {
var remoteChanged = true
if self.snapshot.hasChild(property) {
let remoteValue = self.snapshot.childSnapshotForPath(property).value!
remoteChanged = !newValue.isEqual(remoteValue)
}
if remoteChanged {
DDLogDebug("\(self) writing value \(property) from \(oldValue) to \(newValue) to Firebase")
self.writing.append(property)
self.connection.childByAppendingPath(property).write(newValue).then { () -> Void in
let idx = self.writing.indexOf(property)
if idx != nil {
self.writing.removeAtIndex(idx!)
}
}
}
}
}
}
func cleanup() {
for property in self.properties {
self.removeObserver(self, forKeyPath: property)
}
self.properties.removeAll()
if self.connection != nil {
self.connection.removeAllObservers()
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment