swift: Add discovery & invites to the todos app

Adds advertising to the todos example app, an invite handler that
auto-accepts all invites, discovery that shows nearby users as
well as all their contacts in their address book, and the invite
logic for a given discovered or addressbook-based email.

This CL also includes the following other fixes:
- Adds a public init for SyncgroupInviteHandler in Syncbase.framework
- An initial set of app icons. We need to include more sizes for
  various devices.
- Removes an assert that was invalid in Syncbase model.

Change-Id: I5912917852bcc4d0edb0caf916f79c99ceed9d5b
diff --git a/Syncbase/Source/Database.swift b/Syncbase/Source/Database.swift
index e0370ff..b6118fc 100644
--- a/Syncbase/Source/Database.swift
+++ b/Syncbase/Source/Database.swift
@@ -18,6 +18,11 @@
   /// `onError` is called, no other methods will be called on this handler.
   public let onError: ErrorType -> Void
 
+  public init(onInvite: SyncgroupInvite -> Void, onError: ErrorType -> Void) {
+    self.onInvite = onInvite
+    self.onError = onError
+  }
+
   // This internal-only variable allows us to test SyncgroupInviteHandler structs for equality.
   // This cannot be done otherwise as function calls cannot be tested for equality.
   // Equality is important to facilitate the add/remove APIs in Database where
diff --git a/Syncbase/Source/Syncgroup.swift b/Syncbase/Source/Syncgroup.swift
index cbb8803..d788c67 100644
--- a/Syncbase/Source/Syncgroup.swift
+++ b/Syncbase/Source/Syncgroup.swift
@@ -124,11 +124,13 @@
           mountTables: oldSpec.mountTables,
           isPrivate: oldSpec.isPrivate),
         version: versionedSpec.version))
-      // TODO(sadovsky): There's a race here - it's possible for a collection to get destroyed
-      // after spec.getCollections() but before db.getCollection().
-      try self.database.runInBatch { db in
-        for id in oldSpec.collections {
-          try db.collection(Identifier(coreId: id)).updateAccessList(delta)
+      if !syncgroupOnly {
+        // TODO(sadovsky): There's a race here - it's possible for a collection to get destroyed
+        // after spec.getCollections() but before db.getCollection().
+        try self.database.runInBatch { db in
+          for id in oldSpec.collections {
+            try db.collection(Identifier(coreId: id)).updateAccessList(delta)
+          }
         }
       }
     }
diff --git a/Syncbase/Tests/BasicDatabaseTests.swift b/Syncbase/Tests/BasicDatabaseTests.swift
index 4ef5fd4..9351a2d 100644
--- a/Syncbase/Tests/BasicDatabaseTests.swift
+++ b/Syncbase/Tests/BasicDatabaseTests.swift
@@ -242,7 +242,7 @@
           XCTAssertFalse(didChange)
           XCTAssertEqual(changes.count, 1)
           let change = changes.first!
-          XCTAssertEqual(change.row, try? Syncbase.UserdataCollectionPrefix + sg1.encode())
+          XCTAssertEqual(change.row, try? Syncbase.UserdataInternalSyncgroupPrefix + sg1.encode())
           XCTAssertEqual(change.changeType, WatchChange.ChangeType.Put)
           XCTAssertEqual(change.value, NSData())
           didChange = true
@@ -295,7 +295,7 @@
             XCTAssertFalse(changes.isEmpty)
             for change in changes {
               XCTAssert(change.collectionId?.name == Syncbase.UserdataSyncgroupName)
-              XCTAssert(change.row?.hasPrefix(Syncbase.UserdataCollectionPrefix) ?? false)
+              XCTAssert(change.row?.hasPrefix(Syncbase.UserdataInternalPrefix) ?? false)
             }
             dispatch_semaphore_signal(initialSemaphore)
           },
diff --git a/SyncbaseTodosApp/SyncbaseTodos.xcodeproj/project.pbxproj b/SyncbaseTodosApp/SyncbaseTodos.xcodeproj/project.pbxproj
index eb728da..2ac21dc 100644
--- a/SyncbaseTodosApp/SyncbaseTodos.xcodeproj/project.pbxproj
+++ b/SyncbaseTodosApp/SyncbaseTodos.xcodeproj/project.pbxproj
@@ -14,7 +14,7 @@
 		37B018CC1CFF7A9D004B0A15 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 37B018BF1CFF7A9D004B0A15 /* Main.storyboard */; };
 		37B018CD1CFF7A9D004B0A15 /* CircularImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B018C11CFF7A9D004B0A15 /* CircularImageView.swift */; };
 		37B018CF1CFF7A9D004B0A15 /* MemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B018C31CFF7A9D004B0A15 /* MemberView.swift */; };
-		37B018D01CFF7A9D004B0A15 /* Person.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B018C41CFF7A9D004B0A15 /* Person.swift */; };
+		37B018D01CFF7A9D004B0A15 /* Contact.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B018C41CFF7A9D004B0A15 /* Contact.swift */; };
 		37B018D11CFF7A9D004B0A15 /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B018C51CFF7A9D004B0A15 /* Task.swift */; };
 		37B018D21CFF7A9D004B0A15 /* TasksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B018C61CFF7A9D004B0A15 /* TasksViewController.swift */; };
 		37B018D31CFF7A9D004B0A15 /* TodoList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B018C71CFF7A9D004B0A15 /* TodoList.swift */; };
@@ -112,7 +112,7 @@
 		37B018C11CFF7A9D004B0A15 /* CircularImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularImageView.swift; sourceTree = "<group>"; };
 		37B018C21CFF7A9D004B0A15 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		37B018C31CFF7A9D004B0A15 /* MemberView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MemberView.swift; sourceTree = "<group>"; };
-		37B018C41CFF7A9D004B0A15 /* Person.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Person.swift; sourceTree = "<group>"; };
+		37B018C41CFF7A9D004B0A15 /* Contact.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Contact.swift; sourceTree = "<group>"; };
 		37B018C51CFF7A9D004B0A15 /* Task.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Task.swift; sourceTree = "<group>"; };
 		37B018C61CFF7A9D004B0A15 /* TasksViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TasksViewController.swift; sourceTree = "<group>"; };
 		37B018C71CFF7A9D004B0A15 /* TodoList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TodoList.swift; sourceTree = "<group>"; };
@@ -197,7 +197,7 @@
 		37B018D51CFF7B48004B0A15 /* Model */ = {
 			isa = PBXGroup;
 			children = (
-				37B018C41CFF7A9D004B0A15 /* Person.swift */,
+				37B018C41CFF7A9D004B0A15 /* Contact.swift */,
 				37B018C51CFF7A9D004B0A15 /* Task.swift */,
 				37B018C71CFF7A9D004B0A15 /* TodoList.swift */,
 				932AD50D1D1BCFD3000EA038 /* Marshal.swift */,
@@ -398,7 +398,7 @@
 				37B018C91CFF7A9D004B0A15 /* AppDelegate.swift in Sources */,
 				37B018D11CFF7A9D004B0A15 /* Task.swift in Sources */,
 				37B018D41CFF7A9D004B0A15 /* TodosViewController.swift in Sources */,
-				37B018D01CFF7A9D004B0A15 /* Person.swift in Sources */,
+				37B018D01CFF7A9D004B0A15 /* Contact.swift in Sources */,
 				37B018CD1CFF7A9D004B0A15 /* CircularImageView.swift in Sources */,
 				932AD50E1D1BCFD3000EA038 /* Marshal.swift in Sources */,
 				93213FEF1D1C8BB100297838 /* Syncbase.swift in Sources */,
diff --git a/SyncbaseTodosApp/SyncbaseTodos/Assets.xcassets/AppIcon.appiconset/Contents.json b/SyncbaseTodosApp/SyncbaseTodos/Assets.xcassets/AppIcon.appiconset/Contents.json
index 118c98f..e4e9d38 100644
--- a/SyncbaseTodosApp/SyncbaseTodos/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ b/SyncbaseTodosApp/SyncbaseTodos/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -21,13 +21,15 @@
       "scale" : "3x"
     },
     {
-      "idiom" : "iphone",
       "size" : "60x60",
+      "idiom" : "iphone",
+      "filename" : "app-icon@2x.png",
       "scale" : "2x"
     },
     {
-      "idiom" : "iphone",
       "size" : "60x60",
+      "idiom" : "iphone",
+      "filename" : "app-icon@3x.png",
       "scale" : "3x"
     }
   ],
diff --git a/SyncbaseTodosApp/SyncbaseTodos/Assets.xcassets/AppIcon.appiconset/app-icon@2x.png b/SyncbaseTodosApp/SyncbaseTodos/Assets.xcassets/AppIcon.appiconset/app-icon@2x.png
new file mode 100644
index 0000000..19eaa87
--- /dev/null
+++ b/SyncbaseTodosApp/SyncbaseTodos/Assets.xcassets/AppIcon.appiconset/app-icon@2x.png
Binary files differ
diff --git a/SyncbaseTodosApp/SyncbaseTodos/Assets.xcassets/AppIcon.appiconset/app-icon@3x.png b/SyncbaseTodosApp/SyncbaseTodos/Assets.xcassets/AppIcon.appiconset/app-icon@3x.png
new file mode 100644
index 0000000..7f064bf
--- /dev/null
+++ b/SyncbaseTodosApp/SyncbaseTodos/Assets.xcassets/AppIcon.appiconset/app-icon@3x.png
Binary files differ
diff --git a/SyncbaseTodosApp/SyncbaseTodos/Assets.xcassets/profilePhoto.imageset/Contents.json b/SyncbaseTodosApp/SyncbaseTodos/Assets.xcassets/profilePhoto.imageset/Contents.json
deleted file mode 100644
index d7d9e61..0000000
--- a/SyncbaseTodosApp/SyncbaseTodos/Assets.xcassets/profilePhoto.imageset/Contents.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
-  "images" : [
-    {
-      "idiom" : "universal",
-      "scale" : "1x"
-    },
-    {
-      "idiom" : "universal",
-      "filename" : "profilePhoto.png",
-      "scale" : "2x"
-    },
-    {
-      "idiom" : "universal",
-      "scale" : "3x"
-    }
-  ],
-  "info" : {
-    "version" : 1,
-    "author" : "xcode"
-  }
-}
\ No newline at end of file
diff --git a/SyncbaseTodosApp/SyncbaseTodos/Assets.xcassets/profilePhoto.imageset/profilePhoto.png b/SyncbaseTodosApp/SyncbaseTodos/Assets.xcassets/profilePhoto.imageset/profilePhoto.png
deleted file mode 100644
index d68d016..0000000
--- a/SyncbaseTodosApp/SyncbaseTodos/Assets.xcassets/profilePhoto.imageset/profilePhoto.png
+++ /dev/null
Binary files differ
diff --git a/SyncbaseTodosApp/SyncbaseTodos/Base.lproj/Main.storyboard b/SyncbaseTodosApp/SyncbaseTodos/Base.lproj/Main.storyboard
index 48add4c..75202bf 100644
--- a/SyncbaseTodosApp/SyncbaseTodos/Base.lproj/Main.storyboard
+++ b/SyncbaseTodosApp/SyncbaseTodos/Base.lproj/Main.storyboard
@@ -180,12 +180,6 @@
                                         </connections>
                                     </barButtonItem>
                                     <barButtonItem style="plain" systemItem="flexibleSpace" id="tGB-vG-wwH"/>
-                                    <barButtonItem title="Debug" id="M4k-N2-NVt">
-                                        <connections>
-                                            <action selector="debug" destination="4Az-YS-hA9" id="zFH-Wp-hcs"/>
-                                        </connections>
-                                    </barButtonItem>
-                                    <barButtonItem style="plain" systemItem="flexibleSpace" id="fCt-vQ-eNa"/>
                                 </items>
                             </toolbar>
                         </subviews>
@@ -225,6 +219,7 @@
                         <outlet property="menuToolbar" destination="pgB-6n-kxg" id="t4H-IG-sSd"/>
                         <outlet property="menuToolbarTopConstraint" destination="Tte-CX-3hG" id="TUD-bG-KpB"/>
                         <outlet property="tableView" destination="kmv-xQ-8U6" id="88K-fx-Dzp"/>
+                        <outlet property="toggleSharingButton" destination="rb7-ge-A8E" id="DiM-k8-cmh"/>
                     </connections>
                 </viewController>
                 <placeholder placeholderIdentifier="IBFirstResponder" id="jFT-NN-2Df" userLabel="First Responder" sceneMemberID="firstResponder"/>
@@ -418,7 +413,7 @@
         <scene sceneID="KB2-7v-cHC">
             <objects>
                 <tableViewController id="TEb-eN-UhB" customClass="InviteViewController" customModule="SyncbaseTodos" customModuleProvider="target" sceneMemberID="viewController">
-                    <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="none" rowHeight="74" sectionHeaderHeight="28" sectionFooterHeight="28" id="p4P-W4-gFc">
+                    <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" rowHeight="74" sectionHeaderHeight="28" sectionFooterHeight="28" id="p4P-W4-gFc">
                         <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
                         <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                         <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
@@ -427,42 +422,32 @@
                                 <rect key="frame" x="0.0" y="92" width="600" height="74"/>
                                 <autoresizingMask key="autoresizingMask"/>
                                 <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="SrI-gO-iqH" id="07a-cu-tgk">
-                                    <rect key="frame" x="0.0" y="0.0" width="600" height="74"/>
+                                    <rect key="frame" x="0.0" y="0.0" width="600" height="73.5"/>
                                     <autoresizingMask key="autoresizingMask"/>
                                     <subviews>
-                                        <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="profilePhoto" translatesAutoresizingMaskIntoConstraints="NO" id="ZNe-Xc-Gii" customClass="CircularImageView" customModule="SyncbaseTodos" customModuleProvider="target">
-                                            <rect key="frame" x="18" y="18" width="38" height="38"/>
-                                            <constraints>
-                                                <constraint firstAttribute="width" secondItem="ZNe-Xc-Gii" secondAttribute="height" multiplier="1:1" id="X6H-qC-G5F"/>
-                                            </constraints>
-                                        </imageView>
                                         <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="A person" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Xw3-gj-ST1">
-                                            <rect key="frame" x="71" y="8" width="521" height="58"/>
+                                            <rect key="frame" x="18" y="8" width="574" height="58"/>
                                             <fontDescription key="fontDescription" type="system" pointSize="17"/>
                                             <color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
                                             <nil key="highlightedColor"/>
                                         </label>
                                     </subviews>
                                     <constraints>
-                                        <constraint firstItem="Xw3-gj-ST1" firstAttribute="leading" secondItem="ZNe-Xc-Gii" secondAttribute="trailing" constant="15" id="5sy-T3-58g"/>
-                                        <constraint firstItem="ZNe-Xc-Gii" firstAttribute="top" secondItem="07a-cu-tgk" secondAttribute="topMargin" constant="10" id="60n-Wl-qqy"/>
                                         <constraint firstItem="Xw3-gj-ST1" firstAttribute="top" secondItem="07a-cu-tgk" secondAttribute="topMargin" id="Gn4-CV-9rE"/>
-                                        <constraint firstAttribute="bottomMargin" secondItem="ZNe-Xc-Gii" secondAttribute="bottom" constant="10" id="TT7-Ae-akW"/>
                                         <constraint firstAttribute="bottomMargin" secondItem="Xw3-gj-ST1" secondAttribute="bottom" id="X3K-9W-Xr0"/>
                                         <constraint firstAttribute="trailingMargin" secondItem="Xw3-gj-ST1" secondAttribute="trailing" id="bQu-ls-rnE"/>
-                                        <constraint firstItem="ZNe-Xc-Gii" firstAttribute="leading" secondItem="07a-cu-tgk" secondAttribute="leadingMargin" constant="10" id="z5g-zm-qTP"/>
+                                        <constraint firstItem="Xw3-gj-ST1" firstAttribute="leading" secondItem="07a-cu-tgk" secondAttribute="leadingMargin" constant="10" id="fkA-2x-qZ2"/>
                                     </constraints>
                                 </tableViewCellContentView>
                                 <connections>
                                     <outlet property="nameLabel" destination="Xw3-gj-ST1" id="lvu-cq-AQB"/>
-                                    <outlet property="photoImageView" destination="ZNe-Xc-Gii" id="Pxf-OI-ks4"/>
                                 </connections>
                             </tableViewCell>
                             <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="sendEmailCellId" id="fBZ-PZ-K1C" userLabel="SendEmailCell" customClass="SendEmailCell" customModule="SyncbaseTodos" customModuleProvider="target">
                                 <rect key="frame" x="0.0" y="166" width="600" height="74"/>
                                 <autoresizingMask key="autoresizingMask"/>
                                 <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="fBZ-PZ-K1C" id="II9-ei-hFs">
-                                    <rect key="frame" x="0.0" y="0.0" width="600" height="74"/>
+                                    <rect key="frame" x="0.0" y="0.0" width="600" height="73.5"/>
                                     <autoresizingMask key="autoresizingMask"/>
                                     <subviews>
                                         <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="sendEmail" translatesAutoresizingMaskIntoConstraints="NO" id="BFR-NP-pD9">
@@ -527,7 +512,6 @@
     </scenes>
     <resources>
         <image name="checkmarkOff" width="24" height="24"/>
-        <image name="profilePhoto" width="88" height="88"/>
         <image name="sendEmail" width="88" height="88"/>
         <image name="v23-icon-white" width="320" height="320"/>
     </resources>
diff --git a/SyncbaseTodosApp/SyncbaseTodos/Contact.swift b/SyncbaseTodosApp/SyncbaseTodos/Contact.swift
new file mode 100644
index 0000000..6309cf5
--- /dev/null
+++ b/SyncbaseTodosApp/SyncbaseTodos/Contact.swift
@@ -0,0 +1,52 @@
+// 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 Contacts
+import Foundation
+import Syncbase
+
+final class Contact: CustomStringConvertible {
+  static let keysToFetch = [
+    CNContactFormatter.descriptorForRequiredKeysForStyle(.FullName),
+    CNContactEmailAddressesKey,
+    CNContactImageDataAvailableKey]
+
+  var name: String?
+  var emails: [String]?
+  var imageURL: NSURL?
+  var user: User?
+
+  init(name: String? = nil, emails: [String]? = nil, imageURL: NSURL? = nil, user: User? = nil) {
+    self.name = name
+    self.emails = emails
+    self.user = user
+    self.imageURL = imageURL
+  }
+
+  init (contact: CNContact) {
+    name = CNContactFormatter.stringFromContact(contact, style: .FullName)
+    // Note there are very rare occasions where an email address may be case-sensitive. You may want
+    // to account for this in your app accordingly.
+    emails = contact.emailAddresses.map { ($0.value as! String).lowercaseString }
+    if contact.imageDataAvailable {
+      imageURL = NSURL(string: "contact://image/\(contact.identifier))")
+    }
+  }
+
+  var description: String {
+    if let name = name {
+      if let user = user {
+        // Distinguish the contact better by providing the advertising alias (email) for context.
+        return "\(name) - \(user.alias)"
+      }
+      return name
+    } else if let user = user {
+      return user.alias
+    } else if let emails = emails where !emails.isEmpty {
+      return emails.first!
+    } else {
+      return ""
+    }
+  }
+}
\ No newline at end of file
diff --git a/SyncbaseTodosApp/SyncbaseTodos/InviteViewController.swift b/SyncbaseTodosApp/SyncbaseTodos/InviteViewController.swift
index 78470a0..0d6e00d 100644
--- a/SyncbaseTodosApp/SyncbaseTodos/InviteViewController.swift
+++ b/SyncbaseTodosApp/SyncbaseTodos/InviteViewController.swift
@@ -2,6 +2,9 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+import Contacts
+import ContactsUI
+import Syncbase
 import UIKit
 
 enum Category: Int {
@@ -12,25 +15,63 @@
 }
 
 class InviteViewController: UITableViewController {
+  // Represents the search bar.
   var searchController: UISearchController!
-  let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
-  let personCellId = "personCellId"
-  let sendEmailCell = "sendEmailCellId"
-  var shouldDismiss = false
-  var people: [[Person]] = [[], [], []]
+  // All known people -- this is the model that the tableview loads from.
+  var people: [[Contact]] = [[], [], []]
+  // Allows us to look up address book contacts we might discover via BLE/mDNS.
+  var contactsByLowercaseEmail: [String: Contact] = [:]
+  var contactsByLowercaseEmailMu = NSLock()
+  // True if the search controller's text matches any existing contacts.
   var searchResultsFound = false
-  var searchResults: [[Person]] = [[], [], []]
-  let sectionNames: [Category: String] = [
+  // All matching people for a given search -- this is the model the tableview loads from.
+  var searchResults: [[Contact]] = [[], [], []]
+  // True if contacts were able to be loaded from the iOS Address Book.
+  var didLoadContacts = false
+  // The TodoList we are inviting users to.
+  var todoList: TodoList!
+
+  // Regex used to know when a search represents an email address.
+  let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
+  // The cell that displays people's names or emails.
+  let personCellId = "personCellId"
+  // The cell that represents sending an invite directly to an email address.
+  let sendEmailCell = "sendEmailCellId"
+  // The names of each section of the table view.
+  var sectionNames: [Category: String] = [
       .NearbyContacts: "Nearby Contacts",
       .Nearby: "Nearby",
       .Contacts: "Contacts",
   ]
+  // Set to true if the user clicked on a name to invite while the seearch controller was active.
+  // The invite logic first waits for the search controller to turn inactive before the view
+  // controller is dismissed. This variable keeps track for the didDismissSearchController callback
+  // to know if it should dismiss the VC or not.
+  var shouldDismiss = false
 
   override func viewDidLoad() {
     super.viewDidLoad()
+    guard todoList.collection != nil else {
+      print("Missing collection from todo list \(todoList)")
+      // Pop view since we can't invite a user. This must be done outside of the viewDidLoad.
+      dispatch_async(dispatch_get_main_queue()) {
+        self.dismissViewControllerAnimated(true) { }
+      }
+      return
+    }
     initSearchController()
-//    createFakeData()
-//    tableView.reloadData()
+    loadContacts() {
+      do {
+        try Syncbase.startScanForUsersInNeighborhood(
+          ScanNeighborhoodForUsersHandler(onFound: self.onFound, onLost: self.onLost))
+      } catch {
+        print("Unable to scan for other users: \(error)")
+      }
+    }
+  }
+
+  override func viewWillDisappear(animated: Bool) {
+    Syncbase.stopAllScansForUsersInNeighborhood()
   }
 
   func initSearchController() {
@@ -44,30 +85,63 @@
     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
-//    )
-//  }
+  // MARK: Address Book contacts
+
+  func loadContacts(doneCallback: Void -> Void) {
+    CNContactStore().requestAccessForEntityType(.Contacts) { (granted, error) in
+      guard granted && error == nil else {
+        print("Contants not able to load: granted=\(granted) error=\(error)")
+        dispatch_async(dispatch_get_main_queue()) {
+          self.disableContacts()
+          doneCallback()
+        }
+        return
+      }
+      let request = CNContactFetchRequest(keysToFetch: Contact.keysToFetch)
+      var results: [Contact] = []
+      do {
+        self.contactsByLowercaseEmailMu.lock()
+        defer { self.contactsByLowercaseEmailMu.unlock() }
+        // This is a bit slow loading everything at once. In a real app you want to take a more
+        // advanced strategy, perhaps updating the UI in batches or some other methodology.
+        try CNContactStore().enumerateContactsWithFetchRequest(request) { (cncontact, stop) in
+          let contact = Contact(contact: cncontact)
+          results.append(contact)
+          if let emails = contact.emails {
+            for email in emails {
+              // The email was lowercased in the Contact class's init.
+              self.contactsByLowercaseEmail[email] = contact
+            }
+          }
+        }
+        dispatch_async(dispatch_get_main_queue()) {
+          self.didLoadContacts = true
+          self.people[Category.Contacts.rawValue] = results
+          self.tableView.reloadData()
+          doneCallback()
+        }
+      } catch {
+        print("Unable to fetch contacts: \(error)")
+        dispatch_async(dispatch_get_main_queue()) {
+          self.disableContacts()
+          doneCallback()
+        }
+      }
+    }
+  }
+
+  /// disableContacts is used when the user has not granted authorization to contacts or some other
+  /// error has occured loading contacts. Must be called from main.
+  func disableContacts() {
+    didLoadContacts = false
+    sectionNames[.Contacts] = nil
+    sectionNames[.NearbyContacts] = nil
+    people = [people[Category.Nearby.rawValue]]
+    searchResults = [searchResults[Category.Nearby.rawValue]]
+    tableView.reloadData()
+  }
+
+  // MARK: Tableview delegates
 
   override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
     if searchController.active && !searchResultsFound {
@@ -114,19 +188,39 @@
     let cell = self.tableView.dequeueReusableCellWithIdentifier(personCellId, forIndexPath: indexPath) as! PersonCell
     if searchController.active {
       // Show search results.
-      cell.person = searchResults[indexPath.section][indexPath.row]
+      cell.contact = searchResults[indexPath.section][indexPath.row]
     } else {
       // Not searching, show entire list.
-      cell.person = people[indexPath.section][indexPath.row]
+      cell.contact = people[indexPath.section][indexPath.row]
     }
     cell.updateView()
     return cell
   }
 
+  override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
+    if searchController.active && !searchResultsFound {
+      return SendEmailCell.height
+    }
+    return PersonCell.height
+  }
+
+  // MARK: Inviting
+
   override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
     // No-op if can't create a person from index path.
-    if let personToInvite = personToInvite(indexPath) {
-      invitePerson(personToInvite)
+    if let contact = contactAtIndexPath(indexPath) {
+      do {
+        try inviteContact(contact)
+      } catch {
+        print("Unable to invite \(contact): \(error)")
+        let ac = UIAlertController(
+          title: "Oops!",
+          message: "Unable to send invite. Try again.",
+          preferredStyle: .Alert)
+        ac.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
+        presentViewController(ac, animated: true, completion: nil)
+        return
+      }
       // Dismiss the view.
       if searchController.active {
         // The search bar needs to close before we pop, so handle in didDismissSearch callback.
@@ -138,19 +232,14 @@
     }
   }
 
-  func invitePerson(person: Person) {
-    NSLog("Inviting \(person)")
-    // TODO(azinman): fill in.
-  }
-
-  func personToInvite(indexPath: NSIndexPath) -> Person? {
+  func contactAtIndexPath(indexPath: NSIndexPath) -> Contact? {
     // Determine if we entered an email or not. If email, validate email and construct a Person obj.
-    var personToInvite: Person?
+    var personToInvite: Contact?
     if let text = searchController.searchBar.text where searchController.active && !searchResultsFound {
       let validEmail = NSPredicate(format: "SELF MATCHES %@", emailRegEx).evaluateWithObject(text)
       if validEmail {
         // Create new person with this email.
-        personToInvite = Person(name: "", imageRef: "", email: text)
+        personToInvite = Contact(emails: [text])
       } else {
         // Invalid email, show alert.
         let alert = UIAlertController(title: "Invalid email",
@@ -160,13 +249,14 @@
         navigationController?.presentViewController(alert, animated: true, completion: nil)
       }
     } else if people.indices.contains(indexPath.section) &&
-    people[indexPath.row].indices.contains(indexPath.row) {
+    people[indexPath.section].indices.contains(indexPath.row) {
       // We're inviting an existing Person.
       personToInvite = people[indexPath.section][indexPath.row]
     }
     return personToInvite
   }
 
+  // This is called after selecting a contact if the search controller is active.
   func didDismissSearchController(searchController: UISearchController) {
     // If we were searching when we selected a person, we need to wait for its dismiss animation
     // to finish before popping the view.
@@ -174,45 +264,120 @@
       navigationController?.popViewControllerAnimated(true)
     }
   }
+
+  // inviteContact is the function responsible for actually inviting a user to the collection's
+  // syncgroup.
+  func inviteContact(contact: Contact) throws {
+    if let user = contact.user {
+      // Admin allows them to invite people to the syncgroup as well.
+      try todoList.collection!.syncgroup().inviteUser(user, level: .READ_WRITE_ADMIN)
+    } else if let emails = contact.emails {
+      for email in emails {
+        try todoList.collection!.syncgroup().inviteUser(User(alias: email), level: .READ_WRITE_ADMIN)
+      }
+    }
+  }
+
+  // MARK: Discovery
+
+  func onFound(user: User) {
+    dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)) {
+      // Determine if they're an existing contact.
+      self.contactsByLowercaseEmailMu.lock()
+      defer { self.contactsByLowercaseEmailMu.unlock() }
+      var contact = Contact(user: user)
+      var section = Category.Nearby.rawValue
+      if let existingContact = self.contactsByLowercaseEmail[user.alias.lowercaseString] {
+        contact = existingContact
+        contact.user = user
+        section = Category.NearbyContacts.rawValue
+      }
+      // If we were never able to load contacts, then they should be disabled and we only have 1
+      // section.
+      if !self.didLoadContacts {
+        section = 0
+      }
+      dispatch_async(dispatch_get_main_queue()) {
+        self.people[section].append(contact)
+        self.people[section].sortInPlace { (lhs, rhs) -> Bool in
+          return lhs.description.compare(rhs.description) == NSComparisonResult.OrderedAscending
+        }
+        self.tableView.reloadSections(NSIndexSet(index: section), withRowAnimation: .Automatic)
+      }
+    }
+  }
+
+  func onLost(user: User) {
+    for section in nearbySections {
+      if let idx = people[section].indexOf({ $0.user == user }) {
+        people[section].removeAtIndex(idx)
+        tableView.reloadSections(NSIndexSet(index: section), withRowAnimation: .Automatic)
+        break
+      }
+    }
+  }
+
+  var nearbySections: [Int] {
+    if didLoadContacts {
+      return [Category.NearbyContacts.rawValue, Category.Nearby.rawValue]
+    }
+    return [0]
+  }
 }
 
+// MARK: Search
+
 // Filters people to match the text user searched for.
 extension InviteViewController: UISearchControllerDelegate, UISearchResultsUpdating {
   func updateSearchResultsForSearchController(searchController: UISearchController) {
-    // Start with all people, then filter.
-    searchResults = people
-
     if let searchText = searchController.searchBar.text where searchText.characters.count > 0 {
-      for category in Category.allRawValues {
-        searchResults[category] = searchResults[category].filter { person -> Bool in
-          return person.name.rangeOfString(searchText,
+      for section in nearbySections {
+        searchResults[section] = people[section].filter { contact -> Bool in
+          return contact.description.rangeOfString(searchText,
             options: .CaseInsensitiveSearch,
-            range: person.name.startIndex ..< person.name.endIndex,
+            range: contact.description.startIndex ..< contact.description.endIndex,
             locale: nil) != nil
         }
       }
+      if didLoadContacts {
+        searchResults[Category.Contacts.rawValue] = []
+        if let contacts = try? CNContactStore().unifiedContactsMatchingPredicate(
+          CNContact.predicateForContactsMatchingName(searchText),
+          keysToFetch: Contact.keysToFetch) {
+            searchResults[Category.Contacts.rawValue] = contacts.map { Contact(contact: $0) }
+        }
+      }
+    } else {
+      for i in 0 ..< searchResults.count {
+        searchResults[i] = []
+      }
     }
-
-    searchResultsFound = searchResults.flatten().count > 0
+    searchResultsFound = false
+    for section in searchResults {
+      if !section.isEmpty {
+        searchResultsFound = true
+        break
+      }
+    }
     tableView.reloadData()
   }
 }
 
+// MARK: Tableview Cells
+
 // Displays a person with name and their profile photo.
 class PersonCell: UITableViewCell {
-  @IBOutlet weak var photoImageView: UIImageView!
+  static let height: CGFloat = 44
   @IBOutlet weak var nameLabel: UILabel!
-  var person: Person?
+  var contact: Contact?
 
   func updateView() {
-    nameLabel.text = person?.name
-//    if let imageName = person?.imageName {
-//      photoImageView.image = UIImage(named: imageName)
-//    }
+    nameLabel.text = contact?.description
   }
 }
 
 // Display a cell that shows the email the invite will be sent to.
 class SendEmailCell: UITableViewCell {
+  static let height: CGFloat = 74
   @IBOutlet weak var emailLabel: UILabel!
 }
\ No newline at end of file
diff --git a/SyncbaseTodosApp/SyncbaseTodos/Person.swift b/SyncbaseTodosApp/SyncbaseTodos/Person.swift
deleted file mode 100644
index 96c101c..0000000
--- a/SyncbaseTodosApp/SyncbaseTodos/Person.swift
+++ /dev/null
@@ -1,30 +0,0 @@
-// 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
-
-final class Person: Jsonable {
-  var name: String = ""
-  var imageRef: String = ""
-  var email: String = ""
-
-  init(name: String, imageRef: String, email: String) {
-    self.name = name
-    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
index 44c8a83..11c3c36 100644
--- a/SyncbaseTodosApp/SyncbaseTodos/Syncbase.swift
+++ b/SyncbaseTodosApp/SyncbaseTodos/Syncbase.swift
@@ -80,6 +80,34 @@
   dispatch.unwatch(eventHandler)
 }
 
+// MARK: Invites
+
+private var inviteHandler: SyncgroupInviteHandler?
+
+func startInviteHandler() throws {
+  if let handler = inviteHandler {
+    try Syncbase.database().removeSyncgroupInviteHandler(handler)
+    inviteHandler = nil
+  }
+  let handler = SyncgroupInviteHandler(
+    onInvite: { (invite: SyncgroupInvite) in
+      // Accept all invites automatically.
+      print("Accepting invite \(invite)")
+      try! Syncbase.database().acceptSyncgroupInvite(invite, callback: { (sg, err) in
+        if let err = err {
+          print("Unable to accept invite: \(err)")
+        } else {
+          print("Accepted invite into syncgroup \(sg)")
+        }
+      })
+    },
+    onError: { (err: ErrorType) in
+      print("Unable to accept invites: \(err)")
+  })
+  try Syncbase.database().addSyncgroupInviteHandler(handler)
+  inviteHandler = handler
+}
+
 // MARK: Watch low-level events
 private var watchHandler: WatchChangeHandler?
 
@@ -177,7 +205,6 @@
 
     // 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 }) {
@@ -200,7 +227,7 @@
   dispatch.notify(events)
 }
 
-// MARK: MODEL API
+// MARK: Model API
 
 func addList(list: TodoList) throws {
   let data = try NSJSONSerialization.dataWithJSONObject(list.toJsonable(), options: [])
diff --git a/SyncbaseTodosApp/SyncbaseTodos/TasksViewController.swift b/SyncbaseTodosApp/SyncbaseTodos/TasksViewController.swift
index c079601..f1ec924 100644
--- a/SyncbaseTodosApp/SyncbaseTodos/TasksViewController.swift
+++ b/SyncbaseTodosApp/SyncbaseTodos/TasksViewController.swift
@@ -40,9 +40,16 @@
   }
 
   func onEvents(events: [ModelEvent]) {
+    // TODO(zinman): Make this more fine-grained
     print("got events: \(events)")
     tableView.reloadData()
   }
+
+  override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
+    if let ivc = segue.destinationViewController as? InviteViewController {
+      ivc.todoList = todoList
+    }
+  }
 }
 
 /// Handles tableview functionality, including rendering and swipe actions. Tap actions are
diff --git a/SyncbaseTodosApp/SyncbaseTodos/TodoList.swift b/SyncbaseTodosApp/SyncbaseTodos/TodoList.swift
index 74cc28c..1b4f8f1 100644
--- a/SyncbaseTodosApp/SyncbaseTodos/TodoList.swift
+++ b/SyncbaseTodosApp/SyncbaseTodos/TodoList.swift
@@ -10,7 +10,7 @@
   var collection: Collection? = nil
   var name: String = ""
   var updatedAt: NSDate = NSDate()
-  var members: [Person] = []
+  var members: [Contact] = []
   var tasks: [Task] = []
 
   init(name: String, updatedAt: NSDate = NSDate()) {
@@ -43,4 +43,4 @@
     }
     return TodoList(name: name, updatedAt: NSDate(timeIntervalSince1970: updatedAt))
   }
-}
+}
\ No newline at end of file
diff --git a/SyncbaseTodosApp/SyncbaseTodos/TodosViewController.swift b/SyncbaseTodosApp/SyncbaseTodos/TodosViewController.swift
index 27b7b50..38f27f6 100644
--- a/SyncbaseTodosApp/SyncbaseTodos/TodosViewController.swift
+++ b/SyncbaseTodosApp/SyncbaseTodos/TodosViewController.swift
@@ -9,6 +9,7 @@
 class TodosViewController: UIViewController {
   @IBOutlet weak var tableView: UITableView!
   @IBOutlet weak var addButton: UIBarButtonItem!
+  @IBOutlet weak var toggleSharingButton: UIBarButtonItem!
   // The menu toolbar is shown when the "edit" navigation bar is pressed.
   @IBOutlet weak var menuToolbar: UIToolbar!
   @IBOutlet weak var menuToolbarTopConstraint: NSLayoutConstraint!
@@ -21,11 +22,20 @@
 
   override func viewDidLoad() {
     super.viewDidLoad()
-    // Hide the bottom menu by default
+    // Hide the bottom menu by default.
     menuToolbarTopConstraint.constant = -menuToolbar.frame.size.height
     handler = ModelEventHandler(onEvents: onEvents)
     do {
       try startWatching()
+      try startInviteHandler()
+      // Turn sharing (advertising) on by default, and ensure the text reflects the current initial
+      // state properly.
+      if Syncbase.isAdvertisingPresenceInNeighborhood() {
+        toggleSharingButton.title = "Turn sharing off"
+      } else {
+        toggleSharingButton.title = "Turn sharing on"
+        toggleSharing()
+      }
     } catch {
       print("Unable to start watch: \(error)")
     }
@@ -145,7 +155,7 @@
     })
     ac.addAction(UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Cancel, handler: nil))
     ac.addTextFieldWithConfigurationHandler { $0.placeholder = "My New List" }
-    self.presentViewController(ac, animated: true, completion: nil)
+    presentViewController(ac, animated: true, completion: nil)
   }
 
   func addTodoList(name: String) {
@@ -163,12 +173,30 @@
     }
   }
 
-  @IBAction func debug() {
-    // TODO(azinman): Make real
-  }
-
   @IBAction func toggleSharing() {
-    // TODO(azinman): Make real
+    let isAdvertising = Syncbase.isAdvertisingPresenceInNeighborhood()
+    do {
+      if isAdvertising {
+        try Syncbase.stopAdvertisingPresenceInNeighborhood()
+        toggleSharingButton.title = "Turn sharing on"
+      } else {
+        // Advertise to everyone (we could optionally pick specific users, but no need for this app).
+        try Syncbase.startAdvertisingPresenceInNeighborhood()
+        toggleSharingButton.title = "Turn sharing off"
+      }
+    } catch {
+      var message = "turning off sharing"
+      if !isAdvertising {
+        message = "turning on sharing"
+      }
+      print("Error \(message): \(error)")
+      let ac = UIAlertController(
+        title: "Oops!",
+        message: "Error \(message). Try again.",
+        preferredStyle: .Alert)
+      ac.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
+      presentViewController(ac, animated: true, completion: nil)
+    }
   }
 
   func completeAllTasks(indexPath: NSIndexPath) {