Last active
February 22, 2016 20:49
-
-
Save zaneclaes/ade6baa241e7b91c9bb8 to your computer and use it in GitHub Desktop.
Automatically detect changes to an object in Swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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