Downloading Tutorial

Downloads occur over a network. And networks fail. Especially if the user is on a mobile phone. Follow this tutorial to learn how to seamlessly handle network disconnections.

From the beginning

The ZeroDark framework notifies you when it has discovered new or modified nodes. You can use this opportunity to mark the node as "needs download".

/// ZeroDark has just discovered a new node in the cloud.
///
func didDiscoverNewNode(_ node: ZDCNode, at path: ZDCTreesystemPath, transaction: YapDatabaseReadWriteTransaction) {

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

   // Mark the node as "needs download".
   cloudTransaction.markNodeAsNeedsDownload(node.uuid, components: .all)

   // Try to download it now (may or may not succeed)
   downloadNode(node, at: path)
}

What you're doing here is setting a "needs download" flag on the node. Further, you're writing this flag to the database. So even if your app crashes, or the user restarts their phone, the flag will still be there on the next app launch.

Downloading

Downloading nodes is easy with the help of the DownloadManager:

let options = ZDCDownloadOptions()
options.cacheToDiskManager = false
options.canDownloadWhileInBackground = true

let queue = DispatchQueue.global()

zdc.downloadManager?.downloadNodeData( node,
                              options: options,
                      completionQueue: queue)
{ (cloudDataInfo: ZDCCloudDataInfo?, cryptoFile: ZDCCryptoFile?, error: Error?) in

   if let cloudDataInfo = cloudDataInfo,
      let cryptoFile = cryptoFile
    {
      // download succeeded
   } else {
      // download failed (maybe network disconnection ?)
   }
}

If the download succeeds, you can unmark the node as "needs download":

cloudTransaction.unmarkNodeAsNeedsDownload(node.uuid, components: .all, ifETagMatches: cloudDataInfo.eTag)

// If you only downloaded the metadata section, you can specify that:
cloudTransaction.unmarkNodeAsNeedsDownload(node.uuid, components: .metadata, ifETagMatches: cloudDataInfo.eTag)

// The "needs download" is actually a bitmask:
// - header    = 00001
// - metadata  = 00010
// - thumbnail = 00100
// - data      = 01000
//
// So if we unset only the metadata flag, the other flags remain set.

There's also a simple one-liner you can use to cleanup your downloads:

// Downloads are stored into a temporary directory.
// The OS may delete files from the directory for us,
// but it's better if we cleanup after ourselves.
// 
// This simple one-liner is all you need:
zdc.diskManager?.deleteFileIfUnmanaged(cryptoFile.fileURL)

// Translation: I'm done processing the download file.
// Delete it, unless it's being managed by the DiskManager.

If the download failed due to a network disconnection, then we want to restart the download when network connectivity is restored.

Network Notifications

ZeroDark comes with a built-in solution for receiving notifications when "network reachability" changes. This is a thin-layer atop what the Apple frameworks already provide:

// If the user gets disconnected from the Internet,
// then we may need to restart some downloads after they get reconnected.
//
// We setup a closure to do that here.
zdc.reachability.setReachabilityStatusChange {[weak self] (status: AFNetworkReachabilityStatus) in

   if status == .reachableViaWiFi || status == .reachableViaWWAN {

      self?.downloadMissingOrOutdatedNodes()
   }
}

Restarting Downloads

Once network connectivity is restored, you can restart any needed downloads. The general idea is:

  • walk the treesystem
  • check to see if any nodes are marked as "needs download"
  • if so, download those nodes as needed
func downloadMissingOrOutdatedNodes(localUserID: String) {

  let roConnection = zdc.databaseManager?.roDatabaseConnection
  roConnection?.asyncRead {(transaction) in

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

    // We want to enumerate every node in the database.
    // The NodeManager has a method that will do this for us.
    //
    // We're going to recursively enumerate every node
    // within the home container.
    // For example, if our treesystem looks like this:
    //
    //              (home)
    //              /    \
    //        (convoA)    (convoB)
    //       /   |   \        |
    //  (msg1)(msg2)(msg3)  (msg4)
    //
    // Then the recursiveEnumerate function would give us:
    // - ~/convoA
    // - ~/convoA/msg1
    // - ~/convoA/msg2
    // - ~/convoA/msg3
    // - ~/convoB
    // - ~/convoB/msg4

    zdc.nodeManager.recursiveIterateNodeIDs(withParentID: homeNode.uuid,
                                             transaction: transaction)
    {(nodeID: String, path: [String], recurseInto: inout Bool, stop: inout Bool) in

      if cloudTransaction.nodeIsMarkedAsNeedsDownload(nodeID, components: .all) {
        self.downloadNode(withNodeID: nodeID, transaction: transaction)
      }
    }   
  }
}