swift: Update todos app with working model

Takes UX shell and plumbs it with Syncbase and a live data model.
Uses a central watchpoint and replicates the data in-memory, dispatching
notifications to screens of changes. Also saves the Google Sign In URL
for photos. This will be a useful hook once we figure out a strategy
for querying all member photos or advertising such urls in discovery.

Invite is not yet implemented, but syncing across a given user's
device is working (at least from a Swift perspective, various errors
in joining syncgroups and crashes are occuring in the Go layer).

Other fixes:

- Allow hashValue computation of Identifier to overflow rather than
  panic
- Disable bitcode on todos

Change-Id: Iafc8d5418661a39fa7abb14e8abe43c1801cfa34
diff --git a/Syncbase/Source/Database.swift b/Syncbase/Source/Database.swift
index 19155df..80611ba 100644
--- a/Syncbase/Source/Database.swift
+++ b/Syncbase/Source/Database.swift
@@ -37,20 +37,20 @@
 public struct WatchChangeHandler: Hashable, Equatable {
   /// Called once, when a watch change handler is added, to provide the initial state of the
   /// values being watched.
-  public let onInitialState: [WatchChange] -> Void
+  public let onInitialState: ([WatchChange] -> Void)?
 
   /// Called whenever a batch of changes is committed to the database. Individual puts/deletes
   /// surface as a single-change batch.
-  public let onChangeBatch: [WatchChange] -> Void
+  public let onChangeBatch: ([WatchChange] -> Void)?
 
   /// Called when an error occurs while watching for changes. Once `onError` is called,
   /// no other methods will be called on this handler.
-  public let onError: ErrorType -> Void
+  public let onError: (ErrorType -> Void)?
 
   public init(
-    onInitialState: [WatchChange] -> Void,
-    onChangeBatch: [WatchChange] -> Void,
-    onError: ErrorType -> Void) {
+    onInitialState: ([WatchChange] -> Void)?,
+    onChangeBatch: ([WatchChange] -> Void)?,
+    onError: (ErrorType -> Void)?) {
       self.onInitialState = onInitialState
       self.onChangeBatch = onChangeBatch
       self.onError = onError
@@ -432,11 +432,11 @@
                 // mitigate against out-of-order events should Syncbase.queue be a concurrent queue
                 // rather than a serial queue.
                 dispatch_sync(Syncbase.queue, {
-                  handler.onInitialState(batch)
+                  handler.onInitialState?(batch)
                 })
               } else if !batch.isEmpty {
                 dispatch_sync(Syncbase.queue, {
-                  handler.onChangeBatch(batch)
+                  handler.onChangeBatch?(batch)
                 })
               }
               batch.removeAll()
@@ -445,7 +445,7 @@
           // Notify of error if we're permitted (not canceled).
           if var err = stream.err() where !isCanceled {
             dispatch_sync(Syncbase.queue, {
-              handler.onError(SyncbaseError(coreError: err))
+              handler.onError?(SyncbaseError(coreError: err))
             })
           }
           // Cleanup
diff --git a/Syncbase/Source/Error.swift b/Syncbase/Source/Error.swift
index 7c9d998..73c2463 100644
--- a/Syncbase/Source/Error.swift
+++ b/Syncbase/Source/Error.swift
@@ -20,7 +20,7 @@
   case ReadOnlyBatch
   case ConcurrentBatch
   case BlobNotCommitted
-  case SyncgroupJoinFailed
+  case SyncgroupJoinFailed(detail: String)
   case BadExecStreamHeader
   case InvalidPermissionsChange
   case Exist
@@ -46,7 +46,7 @@
     case .ReadOnlyBatch: self = .ReadOnlyBatch
     case .ConcurrentBatch: self = .ConcurrentBatch
     case .BlobNotCommitted: self = .BlobNotCommitted
-    case .SyncgroupJoinFailed: self = .SyncgroupJoinFailed
+    case .SyncgroupJoinFailed(let detail): self = .SyncgroupJoinFailed(detail: detail)
     case .BadExecStreamHeader: self = .BadExecStreamHeader
     case .InvalidPermissionsChange: self = .InvalidPermissionsChange
     case .Exist: self = .Exist
@@ -92,7 +92,7 @@
     case .ReadOnlyBatch: return "Batch is read-only"
     case .ConcurrentBatch: return "Concurrent batch"
     case .BlobNotCommitted: return "Blob is not yet committed"
-    case .SyncgroupJoinFailed: return "Syncgroup join failed"
+    case .SyncgroupJoinFailed(let detail): return "Syncgroup join failed: \(detail)"
     case .BadExecStreamHeader: return "Exec stream header improperly formatted"
     case .InvalidPermissionsChange: return "The sequence of permission changes is invalid"
     case .Exist: return "Already exists"
diff --git a/Syncbase/Source/Id.swift b/Syncbase/Source/Id.swift
index 8d173c7..9a19864 100644
--- a/Syncbase/Source/Id.swift
+++ b/Syncbase/Source/Id.swift
@@ -41,11 +41,10 @@
   }
 
   public var hashValue: Int {
-    // Note: Copied from VDL.
     var result = 1
     let prime = 31
-    result = prime * result + blessing.hashValue
-    result = prime * result + name.hashValue
+    result = prime &* result &+ blessing.hashValue
+    result = prime &* result &+ name.hashValue
     return result
   }
 
diff --git a/Syncbase/Source/Syncbase.swift b/Syncbase/Source/Syncbase.swift
index 9f39cda..68e8da9 100644
--- a/Syncbase/Source/Syncbase.swift
+++ b/Syncbase/Source/Syncbase.swift
@@ -26,7 +26,6 @@
   static var disableSyncgroupPublishing = false
   static var disableUserdataSyncgroup = false
   static var mountPoints = ["/ns.dev.v.io:8101/tmp/todos/users/"]
-  static var rootDir = Syncbase.defaultRootDir
   /// Queue used to dispatch all asynchronous callbacks. Defaults to main.
   public static var queue: dispatch_queue_t {
     // Map directly to SyncbaseCore.
@@ -84,7 +83,6 @@
         throw SyncbaseError.AlreadyConfigured
       }
       Syncbase.adminUserId = adminUserId
-      Syncbase.rootDir = rootDir
       Syncbase.mountPoints = mountPoints
       Syncbase.defaultBlessingStringPrefix = defaultBlessingStringPrefix
       Syncbase.disableSyncgroupPublishing = disableSyncgroupPublishing
@@ -94,7 +92,7 @@
       // We don't need to set Syncbase.queue as it is a proxy for SyncbaseCore's queue, which is
       // set in the configure below.
       try SyncbaseError.wrap {
-        try SyncbaseCore.Syncbase.configure(rootDir: Syncbase.rootDir, queue: queue)
+        try SyncbaseCore.Syncbase.configure(rootDir: rootDir, queue: queue)
       }
       // We use SyncbaseCore's isLoggedIn because this frameworks would fail as didInit hasn't
       // been set to true yet.
@@ -161,13 +159,16 @@
       guard let row = change.row where change.entityType == .Row && change.changeType == .Put else {
         continue
       }
+      guard let syncgroupId = try? Identifier.decode(
+        row.stringByReplacingOccurrencesOfString(Syncbase.UserdataCollectionPrefix, withString: "")) else {
+          print("Syncbase - Unable to decode userdata key: (row)")
+          continue
+      }
       do {
-        let syncgroupId = try Identifier.decode(
-          row.stringByReplacingOccurrencesOfString(Syncbase.UserdataCollectionPrefix, withString: ""))
         let syncgroup = try Syncbase.database().syncgroup(syncgroupId)
         try syncgroup.join()
-      } catch let e {
-        NSLog("Syncbase - Error joining syncgroup: %@", "\(e)")
+      } catch {
+        NSLog("Syncbase - Error joining syncgroup \(syncgroupId): %@", "\(error)")
       }
     }
   }
diff --git a/SyncbaseCore/Source/Errors.swift b/SyncbaseCore/Source/Errors.swift
index cb4d63d..7cb3c1f 100644
--- a/SyncbaseCore/Source/Errors.swift
+++ b/SyncbaseCore/Source/Errors.swift
@@ -15,7 +15,7 @@
   case ReadOnlyBatch
   case ConcurrentBatch
   case BlobNotCommitted
-  case SyncgroupJoinFailed
+  case SyncgroupJoinFailed(detail: String)
   case BadExecStreamHeader
   case InvalidPermissionsChange
   case Exist
@@ -41,7 +41,7 @@
     case "v.io/v23/services/syncbase.ReadOnlyBatch": self = SyncbaseError.ReadOnlyBatch
     case "v.io/v23/services/syncbase.ConcurrentBatch": self = SyncbaseError.ConcurrentBatch
     case "v.io/v23/services/syncbase.BlobNotCommitted": self = SyncbaseError.BlobNotCommitted
-    case "v.io/v23/services/syncbase.SyncgroupJoinFailed": self = SyncbaseError.SyncgroupJoinFailed
+    case "v.io/v23/services/syncbase.SyncgroupJoinFailed": self = SyncbaseError.SyncgroupJoinFailed(detail: err.msg)
     case "v.io/v23/services/syncbase.BadExecStreamHeader": self = SyncbaseError.BadExecStreamHeader
     case "v.io/v23/services/syncbase.InvalidPermissionsChange": self = SyncbaseError.InvalidPermissionsChange
     case "v.io/v23/verror.Exist": self = SyncbaseError.Exist
@@ -63,7 +63,7 @@
     case .ReadOnlyBatch: return "Batch is read-only"
     case .ConcurrentBatch: return "Concurrent batch"
     case .BlobNotCommitted: return "Blob is not yet committed"
-    case .SyncgroupJoinFailed: return "Syncgroup join failed"
+    case .SyncgroupJoinFailed(let detail): return "Syncgroup join failed: \(detail)"
     case .BadExecStreamHeader: return "Exec stream header improperly formatted"
     case .InvalidPermissionsChange: return "The sequence of permission changes is invalid"
     case .NoExist: return "Does not exist"
diff --git a/SyncbaseTodosApp/SyncbaseTodos.xcodeproj/project.pbxproj b/SyncbaseTodosApp/SyncbaseTodos.xcodeproj/project.pbxproj
index a6fea1c..eb728da 100644
--- a/SyncbaseTodosApp/SyncbaseTodos.xcodeproj/project.pbxproj
+++ b/SyncbaseTodosApp/SyncbaseTodos.xcodeproj/project.pbxproj
@@ -25,6 +25,7 @@
 		931F8BD31D19258800B97CF3 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 931F8BD21D19258800B97CF3 /* StoreKit.framework */; };
 		931F8BD51D19258F00B97CF3 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 931F8BD41D19258F00B97CF3 /* SystemConfiguration.framework */; };
 		931F8BD71D19290B00B97CF3 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 931F8BD61D19290B00B97CF3 /* LoginViewController.swift */; };
+		93213FEF1D1C8BB100297838 /* Syncbase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93213FEE1D1C8BB100297838 /* Syncbase.swift */; };
 		932AD4E91D1A74A8000EA038 /* GoogleAppUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 931F8BB31D19254300B97CF3 /* GoogleAppUtilities.framework */; };
 		932AD4EA1D1A74A8000EA038 /* GoogleAuthUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 931F8BB41D19254300B97CF3 /* GoogleAuthUtilities.framework */; };
 		932AD4EB1D1A74A8000EA038 /* GoogleNetworkingUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 931F8BB51D19254300B97CF3 /* GoogleNetworkingUtilities.framework */; };
@@ -35,6 +36,10 @@
 		932AD4FE1D1B202B000EA038 /* Syncbase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 938DEA941D10DA4A003C9734 /* Syncbase.framework */; };
 		932AD4FF1D1B202B000EA038 /* Syncbase.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 938DEA941D10DA4A003C9734 /* Syncbase.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		932AD5041D1B20C5000EA038 /* SafariServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 932AD5031D1B20C5000EA038 /* SafariServices.framework */; };
+		932AD50E1D1BCFD3000EA038 /* Marshal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 932AD50D1D1BCFD3000EA038 /* Marshal.swift */; };
+		93824A7F1D2491A200F4851B /* SyncbaseCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 93824A7C1D24919600F4851B /* SyncbaseCore.framework */; };
+		93824A801D2491A200F4851B /* SyncbaseCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 93824A7C1D24919600F4851B /* SyncbaseCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		93C462B01D2F108F00394BAB /* Dispatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93C462AF1D2F108F00394BAB /* Dispatch.swift */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -45,6 +50,27 @@
 			remoteGlobalIDString = 9374F6731D010711004ECE59;
 			remoteInfo = Syncbase;
 		};
+		93824A7B1D24919600F4851B /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 93824A751D24919600F4851B /* SyncbaseCore.xcodeproj */;
+			proxyType = 2;
+			remoteGlobalIDString = 30AD2DFF1CDD506700A28A0C;
+			remoteInfo = SyncbaseCore;
+		};
+		93824A7D1D24919600F4851B /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 93824A751D24919600F4851B /* SyncbaseCore.xcodeproj */;
+			proxyType = 2;
+			remoteGlobalIDString = 30AD2E091CDD506700A28A0C;
+			remoteInfo = SyncbaseCoreTests;
+		};
+		93824A811D2491A200F4851B /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 93824A751D24919600F4851B /* SyncbaseCore.xcodeproj */;
+			proxyType = 1;
+			remoteGlobalIDString = 30AD2DFE1CDD506700A28A0C;
+			remoteInfo = SyncbaseCore;
+		};
 		938DEA931D10DA4A003C9734 /* PBXContainerItemProxy */ = {
 			isa = PBXContainerItemProxy;
 			containerPortal = 938DEA8D1D10DA4A003C9734 /* Syncbase.xcodeproj */;
@@ -69,6 +95,7 @@
 			dstSubfolderSpec = 10;
 			files = (
 				932AD4FF1D1B202B000EA038 /* Syncbase.framework in Embed Frameworks */,
+				93824A801D2491A200F4851B /* SyncbaseCore.framework in Embed Frameworks */,
 			);
 			name = "Embed Frameworks";
 			runOnlyForDeploymentPostprocessing = 0;
@@ -102,9 +129,13 @@
 		931F8BD21D19258800B97CF3 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
 		931F8BD41D19258F00B97CF3 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; };
 		931F8BD61D19290B00B97CF3 /* LoginViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = "<group>"; };
+		93213FEE1D1C8BB100297838 /* Syncbase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Syncbase.swift; sourceTree = "<group>"; };
 		932AD4F11D1B1050000EA038 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "SyncbaseTodos/GoogleService-Info.plist"; sourceTree = "<group>"; };
 		932AD5031D1B20C5000EA038 /* SafariServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SafariServices.framework; path = System/Library/Frameworks/SafariServices.framework; sourceTree = SDKROOT; };
+		932AD50D1D1BCFD3000EA038 /* Marshal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Marshal.swift; sourceTree = "<group>"; };
+		93824A751D24919600F4851B /* SyncbaseCore.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = SyncbaseCore.xcodeproj; path = ../SyncbaseCore/SyncbaseCore.xcodeproj; sourceTree = "<group>"; };
 		938DEA8D1D10DA4A003C9734 /* Syncbase.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Syncbase.xcodeproj; path = ../Syncbase/Syncbase.xcodeproj; sourceTree = "<group>"; };
+		93C462AF1D2F108F00394BAB /* Dispatch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dispatch.swift; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -124,6 +155,7 @@
 				931F8BD31D19258800B97CF3 /* StoreKit.framework in Frameworks */,
 				931F8BD11D19258300B97CF3 /* AddressBook.framework in Frameworks */,
 				931F8BCF1D19257600B97CF3 /* libz.tbd in Frameworks */,
+				93824A7F1D2491A200F4851B /* SyncbaseCore.framework in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -133,9 +165,8 @@
 		3783985E1CF3ADB000C35566 = {
 			isa = PBXGroup;
 			children = (
-				938DEA8D1D10DA4A003C9734 /* Syncbase.xcodeproj */,
 				37B018BA1CFF7A9D004B0A15 /* SyncbaseTodos */,
-				931F8BC11D19254700B97CF3 /* Google Sign-In */,
+				93824A831D2491AC00F4851B /* Frameworks */,
 				378398681CF3ADB000C35566 /* Products */,
 			);
 			sourceTree = "<group>";
@@ -169,6 +200,8 @@
 				37B018C41CFF7A9D004B0A15 /* Person.swift */,
 				37B018C51CFF7A9D004B0A15 /* Task.swift */,
 				37B018C71CFF7A9D004B0A15 /* TodoList.swift */,
+				932AD50D1D1BCFD3000EA038 /* Marshal.swift */,
+				93213FEE1D1C8BB100297838 /* Syncbase.swift */,
 			);
 			name = Model;
 			sourceTree = "<group>";
@@ -177,6 +210,7 @@
 			isa = PBXGroup;
 			children = (
 				37B018C11CFF7A9D004B0A15 /* CircularImageView.swift */,
+				93C462AF1D2F108F00394BAB /* Dispatch.swift */,
 				37B018C31CFF7A9D004B0A15 /* MemberView.swift */,
 			);
 			name = Util;
@@ -213,6 +247,25 @@
 			name = "Google Sign-In";
 			sourceTree = "<group>";
 		};
+		93824A761D24919600F4851B /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				93824A7C1D24919600F4851B /* SyncbaseCore.framework */,
+				93824A7E1D24919600F4851B /* SyncbaseCoreTests.xctest */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		93824A831D2491AC00F4851B /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+				931F8BC11D19254700B97CF3 /* Google Sign-In */,
+				938DEA8D1D10DA4A003C9734 /* Syncbase.xcodeproj */,
+				93824A751D24919600F4851B /* SyncbaseCore.xcodeproj */,
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
 		938DEA8E1D10DA4A003C9734 /* Products */ = {
 			isa = PBXGroup;
 			children = (
@@ -238,6 +291,7 @@
 			);
 			dependencies = (
 				932AD5011D1B202B000EA038 /* PBXTargetDependency */,
+				93824A821D2491A200F4851B /* PBXTargetDependency */,
 			);
 			name = SyncbaseTodos;
 			productName = VanadiumTodos;
@@ -275,6 +329,10 @@
 					ProductGroup = 938DEA8E1D10DA4A003C9734 /* Products */;
 					ProjectRef = 938DEA8D1D10DA4A003C9734 /* Syncbase.xcodeproj */;
 				},
+				{
+					ProductGroup = 93824A761D24919600F4851B /* Products */;
+					ProjectRef = 93824A751D24919600F4851B /* SyncbaseCore.xcodeproj */;
+				},
 			);
 			projectRoot = "";
 			targets = (
@@ -284,6 +342,20 @@
 /* End PBXProject section */
 
 /* Begin PBXReferenceProxy section */
+		93824A7C1D24919600F4851B /* SyncbaseCore.framework */ = {
+			isa = PBXReferenceProxy;
+			fileType = wrapper.framework;
+			path = SyncbaseCore.framework;
+			remoteRef = 93824A7B1D24919600F4851B /* PBXContainerItemProxy */;
+			sourceTree = BUILT_PRODUCTS_DIR;
+		};
+		93824A7E1D24919600F4851B /* SyncbaseCoreTests.xctest */ = {
+			isa = PBXReferenceProxy;
+			fileType = wrapper.cfbundle;
+			path = SyncbaseCoreTests.xctest;
+			remoteRef = 93824A7D1D24919600F4851B /* PBXContainerItemProxy */;
+			sourceTree = BUILT_PRODUCTS_DIR;
+		};
 		938DEA941D10DA4A003C9734 /* Syncbase.framework */ = {
 			isa = PBXReferenceProxy;
 			fileType = wrapper.framework;
@@ -328,8 +400,11 @@
 				37B018D41CFF7A9D004B0A15 /* TodosViewController.swift in Sources */,
 				37B018D01CFF7A9D004B0A15 /* Person.swift in Sources */,
 				37B018CD1CFF7A9D004B0A15 /* CircularImageView.swift in Sources */,
+				932AD50E1D1BCFD3000EA038 /* Marshal.swift in Sources */,
+				93213FEF1D1C8BB100297838 /* Syncbase.swift in Sources */,
 				931F8BD71D19290B00B97CF3 /* LoginViewController.swift in Sources */,
 				37B018D21CFF7A9D004B0A15 /* TasksViewController.swift in Sources */,
+				93C462B01D2F108F00394BAB /* Dispatch.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -341,6 +416,11 @@
 			name = Syncbase;
 			targetProxy = 932AD5001D1B202B000EA038 /* PBXContainerItemProxy */;
 		};
+		93824A821D2491A200F4851B /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			name = SyncbaseCore;
+			targetProxy = 93824A811D2491A200F4851B /* PBXContainerItemProxy */;
+		};
 /* End PBXTargetDependency section */
 
 /* Begin PBXVariantGroup section */
@@ -384,6 +464,7 @@
 				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
 				COPY_PHASE_STRIP = NO;
 				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_BITCODE = NO;
 				ENABLE_STRICT_OBJC_MSGSEND = YES;
 				ENABLE_TESTABILITY = YES;
 				FRAMEWORK_SEARCH_PATHS = $SRCROOT/../../../third_party/swift/google_signin_sdk_4_0_0;
@@ -431,6 +512,7 @@
 				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
 				COPY_PHASE_STRIP = NO;
 				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_BITCODE = NO;
 				ENABLE_NS_ASSERTIONS = NO;
 				ENABLE_STRICT_OBJC_MSGSEND = YES;
 				FRAMEWORK_SEARCH_PATHS = $SRCROOT/../../../third_party/swift/google_signin_sdk_4_0_0;
diff --git a/SyncbaseTodosApp/SyncbaseTodos/AppDelegate.swift b/SyncbaseTodosApp/SyncbaseTodos/AppDelegate.swift
index 68331ec..0bf11e6 100644
--- a/SyncbaseTodosApp/SyncbaseTodos/AppDelegate.swift
+++ b/SyncbaseTodosApp/SyncbaseTodos/AppDelegate.swift
@@ -3,8 +3,8 @@
 // license that can be found in the LICENSE file.
 
 import GoogleSignIn
-import UIKit
 import Syncbase
+import UIKit
 
 @UIApplicationMain
 class AppDelegate: UIResponder, UIApplicationDelegate {
@@ -22,9 +22,8 @@
       // Craft a blessing prefix using google sign-in and the dev.v.io blessings provider.
       defaultBlessingStringPrefix: "dev.v.io:o:\(clientID):",
       // Cloud mount-point.
-      mountPoints: ["/ns.dev.v.io:8101/tmp/todos/users/"],
-      // TODO(zinman): Remove this once we have create-or-join implemented.
-      disableUserdataSyncgroup: true)
+      // TODO(mrschmidt): Remove the ios-specific portion of mountpoint when VOM is implemented.
+      mountPoints: ["/ns.dev.v.io:8101/tmp/ios/todos/users/"])
     return true
   }
 
@@ -53,7 +52,7 @@
   }
 
   func applicationWillTerminate(application: UIApplication) {
-    // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
+    Syncbase.shutdown()
   }
 }
 
diff --git a/SyncbaseTodosApp/SyncbaseTodos/Base.lproj/Main.storyboard b/SyncbaseTodosApp/SyncbaseTodos/Base.lproj/Main.storyboard
index 49c9876..48add4c 100644
--- a/SyncbaseTodosApp/SyncbaseTodos/Base.lproj/Main.storyboard
+++ b/SyncbaseTodosApp/SyncbaseTodos/Base.lproj/Main.storyboard
@@ -215,6 +215,9 @@
                         </barButtonItem>
                         <barButtonItem key="rightBarButtonItem" systemItem="add" id="mjQ-zJ-bqJ">
                             <color key="tintColor" red="1" green="1" blue="1" alpha="1" colorSpace="calibratedRGB"/>
+                            <connections>
+                                <action selector="didPressAddTodoList" destination="4Az-YS-hA9" id="3sh-hT-J0z"/>
+                            </connections>
                         </barButtonItem>
                     </navigationItem>
                     <connections>
@@ -317,7 +320,7 @@
                                                     </constraints>
                                                     <state key="normal" image="checkmarkOff"/>
                                                     <connections>
-                                                        <action selector="toggleComplete" destination="3XL-e6-zxp" eventType="touchUpInside" id="ShA-fQ-Ywv"/>
+                                                        <action selector="toggleIsDone:" destination="9RW-nK-7je" eventType="touchUpInside" id="g1i-qU-2OC"/>
                                                     </connections>
                                                 </button>
                                                 <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Retrieve noogler hat" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="IFI-3u-8gC">
diff --git a/SyncbaseTodosApp/SyncbaseTodos/Dispatch.swift b/SyncbaseTodosApp/SyncbaseTodos/Dispatch.swift
new file mode 100644
index 0000000..6d010b3
--- /dev/null
+++ b/SyncbaseTodosApp/SyncbaseTodos/Dispatch.swift
@@ -0,0 +1,59 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+import Foundation
+
+// Normally this would be a static variable on DispatchHandler but we can't do that for a generic
+// type in Swift as of 2.x.
+var _dispatchHandlerUniqueCounter: Int32 = 0
+
+/// Struct to hold the callbacks for a Dispatch listener. It is hashable to support adding/removing
+/// callbacks -- function pointers are not otherwise equatable in Swift.
+class DispatchHandler<Event>: Hashable {
+  var onEvents: [Event] -> ()
+  let uniqueId = OSAtomicIncrement32(&_dispatchHandlerUniqueCounter)
+  var hashValue: Int { return uniqueId.hashValue }
+
+  required init(onEvents: [Event] -> ()) {
+    self.onEvents = onEvents
+  }
+}
+
+func == <Event>(lhs: DispatchHandler<Event>, rhs: DispatchHandler<Event>) -> Bool {
+  return lhs.uniqueId == rhs.uniqueId
+}
+
+class Dispatch<Event> {
+  var queue = dispatch_get_main_queue()
+  private var handlers: Set<DispatchHandler<Event>> = []
+  private var handlerMu = NSLock()
+
+  func notify(events: [Event]) {
+    if events.isEmpty {
+      return
+    }
+    dispatch_async(queue) {
+      // Normally having a mutex before doing a callback could result in a deadlock should the
+      // callback end up calling functions that attempt to acquire the mutex like watch or unwatch.
+      // However, we know in this very simple program that is never the case, and thus it is safe.
+      self.handlerMu.lock()
+      for handler in self.handlers {
+        handler.onEvents(events)
+      }
+      self.handlerMu.unlock()
+    }
+  }
+
+  func watch(eventHandler: DispatchHandler<Event>) {
+    self.handlerMu.lock()
+    handlers.insert(eventHandler)
+    self.handlerMu.unlock()
+  }
+
+  func unwatch(eventHandler: DispatchHandler<Event>) {
+    self.handlerMu.lock()
+    handlers.remove(eventHandler)
+    self.handlerMu.unlock()
+  }
+}
diff --git a/SyncbaseTodosApp/SyncbaseTodos/InviteViewController.swift b/SyncbaseTodosApp/SyncbaseTodos/InviteViewController.swift
index 407bd31..78470a0 100644
--- a/SyncbaseTodosApp/SyncbaseTodos/InviteViewController.swift
+++ b/SyncbaseTodosApp/SyncbaseTodos/InviteViewController.swift
@@ -29,8 +29,8 @@
   override func viewDidLoad() {
     super.viewDidLoad()
     initSearchController()
-    createFakeData()
-    tableView.reloadData()
+//    createFakeData()
+//    tableView.reloadData()
   }
 
   func initSearchController() {
@@ -44,30 +44,30 @@
     definesPresentationContext = true
   }
 
-  func createFakeData() {
-    // TODO(azinman): Remove.
-    people.insert([
-      Person(name: "Lady Rainicorn", imageName: "profilePhoto"),
-      Person(name: "Princess Bubblegum", imageName: "profilePhoto"),
-      Person(name: "Ice King", imageName: "profilePhoto")
-      ],
-      atIndex: Category.NearbyContacts.rawValue
-    )
-
-    people.insert([
-      Person(name: "Gunter", imageName: "profilePhoto"),
-      Person(name: "dayang@google.com", imageName: "profilePhoto"),
-      Person(name: "Tom", imageName: "profilePhoto"),
-      ],
-      atIndex: Category.Nearby.rawValue
-    )
-
-    people.insert([
-      Person(name: "Lady Rainicorn", imageName: "profilePhoto"),
-      ],
-      atIndex: Category.Contacts.rawValue
-    )
-  }
+//  func createFakeData() {
+//    // TODO(azinman): Remove.
+//    people.insert([
+//      Person(name: "Lady Rainicorn", imageName: "profilePhoto"),
+//      Person(name: "Princess Bubblegum", imageName: "profilePhoto"),
+//      Person(name: "Ice King", imageName: "profilePhoto")
+//      ],
+//      atIndex: Category.NearbyContacts.rawValue
+//    )
+//
+//    people.insert([
+//      Person(name: "Gunter", imageName: "profilePhoto"),
+//      Person(name: "dayang@google.com", imageName: "profilePhoto"),
+//      Person(name: "Tom", imageName: "profilePhoto"),
+//      ],
+//      atIndex: Category.Nearby.rawValue
+//    )
+//
+//    people.insert([
+//      Person(name: "Lady Rainicorn", imageName: "profilePhoto"),
+//      ],
+//      atIndex: Category.Contacts.rawValue
+//    )
+//  }
 
   override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
     if searchController.active && !searchResultsFound {
@@ -150,8 +150,7 @@
       let validEmail = NSPredicate(format: "SELF MATCHES %@", emailRegEx).evaluateWithObject(text)
       if validEmail {
         // Create new person with this email.
-        personToInvite = Person()
-        personToInvite?.email = text
+        personToInvite = Person(name: "", imageRef: "", email: text)
       } else {
         // Invalid email, show alert.
         let alert = UIAlertController(title: "Invalid email",
@@ -207,9 +206,9 @@
 
   func updateView() {
     nameLabel.text = person?.name
-    if let imageName = person?.imageName {
-      photoImageView.image = UIImage(named: imageName)
-    }
+//    if let imageName = person?.imageName {
+//      photoImageView.image = UIImage(named: imageName)
+//    }
   }
 }
 
diff --git a/SyncbaseTodosApp/SyncbaseTodos/LoginViewController.swift b/SyncbaseTodosApp/SyncbaseTodos/LoginViewController.swift
index c9eb40d..fe2a724 100644
--- a/SyncbaseTodosApp/SyncbaseTodos/LoginViewController.swift
+++ b/SyncbaseTodosApp/SyncbaseTodos/LoginViewController.swift
@@ -26,8 +26,8 @@
         }
         return
       }
-    } catch let e {
-      print("Syncbase error: \(e) ")
+    } catch {
+      print("Syncbase error: \(error) ")
     }
     if GIDSignIn.sharedInstance().hasAuthInKeychain() {
       showSpinner()
@@ -58,6 +58,11 @@
     ac.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
     presentViewController(ac, animated: true, completion: nil)
   }
+
+  func signIn(signIn: GIDSignIn!, presentViewController viewController: UIViewController!) {
+    showSpinner()
+    presentViewController(viewController, animated: true, completion: nil)
+  }
 }
 
 extension LoginViewController: GIDSignInDelegate {
@@ -75,6 +80,12 @@
 
       Syncbase.login(GoogleOAuthCredentials(token: user.authentication.accessToken)) { err in
         if err == nil {
+          let imageURL = user.profile.imageURLWithDimension(160)
+          do {
+            try setUserPhotoURL(imageURL)
+          } catch {
+            print("Unexpected error: \(error)")
+          }
           self.didSignIn()
         } else {
           print("Unable to login to Syncbase: \(err) ")
diff --git a/SyncbaseTodosApp/SyncbaseTodos/Marshal.swift b/SyncbaseTodosApp/SyncbaseTodos/Marshal.swift
new file mode 100644
index 0000000..37aee7b
--- /dev/null
+++ b/SyncbaseTodosApp/SyncbaseTodos/Marshal.swift
@@ -0,0 +1,28 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// NOTE: This file is largely temporary until we have a proper Swift-VOM implementation.
+
+import Foundation
+import Syncbase
+
+protocol Jsonable {
+  func toJsonable() -> [String: AnyObject]
+  static func fromJsonable(data: [String: AnyObject]) -> Self?
+}
+
+extension NSData {
+  func unpack<T: Jsonable>() throws -> T? {
+    guard let data = try NSJSONSerialization.JSONObjectWithData(self, options: []) as? [String: AnyObject] else {
+      throw SyncbaseError.CastError(obj: NSString(data: self, encoding: NSUTF8StringEncoding) ??
+        self.description)
+    }
+    return T.fromJsonable(data)
+  }
+
+  func pack<T: Jsonable>(obj: T) throws -> NSData {
+    let jsonable = obj.toJsonable()
+    return try NSJSONSerialization.dataWithJSONObject(jsonable, options: [])
+  }
+}
diff --git a/SyncbaseTodosApp/SyncbaseTodos/MemberView.swift b/SyncbaseTodosApp/SyncbaseTodos/MemberView.swift
index 8142b86..1e2c9d1 100644
--- a/SyncbaseTodosApp/SyncbaseTodos/MemberView.swift
+++ b/SyncbaseTodosApp/SyncbaseTodos/MemberView.swift
@@ -13,15 +13,16 @@
     for view in subviews {
       view.removeFromSuperview()
     }
-    if let list = todoList {
-      var x: CGFloat = 0
-      // Create and add a photo circle for all members
-      for member in list.members {
-        let profilePhoto = imageFactory(member.imageName, offset: x)
-        insertSubview(profilePhoto, atIndex: 0)
-        x += profilePhoto.frame.size.width - profilePhoto.frame.size.width * 0.25
-      }
-    }
+// TODO(zinman): Uncomment & fix when we get photos for members.
+//    if let list = todoList {
+//      var x: CGFloat = 0
+//      // Create and add a photo circle for all members
+//      for member in list.members {
+//        let profilePhoto = imageFactory(member.imageName, offset: x)
+//        insertSubview(profilePhoto, atIndex: 0)
+//        x += profilePhoto.frame.size.width - profilePhoto.frame.size.width * 0.25
+//      }
+//    }
   }
 
   func imageFactory(imageName: String, offset: CGFloat) -> UIImageView {
diff --git a/SyncbaseTodosApp/SyncbaseTodos/Person.swift b/SyncbaseTodosApp/SyncbaseTodos/Person.swift
index 8b90930..96c101c 100644
--- a/SyncbaseTodosApp/SyncbaseTodos/Person.swift
+++ b/SyncbaseTodosApp/SyncbaseTodos/Person.swift
@@ -4,15 +4,27 @@
 
 import Foundation
 
-// All of this is trash to be replaced by syncbase model
-class Person {
+final class Person: Jsonable {
   var name: String = ""
-  var imageName: String = ""
+  var imageRef: String = ""
   var email: String = ""
 
-  convenience init(name: String, imageName: String) {
-    self.init()
+  init(name: String, imageRef: String, email: String) {
     self.name = name
-    self.imageName = imageName
+    self.imageRef = imageRef
+    self.email = email
+  }
+
+  func toJsonable() -> [String: AnyObject] {
+    return ["name": name, "imageRef": imageRef, "email": email]
+  }
+
+  static func fromJsonable(data: [String: AnyObject]) -> Person? {
+    guard let name = data["name"] as? String,
+      imageRef = data["imageRef"] as? String,
+      email = data["email"] as? String else {
+        return nil
+    }
+    return Person(name: name, imageRef: imageRef, email: email)
   }
 }
\ No newline at end of file
diff --git a/SyncbaseTodosApp/SyncbaseTodos/Syncbase.swift b/SyncbaseTodosApp/SyncbaseTodos/Syncbase.swift
new file mode 100644
index 0000000..44c8a83
--- /dev/null
+++ b/SyncbaseTodosApp/SyncbaseTodos/Syncbase.swift
@@ -0,0 +1,324 @@
+// Copyright 2016 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// The Todo app's model uses the Syncbase key-value store using the following data types:
+//
+// type TodoList struct {
+//   Name string
+//   UpdatedAt timestamp
+// }
+//
+// type Task struct {
+//   Text string
+//   AddedAt timestamp
+//   Done bool
+// }
+//
+// These data types are currently serialized using JSON until VOM has been ported to Swift, at which
+// point we'll use the generated VDL / VOM structs. Until then, we do not have cross-platform
+// compatibility with Android, which uses VOM.
+//
+// Each Todo List is stored in its own collection in the Syncbase database, where its rows represent
+// the individual tasks. The collection is named with a unique UUID (that UUID is generated
+// automatically by the API's Database.createCollection method). The metadata on this list, that is
+// to say the serialized TodoList struct, is stored in a special row in these collections with
+// the key `todoListKey`. All other rows are the serialized `Task` structs.
+//
+// This file provides the CRUD (create, read, update, delete) APIs to manipulate todo lists and
+// structs in their high-level Swift representations. It also contains the logic that "watches" the
+// Syncbase database in order to replicate these lists and todos in-memory as Swift structs as
+// the data changes (either from the user invoking the CRUD APIs or from other users). As the watch
+// logic determines the exact change, the related `ModelEvent` notifies any listeners to these
+// changes, which are the TodosViewController or the TasksViewController.
+//
+// Note that the data model is "unidirectional," which is to say that when the user press a button,
+// that button only will call the appropriate data model API call. It does not update the UI itself.
+// Instead, the running watch will receive the change the same way regardless if it originated on
+// this device or another, and update the UI accordingly. This is the principal intended design of
+// Syncbase.
+
+import Foundation
+import Syncbase
+
+let todoListKey = "list"
+let todoListCollectionPrefix = "lists"
+let userProfilePhotoURLKey = "userProfilePhotoURL"
+
+var todoLists: [TodoList] = []
+
+// MARK: Watch high level events
+
+enum ModelEvent {
+  case ReloadLists(lists: [TodoList])
+  case AddList(list: TodoList, index: Int)
+  case UpdateList(list: TodoList, index: Int)
+  case DeleteList(list: TodoList, index: Int)
+  case AddTask(list: TodoList, listIndex: Int, task: Task, taskIndex: Int)
+  case UpdateTask(list: TodoList, listIndex: Int, task: Task, taskIndex: Int)
+  case DeleteTask(list: TodoList, listIndex: Int, task: Task, taskIndex: Int)
+}
+
+// Struct to hold the callbacks for WatchEvents. It is hashable to support adding/removing callbacks
+// which otherwise cannot be equated.
+
+private let dispatch = Dispatch<ModelEvent>()
+
+final class ModelEventHandler: DispatchHandler<ModelEvent> {
+  required init(onEvents: [ModelEvent] -> ()) {
+    super.init(onEvents: onEvents)
+  }
+}
+
+/// Used by the ViewControllers to subscribe to model events.
+func startWatchModelEvents(eventHandler: ModelEventHandler) {
+  dispatch.watch(eventHandler)
+}
+
+/// Used by the ViewControllers to unsubscribe to model events.
+func stopWatchingModelEvents(eventHandler: ModelEventHandler) {
+  dispatch.unwatch(eventHandler)
+}
+
+// MARK: Watch low-level events
+private var watchHandler: WatchChangeHandler?
+
+func startWatching() throws {
+  if let handler = watchHandler {
+    try Syncbase.database().removeWatchChangeHandler(handler)
+    watchHandler = nil
+  }
+  let handler = WatchChangeHandler(
+    onInitialState: onInitialState,
+    onChangeBatch: onChangeBatch,
+    onError: { err in
+      // No more calls to watch will occur.
+      print("Unexpected error watching model: \(err)")
+  })
+  try Syncbase.database().addWatchChangeHandler(handler: handler)
+  watchHandler = handler
+}
+
+private func onInitialState(changes: [WatchChange]) {
+  // As the changes can come in any order (e.g. a task after the TodoList serialized), we bucket
+  // by collectionId and keep the serialized todoList row first. That way we can re-construct all
+  // of the data properly.
+
+  // Now that we have it all bucketed and properly ordered, reconstruct the data.
+  todoLists.removeAll()
+  for (collectionId, changes) in groupListChangesByCollectionId(changes) {
+    // TOD(zinman): How can this fail? How to better handle than crashing?
+    let collection = try! Syncbase.database().collection(collectionId)
+    // groupListChangesByCollectionId puts the serialized TodoList first.
+    guard let list = changes.first?.toTodoList(collection) else {
+      continue
+    }
+    for change in changes.dropFirst() {
+      if let task = change.toTask() {
+        list.tasks.append(task)
+      }
+    }
+    todoLists.append(list)
+  }
+
+  dispatch.notify([ModelEvent.ReloadLists(lists: todoLists)])
+}
+
+func onChangeBatch(changes: [WatchChange]) {
+  var events: [ModelEvent] = []
+  for (collectionId, var changes) in groupListChangesByCollectionId(changes) {
+    // TOD(zinman): How can this fail? How to better handle than crashing?
+    let collection = try! Syncbase.database().collection(collectionId)
+    var list: TodoList!
+    // This is a O(N) search, but it's ok since this is after the initial state we expeect few of
+    // these callbacks AND n to be small. If either of these assumptions change we'll need to
+    // use a map or an alternative approach to loading all into memory.
+    var listIdx = todoLists.indexOf({ $0.collection?.collectionId == collectionId })
+    if let idx = listIdx {
+      list = todoLists[idx]
+    }
+
+    // The first change will always be a TodoList (row == todoListKey) IF this batch is adding
+    // updating, or deleting a TodoList. Otherwise, the changes are to tasks in the TodoList.
+    let firstChange = changes.first!
+    if let newList = firstChange.toTodoList(collection) {
+      changes = Array(changes.dropFirst(1))
+      // First change is a Put that contains a TodoList. It's either an insert or an update.
+      if listIdx != nil {
+        // It's an update of the TodoList. We can recycle its tasks knowing any updates to its
+        // tasks will be in a separate change.
+        newList.tasks = list.tasks
+        // TODO(zinman): Confirm this later on.
+        newList.members = list.members
+        list = newList
+        todoLists[listIdx!] = list
+        events.append(ModelEvent.UpdateList(list: list, index: listIdx!))
+      } else {
+        // Inserting a new TodoList.
+        list = newList
+        listIdx = todoLists.count
+        todoLists.append(list)
+        events.append(ModelEvent.AddList(list: list, index: listIdx!))
+      }
+    } else if firstChange.changeType == .Delete &&
+    (firstChange.row == todoListKey || firstChange.entityType == .Collection) {
+      changes = Array(changes.dropFirst(1))
+      // Deleting this collection -- since it's already grouped by this collectionId we can
+      // continue after deleting the TodoList.
+      if listIdx != nil {
+        todoLists.removeAtIndex(listIdx!)
+        events.append(ModelEvent.DeleteList(list: list, index: listIdx!))
+      }
+      continue
+    } else if listIdx == nil {
+      // Don't have list existing or incoming to process the changes.
+      continue
+    }
+
+    // Process task changes
+    for change in changes {
+      assert(firstChange.row != todoListKey)
+      if let task = change.toTask() {
+        // Is it an update or an insert?
+        if let taskIdx = list.tasks.indexOf({ $0.key == task.key }) {
+          list.tasks[taskIdx] = task
+          events.append(ModelEvent.UpdateTask(list: list, listIndex: listIdx!, task: task, taskIndex: taskIdx))
+        } else {
+          list.tasks.append(task)
+          events.append(ModelEvent.UpdateTask(list: list, listIndex: listIdx!, task: task, taskIndex: list.tasks.count - 1))
+        }
+      } else if change.changeType == .Delete,
+        let row = change.row,
+        key = NSUUID(UUIDString: row) {
+          if let taskIdx = list.tasks.indexOf({ $0.key == key }) {
+            let task = list.tasks.removeAtIndex(taskIdx)
+            events.append(ModelEvent.DeleteTask(list: list, listIndex: listIdx!, task: task, taskIndex: taskIdx))
+          }
+      }
+    }
+  }
+  dispatch.notify(events)
+}
+
+// MARK: MODEL API
+
+func addList(list: TodoList) throws {
+  let data = try NSJSONSerialization.dataWithJSONObject(list.toJsonable(), options: [])
+  list.collection = try Syncbase.database().createCollection(prefix: todoListCollectionPrefix)
+  try list.collection!.put(todoListKey, value: data)
+  // No need to update the local data ourselves -- that will happen in the watch handler.
+}
+
+func removeList(list: TodoList) throws {
+  guard let collection = list.collection else {
+    throw SyncbaseError.IllegalArgument(detail: "Missing collection from TodoList: \(list)")
+  }
+  try collection.destroy()
+  // No need to update the local data ourselves -- that will happen in the watch handler.
+}
+
+func addTask(list: TodoList, task: Task) throws {
+  let data = try NSJSONSerialization.dataWithJSONObject(task.toJsonable(), options: [])
+  let key = task.key.UUIDString
+  try list.collection!.put(key, value: data)
+}
+
+func removeTask(list: TodoList, task: Task) throws {
+  let key = task.key.UUIDString
+  try list.collection!.delete(key)
+}
+
+func setTaskIsDone(list: TodoList, task: Task, isDone: Bool) throws {
+  task.done = isDone
+  // This is the same operation as updating the row since we put the data.
+  try addTask(list, task: task)
+}
+
+func setTasksAreDone(list: TodoList, tasks: [Task], isDone: Bool) throws {
+  try Syncbase.database().runInBatch { bdb in
+    for task in tasks {
+      task.done = isDone
+      let data = try NSJSONSerialization.dataWithJSONObject(task.toJsonable(), options: [])
+      let key = task.key.UUIDString
+      // We must get a reference via the batch database handle rather than the existing non-batch
+      // cached collection.
+      try bdb.collection(list.collection!.collectionId).put(key, value: data)
+    }
+  }
+}
+
+func setUserPhotoURL(url: NSURL) throws {
+  let jsonable = [url.absoluteString]
+  let json = try NSJSONSerialization.dataWithJSONObject(jsonable, options: [])
+  try Syncbase.database().userdataCollection.put(userProfilePhotoURLKey, value: json)
+}
+
+func userProfileURL() throws -> NSURL? {
+  let data: NSData? = try Syncbase.database().userdataCollection.get(userProfilePhotoURLKey)
+  guard let json = data,
+    jsonable = try NSJSONSerialization.JSONObjectWithData(json, options: []) as? [String],
+    url = jsonable.first else {
+      return nil
+  }
+  return NSURL(string: url)
+}
+
+// MARK: Helpers
+
+private func groupListChangesByCollectionId(changes: [WatchChange]) -> [Identifier: [WatchChange]] {
+  var changesByCollectionId: [Identifier: [WatchChange]] = [:]
+  for change in changes {
+    guard let collectionId = change.collectionId where collectionId.name.hasPrefix(todoListCollectionPrefix) else {
+      continue
+    }
+    // Keep deletes on all entities (collections for lists, rows for tasks), but otherwise only keep
+    // row changes for puts -- we don't use the collection put change.
+    if change.changeType == .Put && change.entityType != .Row {
+      continue
+    }
+    if var cxChanges = changesByCollectionId[collectionId] {
+      if change.row == todoListKey {
+        // This is the row that describes the whole TodoList. Keep it first.
+        cxChanges.insert(change, atIndex: 0)
+      } else {
+        cxChanges.append(change)
+      }
+      changesByCollectionId[collectionId] = cxChanges
+    } else {
+      changesByCollectionId[collectionId] = [change]
+    }
+  }
+  return changesByCollectionId
+}
+
+private extension WatchChange {
+  func toTask() -> Task? {
+    if changeType != .Put || entityType != .Row || row == todoListKey {
+      return nil
+    }
+    guard let row = row,
+      key = NSUUID(UUIDString: row),
+      json = value,
+      obj = try? NSJSONSerialization.JSONObjectWithData(json, options: []) as? [String: AnyObject],
+      jsonable = obj,
+      task = Task.fromJsonable(jsonable) else {
+        return nil
+    }
+    task.key = key
+    return task
+  }
+
+  func toTodoList(collection: Collection) -> TodoList? {
+    if changeType != .Put || entityType != .Row || row != todoListKey {
+      return nil
+    }
+    guard let json = value,
+      obj = try? NSJSONSerialization.JSONObjectWithData(json, options: []) as? [String: AnyObject],
+      jsonable = obj,
+      list = TodoList.fromJsonable(jsonable) else {
+        return nil
+    }
+    list.collection = collection
+    return list
+  }
+}
diff --git a/SyncbaseTodosApp/SyncbaseTodos/Task.swift b/SyncbaseTodosApp/SyncbaseTodos/Task.swift
index fc5f1c5..429a9c3 100644
--- a/SyncbaseTodosApp/SyncbaseTodos/Task.swift
+++ b/SyncbaseTodosApp/SyncbaseTodos/Task.swift
@@ -5,19 +5,29 @@
 import Foundation
 
 // All of this is trash to be replaced by syncbase model
-class Task {
+final class Task: Jsonable {
+  var key: NSUUID = NSUUID()
   var text: String = ""
   var addedAt: NSDate = NSDate()
   var done: Bool = false
 
-  convenience init(text: String) {
-    self.init()
+  init(key: NSUUID = NSUUID(), text: String, addedAt: NSDate = NSDate(), done: Bool = false) {
+    self.key = key
     self.text = text
+    self.addedAt = addedAt
+    self.done = done
   }
 
-  convenience init(text: String, done: Bool) {
-    self.init()
-    self.text = text
-    self.done = done
+  func toJsonable() -> [String: AnyObject] {
+    return ["text": text, "addedAt": addedAt.timeIntervalSince1970, "done": done]
+  }
+
+  static func fromJsonable(data: [String: AnyObject]) -> Task? {
+    guard let text = data["text"] as? String,
+      addedAt = data["addedAt"] as? NSTimeInterval,
+      done = data["done"] as? Bool else {
+        return nil
+    }
+    return Task(text: text, addedAt: NSDate(timeIntervalSince1970: addedAt), done: done)
   }
 }
\ No newline at end of file
diff --git a/SyncbaseTodosApp/SyncbaseTodos/TasksViewController.swift b/SyncbaseTodosApp/SyncbaseTodos/TasksViewController.swift
index c9eb779..c079601 100644
--- a/SyncbaseTodosApp/SyncbaseTodos/TasksViewController.swift
+++ b/SyncbaseTodosApp/SyncbaseTodos/TasksViewController.swift
@@ -14,7 +14,8 @@
   let taskCellId = "taskCellId"
   @IBOutlet weak var tableView: UITableView!
   @IBOutlet weak var addButton: UIBarButtonItem!
-  var todoList = TodoList() // Set by segue from TodosViewController
+  var todoList: TodoList! // Set by segue from TodosViewController
+  var handler: ModelEventHandler!
   static let dateFormatter: NSDateFormatter = {
     let dateFormatter = NSDateFormatter()
     dateFormatter.dateFormat = "MMM d"
@@ -24,14 +25,28 @@
   override func viewDidLoad() {
     super.viewDidLoad()
     title = todoList.name
+    handler = ModelEventHandler(onEvents: onEvents)
+    tableView.reloadData()
+  }
+
+  override func viewWillAppear(animated: Bool) {
+    super.viewWillAppear(animated)
+    startWatchModelEvents(handler)
+  }
+
+  override func viewWillDisappear(animated: Bool) {
+    super.viewWillDisappear(animated)
+    stopWatchingModelEvents(handler)
+  }
+
+  func onEvents(events: [ModelEvent]) {
+    print("got events: \(events)")
     tableView.reloadData()
   }
 }
 
-/*
- * Handles tableview functionality, including rendering and swipe actions. Tap actions are
- * handled directly in Main.storyboard using segues
- */
+/// Handles tableview functionality, including rendering and swipe actions. Tap actions are
+/// handled directly in Main.storyboard using segues.
 extension TasksViewController: UITableViewDataSource, UITableViewDelegate {
   func numberOfSectionsInTableView(tableView: UITableView) -> Int {
     return 2
@@ -62,7 +77,18 @@
 
   func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
     if editingStyle == .Delete {
-      self.deleteTask(indexPath)
+      let task = todoList.tasks[indexPath.row]
+      do {
+        try removeTask(todoList, task: task)
+      } catch {
+        print("Unexpected error: \(error)")
+        let ac = UIAlertController(
+          title: "Oops!",
+          message: "Error deleting task. Try again.",
+          preferredStyle: .Alert)
+        ac.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
+        presentViewController(ac, animated: true, completion: nil)
+      }
     }
   }
 
@@ -75,9 +101,7 @@
   }
 }
 
-/*
- * IBActions and data modification functions
- */
+/// IBActions and data modification functions.
 extension TasksViewController {
   @IBAction func toggleEdit() {
     // Do this manually because we're a UIViewController not a UITableViewController, so we don't
@@ -100,32 +124,50 @@
     alert.addTextFieldWithConfigurationHandler { (textField) in }
     alert.addAction(UIAlertAction(title: "Cancel", style: .Cancel, handler: nil))
     alert.addAction(UIAlertAction(title: "Add", style: .Default, handler: { [weak self] action in
-      if let field = alert.textFields?.first, text = field.text {
-        self?.addTask(text)
+      if let field = alert.textFields?.first,
+        text = field.text,
+        todoList = self?.todoList {
+          let task = Task(text: text)
+          do {
+            try addTask(todoList, task: task)
+          } catch {
+            print("Unexpected error: \(error)")
+            let ac = UIAlertController(
+              title: "Oops!",
+              message: "Error adding task. Try again.",
+              preferredStyle: .Alert)
+            ac.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
+            self?.presentViewController(ac, animated: true, completion: nil)
+          }
       }
       }))
     presentViewController(alert, animated: true, completion: nil)
   }
 
-  func addTask(text: String) {
-    // TODO(azinman): Make real
-    let task = Task(text: text)
-    todoList.tasks.insert(task, atIndex: 0)
-    tableView.insertRowsAtIndexPaths([NSIndexPath(forRow: 0, inSection: Section.Tasks.rawValue)], withRowAnimation: .Automatic)
-  }
-
-  func deleteTask(indexPath: NSIndexPath) {
-    todoList.tasks.removeAtIndex(indexPath.row)
-    tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
+  @IBAction func toggleIsDone(sender: UIButton) {
+    if let contentView = sender.superview,
+      let taskCell = contentView.superview as? TaskCell {
+        do {
+          try setTaskIsDone(todoList, task: taskCell.task, isDone: !taskCell.task.done)
+        } catch {
+          print("Unexpected error: \(error)")
+          let ac = UIAlertController(
+            title: "Oops!",
+            message: "Error marking done. Try again.",
+            preferredStyle: .Alert)
+          ac.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
+          presentViewController(ac, animated: true, completion: nil)
+        }
+    }
   }
 }
 
-// Displays the memebers of the todo list as photos and an invite button to launch the invite flow
+/// Displays the memebers of the todo list as photos and an invite button to launch the invite flow.
 class InviteCell: UITableViewCell {
   @IBOutlet weak var memberView: MemberView!
   var todoList: TodoList?
 
-  // Member view has its own render method that draws the member photos
+  // Member view has its own render method that draws the member photos.
   func updateView() {
     selectionStyle = .None
     memberView.todoList = todoList
@@ -139,7 +181,8 @@
   @IBOutlet weak var completeButton: UIButton!
   @IBOutlet weak var taskNameLabel: UILabel!
   @IBOutlet weak var addedAtLabel: UILabel!
-  var task = Task(text: "default")
+  weak var delegate: TasksViewController!
+  var task: Task!
 
   func updateView() {
     selectionStyle = .None
@@ -166,9 +209,4 @@
       completeButton.setImage(UIImage(named: "checkmarkOff"), forState: .Normal)
     }
   }
-
-  @IBAction func toggleComplete() {
-    task.done = !task.done
-    updateView()
-  }
-}
\ No newline at end of file
+}
diff --git a/SyncbaseTodosApp/SyncbaseTodos/TodoList.swift b/SyncbaseTodosApp/SyncbaseTodos/TodoList.swift
index d3f2275..74cc28c 100644
--- a/SyncbaseTodosApp/SyncbaseTodos/TodoList.swift
+++ b/SyncbaseTodosApp/SyncbaseTodos/TodoList.swift
@@ -3,21 +3,23 @@
 // license that can be found in the LICENSE file.
 
 import Foundation
+import Syncbase
 
-// All of this is trash to be replaced by syncbase model
-class TodoList {
+final class TodoList: Jsonable {
+  // This is set in Syncbase.swift when the TodoList is originally deserialized.
+  var collection: Collection? = nil
   var name: String = ""
   var updatedAt: NSDate = NSDate()
   var members: [Person] = []
   var tasks: [Task] = []
 
-  convenience init (name: String) {
-    self.init()
+  init(name: String, updatedAt: NSDate = NSDate()) {
     self.name = name
+    self.updatedAt = updatedAt
   }
 
   func isComplete() -> Bool {
-    return tasks.filter { task in
+    return !tasks.isEmpty && tasks.filter { task in
       return !task.done
     }.count == 0
   }
@@ -27,4 +29,18 @@
       return task.done
     }.count
   }
-}
\ No newline at end of file
+
+  func toJsonable() -> [String: AnyObject] {
+    return [
+      "name": name,
+      "updatedAt": updatedAt.timeIntervalSince1970]
+  }
+
+  static func fromJsonable(data: [String: AnyObject]) -> TodoList? {
+    guard let name = data["name"] as? String,
+      updatedAt = data["updatedAt"] as? NSTimeInterval else {
+        return nil
+    }
+    return TodoList(name: name, updatedAt: NSDate(timeIntervalSince1970: updatedAt))
+  }
+}
diff --git a/SyncbaseTodosApp/SyncbaseTodos/TodosViewController.swift b/SyncbaseTodosApp/SyncbaseTodos/TodosViewController.swift
index e83bcda..27b7b50 100644
--- a/SyncbaseTodosApp/SyncbaseTodos/TodosViewController.swift
+++ b/SyncbaseTodosApp/SyncbaseTodos/TodosViewController.swift
@@ -2,15 +2,17 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+import GoogleSignIn
 import UIKit
+import Syncbase
 
 class TodosViewController: UIViewController {
   @IBOutlet weak var tableView: UITableView!
   @IBOutlet weak var addButton: UIBarButtonItem!
-  // The menu toolbar is shown when the "edit" navigation bar is pressed
+  // The menu toolbar is shown when the "edit" navigation bar is pressed.
   @IBOutlet weak var menuToolbar: UIToolbar!
   @IBOutlet weak var menuToolbarTopConstraint: NSLayoutConstraint!
-  var data: [TodoList] = []
+  var handler: ModelEventHandler!
   static let dateFormatter: NSDateFormatter = {
     let dateFormatter = NSDateFormatter()
     dateFormatter.dateFormat = "MMM d"
@@ -21,62 +23,57 @@
     super.viewDidLoad()
     // Hide the bottom menu by default
     menuToolbarTopConstraint.constant = -menuToolbar.frame.size.height
-    createFakeData()
-    tableView.reloadData()
+    handler = ModelEventHandler(onEvents: onEvents)
+    do {
+      try startWatching()
+    } catch {
+      print("Unable to start watch: \(error)")
+    }
   }
 
   override func viewWillAppear(animated: Bool) {
     super.viewWillAppear(animated)
+    startWatchModelEvents(handler)
     tableView.reloadData()
   }
 
-  func createFakeData() {
-    // TODO(azinman): Remove
-    data = [TodoList(name: "Nooglers Training"), TodoList(name: "Sunday BBQ Shopping")]
-    let person = Person(name: "John", imageName: "profilePhoto")
-    data[0].members = [person, person, person]
-    data[0].tasks = [
-      Task(text: "Retrieve Noogler Hat"),
-      Task(text: "Eat lunch at a cafe", done: true),
-      Task(text: "Pick up badge", done: true),
-      Task(text: "Parkin building 45", done: true),
-    ]
-
-    data[1].members = [person, person]
-    data[1].tasks = [
-      Task(text: "Apples"),
-      Task(text: "Frosted Mini Wheats", done: true),
-      Task(text: "Whole wheat bagels"),
-      Task(text: "Kale"),
-      Task(text: "Eggs", done: true),
-    ]
+  override func viewWillDisappear(animated: Bool) {
+    super.viewWillDisappear(animated)
+    stopWatchingModelEvents(handler)
   }
 
   override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
-    // When we tap on a todo list and segue into the tasks list
+    // When we tap on a todo list and segue into the tasks list.
     if let tvc = segue.destinationViewController as? TasksViewController,
       cell = sender as? TodoListCell,
       indexPath = tableView.indexPathForCell(cell) {
-        tvc.todoList = data[indexPath.row]
+        tvc.todoList = todoLists[indexPath.row]
     }
   }
+
+  func onEvents(events: [ModelEvent]) {
+    print("got events: \(events)")
+    tableView.reloadData()
+  }
 }
 
-//Handles tableview functionality, including rendering and swipe actions. Tap actions are
-// handled directly in Main.storyboard using segues
+/// Handles tableview functionality, including rendering and swipe actions. Tap actions are
+/// handled directly in Main.storyboard using segues.
 extension TodosViewController: UITableViewDelegate, UITableViewDataSource {
   func numberOfSectionsInTableView(tableView: UITableView) -> Int {
     return 1
   }
 
   func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
-    return data.count
+    return todoLists.count
   }
 
   func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
     // TodoListCell is the prototype inside the Main.storyboard. Cannot fail.
-    let cell = tableView.dequeueReusableCellWithIdentifier(TodoListCell.todoListCellId, forIndexPath: indexPath) as! TodoListCell
-    cell.todoList = data[indexPath.row]
+    let cell = tableView.dequeueReusableCellWithIdentifier(
+      TodoListCell.todoListCellId,
+      forIndexPath: indexPath) as! TodoListCell
+    cell.todoList = todoLists[indexPath.row]
     cell.updateView()
     return cell
   }
@@ -84,8 +81,7 @@
   func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
     switch editingStyle {
     case .Delete:
-      data.removeAtIndex(indexPath.row)
-      tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
+      deleteRow(indexPath)
     default: break
     }
   }
@@ -96,18 +92,32 @@
         self?.completeAllTasks(indexPath)
       }),
       UITableViewRowAction(style: .Default, title: "Delete", handler: { [weak self](action, indexPath) in
-        self?.deleteList(indexPath)
+        self?.deleteRow(indexPath)
       })
     ]
     return actions
   }
+
+  func deleteRow(indexPath: NSIndexPath) {
+    do {
+      try removeList(todoLists[indexPath.row])
+    } catch {
+      print("Unexpected error: \(error)")
+      let ac = UIAlertController(
+        title: "Oops!",
+        message: "Error deleting list. Try again.",
+        preferredStyle: .Alert)
+      ac.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
+      self.presentViewController(ac, animated: true, completion: nil)
+    }
+  }
 }
 
-// IBActions and data modification functions
+/// IBActions and data modification functions.
 extension TodosViewController {
   @IBAction func toggleEdit() {
     // Do this manually because we're a UIViewController not a UITableViewController, so we don't
-    // get editing behavior for free
+    // get editing behavior for free.
     if tableView.editing {
       tableView.setEditing(false, animated: true)
       navigationItem.leftBarButtonItem?.title = "Edit"
@@ -123,6 +133,36 @@
     UIView.animateWithDuration(0.35) { self.view.layoutIfNeeded() }
   }
 
+  @IBAction func didPressAddTodoList() {
+    let ac = UIAlertController(title: "New Todo List",
+      message: "Please name your new list",
+      preferredStyle: UIAlertControllerStyle.Alert)
+    ac.addAction(UIAlertAction(title: "Create", style: UIAlertActionStyle.Default)
+      { (action: UIAlertAction) in
+        if let name = ac.textFields?.first?.text where !name.isEmpty {
+          self.addTodoList(name)
+        }
+    })
+    ac.addAction(UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Cancel, handler: nil))
+    ac.addTextFieldWithConfigurationHandler { $0.placeholder = "My New List" }
+    self.presentViewController(ac, animated: true, completion: nil)
+  }
+
+  func addTodoList(name: String) {
+    let list = TodoList(name: name)
+    do {
+      try addList(list)
+    } catch {
+      print("Error adding list: \(error)")
+      let ac = UIAlertController(
+        title: "Oops!",
+        message: "Error adding list. Try again.",
+        preferredStyle: .Alert)
+      ac.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
+      self.presentViewController(ac, animated: true, completion: nil)
+    }
+  }
+
   @IBAction func debug() {
     // TODO(azinman): Make real
   }
@@ -132,19 +172,18 @@
   }
 
   func completeAllTasks(indexPath: NSIndexPath) {
-    // TODO(azinman): Make real
-    assert(data.indices.contains(indexPath.row), "data does not contain that index path")
-    let todoList = data[indexPath.row]
-    for task in todoList.tasks {
-      task.done = true
+    let todoList = todoLists[indexPath.row]
+    do {
+      try setTasksAreDone(todoList, tasks: todoList.tasks, isDone: true)
+    } catch {
+      print("Error completing all tasks: \(error)")
+      let ac = UIAlertController(
+        title: "Oops!",
+        message: "Error completing all tasks. Try again.",
+        preferredStyle: .Alert)
+      ac.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
+      presentViewController(ac, animated: true, completion: nil)
     }
-    tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
-  }
-
-  func deleteList(indexPath: NSIndexPath) {
-    // TODO(azinman): Make real
-    data.removeAtIndex(indexPath.row)
-    tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
   }
 }
 
@@ -155,11 +194,10 @@
   @IBOutlet weak var memberView: MemberView!
   @IBOutlet weak var lastModifiedLabel: UILabel!
   static let todoListCellId = "todoListCellId"
-  var todoList = TodoList()
+  var todoList: TodoList!
 
-  // Fills in the iboutlets with data from todoList local property.
-  // memberView has it's only render method that draws out the photos of the members in this todo
-  // list.
+  /// Fills in the iboutlets with data from todoList local property. memberView has it's only render
+  /// method that draws out the photos of the members in this todo list.
   func updateView() {
     if todoList.isComplete() {
       // Draw a strikethrough
@@ -171,7 +209,11 @@
     } else {
       titleLabel.text = todoList.name
     }
-    completedLabel.text = "\(todoList.numberTasksComplete())/\(todoList.tasks.count) completed"
+    if todoList.tasks.isEmpty {
+      completedLabel.text = "No tasks"
+    } else {
+      completedLabel.text = "\(todoList.numberTasksComplete())/\(todoList.tasks.count) completed"
+    }
     lastModifiedLabel.text = TodosViewController.dateFormatter.stringFromDate(todoList.updatedAt)
     // Draw the photos of list members
     memberView.todoList = todoList