Mapping Tutorial

When writing your app, there's often a need to map from nodes your custom objects. Or vice-versa. The framework has several different tools to assist with this process.

ZDCNode != YourObject

With ZeroDark, you get to create your own custom objects. The framework doesn't force you to subclass some awkward base class. In fact, you're not explicitly told not to subclass ZDCNode.

Instead, you create nodes in a treesystem, and map those nodes to your own custom objects:

let treesystemPath = ZDCTreesystemPath(pathComponents: ["foo"])

let db = zdc.databaseManager!
db.rwDatabaseConnection.asyncReadWrite {(transaction) in

  guard let cloudTransaction = zdc.cloudTransaction(transaction, forLocalUserID: self.localUserID) else {
    return
  }

  do {
    // Create node
    let node = try cloudTransaction.createNode(withPath: treesystemPath)

    // Perform optional mapping here
  } catch {}

  // Node is now added to treesystem, and queued for upload
}

As discussed in the push article, the above code creates a node. And when the framework is ready to upload that node to the cloud, it will ask the ZeroDarkCloudDelegate for the data to upload:

/// ZeroDarkCloudDelegate protocol function.
/// Framework is asking us to supply the data for a node.
/// This is the data that will get uploaded to the cloud.
/// ZeroDark handles the encryption & uploading for us.
///
func data(for node: ZDCNode, at path: ZDCTreesystemPath, transaction: YapDatabaseReadTransaction) -> ZDCData {

  // Implement me !
}

When you implement this method, you'll see that you need a way to map from the given parameters (node & path) to your custom object(s). There are several different ways to do this.

Option 1: Simple path-based approach

It may be the case that mapping is simple because of how you've structured treesystem paths:

  • If path is /X => object type is Album, and X is AlbumID
  • If path is /X/Y => object type is Song, and X is AlbumID, and Y is SongID

Easy peasy, lemon squeezy. But don't feel pressured into this system. The best treesystem design for your app is the one that optimizes for the cloud (as discussed here).

Option 2: Linking

A common scenario is:

  • there's a one-to-one-mapping between a node and your own object
  • your own object is also stored in the same database

The ZeroDark framework uses a database internally. It needs atomic transactions to ensure syncing works smoothly. And for this task, it uses YapDatabase. And since this database is awesome, there's a chance you may decide to use it too. (You don't have to.)

When this is the case, you can use linking:

let db = zdc.databaseManager!
db.rwDatabaseConnection.asyncReadWrite {(transaction) in

  guard let cloudTransaction = zdc.cloudTransaction(transaction, forLocalUserID: self.localUserID) else {
     return
  }

  // Store your own object in the database
  transaction.setObject(album, forKey: album.id, inCollection: "albums")

  do {
    // Create node
    let node = try cloudTransaction.createNode(withPath: treesystemPath)

    // Link node to your own object
    try cloudTransaction.linkNodeID(node.uuid, toKey: album.id, inCollection: "albums")

  } catch {}

  // Node is now added to treesystem, and queued for upload.
  // Node is also linked to your album object.
}

Once linked, it becomes easy to map from a node to your custom object:

/// ZeroDarkCloudDelegate protocol function.
///
func data(for node: ZDCNode, at path: ZDCTreesystemPath, transaction: YapDatabaseReadTransaction) -> ZDCData {

  guard let cloudTransaction = zdc.cloudTransaction(transaction, forLocalUserID: node.localUserID) else {
    return ZDCData()
  }

  let linked = cloudTransaction.linkedObject(forNodeID: node.uuid)

  if let conversation = linked as? Album {
    // Return cloud data for album...
  } else if let song = linked as? Song {
    // Return cloud data for song...
  }
}

And mapping in the opposite direction is just as easy:

let db = zdc.databaseManager!
db.rwDatabaseConnection.asyncReadWrite {(transaction) in

  guard let cloudTransaction = zdc.cloudTransaction(transaction, forLocalUserID: self.localUserID) else {
    return
  }

  // Save modified album to database
  transaction.setObject(album, forKey: album.id, inCollection: "albums")

  // Tell ZeroDark to upload the modified album
  if let nodeID = cloudTransaction.linkedNodeID(forKey: album.id, inCollection: "albums") {
    cloudTransaction.queueDataUpload(forNodeID: nodeID, withChangeset: nil)
  }
}

You can find the full set of API's for linking here.

Option 3: Tags

Linking works great in some scenarios, but it come with several restrictions:

  • a node can only be "linked" to a single custom object
  • a custom object can only be "linked" to a single node
  • this only works for custom objects in the same database

When you need something more flexible, you can use tags. Tags allow you to associate arbitrary key/value pairs with a given node:

let db = zdc.databaseManager!
db.rwDatabaseConnection.asyncReadWrite {(transaction) in

  guard let cloudTransaction = zdc.cloudTransaction(transaction, forLocalUserID: self.localUserID) else {
     return
  }

  do {
    // Create node
    let node = try cloudTransaction.createNode(withPath: treesystemPath)

    // Tag the node, so we can map it to our Album
    cloudTransaction.setTag(album.id, forNodeID: node.uuid, withIdentifier: "albumID")

  } catch {}

  // Node is now added to treesystem, and queued for upload.
  // Node has "albumID" tag associated with it.
}

Now we can read that tag to perform our mapping:

/// ZeroDarkCloudDelegate protocol function.
///
func data(for node: ZDCNode, at path: ZDCTreesystemPath, transaction: YapDatabaseReadTransaction) -> ZDCData {

  guard let cloudTransaction = zdc.cloudTransaction(transaction, forLocalUserID: node.localUserID) else {
    return ZDCData()
  }

  if let albumID = cloudTransaction.tag(forNodeID: node.uuid, withIdentifier: "albumID") as? String {
    // Return cloud data for album...
  }
}

Option #4: Exploiting node.uuid

Every node has a uuid, which is a string (generated via NSUUID). And this value never changes. So you could simply store this value in your own custom object:

let db = zdc.databaseManager!
db.rwDatabaseConnection.asyncReadWrite {(transaction) in

  guard let cloudTransaction = zdc.cloudTransaction(transaction, forLocalUserID: self.localUserID) else {
     return
  }

  let node: ZDCNode!
  do {
    // Create node
    node = try cloudTransaction.createNode(withPath: treesystemPath)

  } catch {
    return
  }

  album.nodeID = node.uuid
}

Summary

Choose whichever option is easiest for your situation. And keep in mind that you can mix-and-match these options.