Swift Tutorial: Real-Time Data Synchronization with Firebase

This is part of a free course that teaches game design and the Swift programming language. It contains examples from a real game I created called ForeverMaze. The full course includes the source code for the whole game.

In this tutorial, I’ll introduce a data class that “magically” synchronizes in real-time via Firebase. You interface with it like any normal local object, but it actually is always in-sync with the server. We’ll learn about intermediate topics in Swift like KVO and Reflection.

For example, if I add a new variable in my subclass, dynamic var health:Int = 0 , the value is atomically initialized with the value from the server. Any time I change the value locally, it is immediately written to the server. Any time the server value is changed, my local value is instantly changed. There’s no setup required, other than adding the dynamic keyword. Sound too good to be true? It isn’t.

Watch the video to get the full explanation.

Download DynamicObject.Swift

 

Requirements

You’ll need the Firebase, PromiseKit and (optionally) CocoaLumberjack cocoa pods.

pod 'Firebase', '>= 2.4.3'  
pod "PromiseKit", ">= 3.0"
pod "CocoaLumberjack/Swift"

Explaining how PromiseKit works is outside the scope of this article, but you’ll see in the last section how it makes the syntax very clean and easy to use.

Using Reflection

The hardest part of this problem is simply figuring out what variables, in a class, are the ones we want to synchronize with the server. To do this, I used a technique called “reflection” to try to find out what variables are dynamic.

Note: due to what appears to be a bug in the Swift language, this function is sub-perfect. See the comments for details.

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
}

This code accepts a filter function to run. So in practice, it’s used like this to create a variable on our DynamicObject  class.

  var firebaseProperties:[String] {
    return getProperties(self, filter: { (name, attributes) -> (Bool) in
      // Not read-only implies writablitiy, or Q implies private
      return attributes.rangeOfString(",R") != nil && attributes.rangeOfString("Q,") == nil
    })
  }

Again, we should have been able to look for D instead of Q or R, which would have made this code only work with dynamic properties, but this does not appear to presently work correctly as of the writing of this article.

Initializing and Watching for Changes

When we initialize a DynamicObject, we need to provide a path to the Firebase object. The first thing we do is to create a connection object, and finally call the load function:

init (firebasePath: String!) {
    self.connection = firebasePath == nil ? nil : Firebase(url: firebasePath)
    // ... other code
    load()
}

The load function adds an observer to the connection object, which immediately begins watching for changes. As soon as data is retrieved, it goes through all of the firebaseProperties  (from the above section) and assigns the value from the server to the local instance. Once it’s done, the isLoading  boolean will be false, and all data on the object is available.

If subsequent changes are detected, the load function continues to copy these changes to the local instance of the object. When the value changes, it invokes a local function onPropertyChangedRemotely .

KVO and Writing Changes to Firebase

I used KVO, or Key-Value Observation, to automatically detect when the value was changed locally and then copy these changes to the server. If you look at the observeValueForKeyPath function, it’s looking for any (local) change of the value. When this happens, it immediately writes the value to Firebase.

Putting it All Together

One sample case where we might want “dynamic” objects is a player, who has some health.

class Player : DynamicObject {
  let playerID: String!

  dynamic var health:Int = 0
  
  init (playerID: String!) {
    self.playerID = playerID
    super.init(firebasePath: playerID == nil ? nil : "/players/\(playerID)")
  }

  override func onPropertyChangedRemotely(property: String, oldValue: AnyObject) {
    if property == "health" {
      // the health was changed!
    }
  }
}

Now we can load the player and wait for it to be ready:

let player = Player("1234")
player.loading.then { (snapshot) -> Void in
  // ... do something
}

After this code runs, if the remote value of the health is changed (such as another player “hurting” this player), the onPropertyChangedRemotely  function will be called. And if we want to change the player’s health, so that the new value is sent to all other clients, we can simply do:

player.health -= 1

And viola! All other clients have the onPropertyChangedRemotely  function called right away.

Conclusion

Hopefully you see how powerful and useful it is to have a base class that automatically synchronizes data with minimal setup work!

If you enjoyed this post, why not enroll in the free course? It contains hours of video content, source code downloads, examples and more. I cover many different intermediate and advanced topics for creating a real-time game in Swift with SpriteKit and Firebase. You can check out the complete game at ForeverMaze.com.

[author]

Leave A Comment

You must be logged in to post a comment.

Back to Top