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