“Tell Them What You’re Going To Tell Them”
• InstallaFon and setup walk-‐through
• Demo of Grocery Sync sample app
• Tour of Grocery Sync code with digressions about the API
and the document model
and querying
• Beyond Grocery Sync
GeNng Up And Running
• Download Couchbase Lite
• Download Grocery Sync sample code
• Copy framework into sample app folder
• Build & Run
1. Download Couchbase Litewww.couchbase.com/communi:es/couchbase-‐lite
Download Grocery Syncgithub.com/couchbaselabs/Grocery-‐Sync-‐iOS
Plug In The Framework
option
Live Demo
A Tour of the Code and the API
Ini;aliza;onDemoAppDelegate.m:64
// Initialize Couchbase Lite and find/create my database: NSError* error; self.database = [[CBLManager sharedInstance] createDatabaseNamed: kDatabaseName error: &error]; if (!self.database) [self showAlert: @"Couldn't open database" error: error fatal: YES];
CBLManager
Database “otherdb”
CBLDatabase “db”
CBLDocument “doc3”
CBLDocument “doc2”
Document “doc1”
CBLDocument “doc1” !{ “text”: “Vacuum”, “created”: “2013-10-08”, “check”: false }
thumb.jpg
Manager, Databases, Documents
Manager
• Collec;on of named databases
• Generally a singleton Unless you run on mul:ple threads
• Manages database storage (local directory)
Database
• Namespace for documents
• Contains views and their indexes
• Contains valida;on func;ons
• Source and target of replica;on
Document
• Has unique ID within its database
• Contains arbitrary* JSON object *except keys that start with “_” are reserved
There is no explicit, enforced schema
Denormaliza:on is OK — use arrays or dic:onaries
• May contain binary aVachments Data blobs, can be large, tagged with MIME type
• Versioned Mul:-‐Version Concurrency Control (MVCC)
Every update creates a revision ID (based on digest of contents)
Revision ID history is stored
Ini;aliza;onDemoAppDelegate.m:64
// Initialize Couchbase Lite and find/create my database: NSError* error; self.database = [[CBLManager sharedInstance] createDatabaseNamed: kDatabaseName error: &error]; if (!self.database) [self showAlert: @"Couldn't open database" error: error fatal: YES];
Crea;ng a Database ViewRootViewController.m:101
// Define a view with a map function that indexes to-‐do items by creation date: [[theDatabase viewNamed: @"byDate"] setMapBlock: MAPBLOCK({ id date = doc[@"created_at"]; if (date) emit(date, doc); }) reduceBlock: nil version: @"1.1"];
Not a UIView — a database “view” is like an index.
View “completed”
Views & Queries
CBLDatabase “db”
CBLView “byDate”
function(doc) { emit(doc.created, doc.title); }map function
key value docID
“2013-‐03-‐12” “taxes” “doc17”
“2013-‐09-‐30” “call mom” “doc62”
“2013-‐10-‐17” “cat food” “doc82”
“2013-‐10-‐17” “tea bags” “doc83”
“2013-‐10-‐22” “upgrade” “doc90”
view index
CBLQuery}
Views
• Map/Reduce mechanism Popular in other NoSQL databases
A view is similar to index in rela:onal database.
• App-‐defined map func;on Called on every document
Can emit arbitrary key/value pairs into the index
• Op;onal reduce func;on Data aggrega:on / grouping
• Func;ons are registered as na;ve blocks/callbacks Not stored as JavaScript in “design document” (as in CouchDB)
Crea;ng a Database ViewRootViewController.m:101
// Define a view with a map function that indexes to-‐do items by creation date: [[theDatabase viewNamed: @"byDate"] setMapBlock: MAPBLOCK({ id date = doc[@"created_at"]; if (date) emit(date, doc); }) reduceBlock: nil version: @"1.1"];
Not a UIView — a database “view” is like an index.
Driving the Table from a View QueryRootViewController.m:101
// Create a query sorted by descending date, i.e. newest items first: CBLLiveQuery* query = [[[database viewNamed:@"byDate"] query] asLiveQuery]; query.descending = YES; ! // Plug the query into the CBLUITableSource, which will use it to drive the table. // (The CBLUITableSource uses KVO to observe the query's .rows property.) self.dataSource.query = query; self.dataSource.labelProperty = @"text";
@property(nonatomic, strong) IBOutlet UITableView *tableView; @property(nonatomic, strong) IBOutlet CBLUITableSource* dataSource;
RootViewController.h:41
Queries
• Basic feature set Key ranges, offset/limit, reverse, group by key…
No joins or fancy sor:ng
but compound keys (and clever emits) allow for some tricks
• LiveQuery subclass Monitors database, pushes no:fica:ons
Uses KVO on iOS, can drive a UITableView
• Upcoming goodies! Full-‐text indexing
Geo (bounding-‐box) queries
Live Queries & Table Data Sources
key value docID
“2013-‐09-‐30” “Pencil shavings”“doc62”
“2013-‐10-‐17” “Mangos” “doc82”
“2013-‐10-‐17” “second” “doc83”
view index CBLLiveQuery
CBLQuery}data source
CBLUI-‐TableSource
Driving the Table from a View QueryRootViewController.m:101
// Create a query sorted by descending date, i.e. newest items first: CBLLiveQuery* query = [[[database viewNamed:@"byDate"] query] asLiveQuery]; query.descending = YES; ! // Plug the query into the CBLUITableSource, which will use it to drive the table. // (The CBLUITableSource uses KVO to observe the query's .rows property.) self.dataSource.query = query; self.dataSource.labelProperty = @"text";
@property(nonatomic, strong) IBOutlet UITableView *tableView; @property(nonatomic, strong) IBOutlet CBLUITableSource* dataSource;
RootViewController.h:41
Wiring Up The Table ViewRootViewController.xib
Displaying Table CellsDemoAppDelegate.m:131
-‐ (void)couchTableSource:(CBLUITableSource*)source willUseCell:(UITableViewCell*)cell forRow:(CBLQueryRow*)row { // Set the cell background and font: ……… // Configure the cell contents. Map function (above) copies the doc properties // into its value, so we can read them without having to load the document. NSDictionary* rowValue = row.value; BOOL checked = [rowValue[@"check"] boolValue]; if (checked) { cell.textLabel.textColor = [UIColor grayColor]; cell.imageView.image = [UIImage imageNamed:@"checked"]; } else { cell.textLabel.textColor = [UIColor blackColor]; cell.imageView.image = [UIImage imageNamed: @"unchecked"]; } // cell.textLabel.text is already set, thanks to setting up labelProperty }
Responding To TapsDemoAppDelegate.m:131
-‐ (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { // Ask CBLUITableSource for the corresponding query row, and get its document: CBLQueryRow *row = [self.dataSource rowAtIndex:indexPath.row]; CBLDocument *doc = row.document; ! // Toggle the document's 'checked' property: NSMutableDictionary *docContent = [doc.properties mutableCopy]; BOOL wasChecked = [docContent[@"check"] boolValue]; docContent[@"check"] = @(!wasChecked); ! // Save changes: NSError* error; if (![doc.currentRevision putProperties: docContent error: &error]) { [self showErrorAlert: @"Failed to update item" forError: error]; } }
Adding New ItemsDemoAppDelegate.m:247
-‐(void)textFieldDidEndEditing:(UITextField *)textField { // Get the name of the item from the text field: NSString *text = addItemTextField.text; if (text.length == 0) { return; } addItemTextField.text = nil; ! // Create the new document's properties: NSDictionary *inDocument = @{ @"text": text, @"check": @NO, @"created_at": [CBLJSON JSONObjectWithDate: [NSDate date]] }; // Save the document: CBLDocument* doc = [database untitledDocument]; NSError* error; if (![doc putProperties: inDocument error: &error]) { [self showErrorAlert: @"Couldn't save new item" forError: error];
Dele;ng ItemsDemoAppDelegate.m:191
-‐ (NSArray*)checkedDocuments { NSMutableArray* checked = [NSMutableArray array]; for (CBLQueryRow* row in self.dataSource.rows) { CBLDocument* doc = row.document; if ([doc[@"check"] boolValue]) [checked addObject: doc]; } return checked; } !!-‐ (void)deleteCheckedDocuments { NSError* error; if (![dataSource deleteDocuments: self.checkedDocuments error: &error]) { [self showErrorAlert: @"Failed to delete items" forError: error]; } }
OK, But Where’s The Sync?
Crea;ng Replica;onsDemoAppDelegate.m:291
// Tell the database to use this URL for bidirectional sync. // This call returns an array of the pull and push replication objects: NSArray* repls = [self.database replicateWithURL: newRemoteURL exclusively: YES]; if (repls) { _pull = repls[0]; _push = repls[1]; _pull.continuous = _push.continuous = YES; // Observe replication progress changes, in both directions: NSNotificationCenter* nctr = [NSNotificationCenter defaultCenter]; [nctr addObserver: self selector: @selector(replicationProgress:) name: kCBLReplicationChangeNotification object: _pull]; [nctr addObserver: self selector: @selector(replicationProgress:) name: kCBLReplicationChangeNotification object: _push]; }
Replica;on
Database “db”
Replica;onDir: push Remote: hkp://server/db Auth: <token>
Replica;onDir: pull Remote: hkp://server/db Auth: <token>
notifications
Replica;on
• Each Replica;on is one-‐direc;onal (push or pull)
• Replica;ons can be one-‐shot, con;nuous or persistent One-‐shot: Stops when complete.
Con:nuous: Keeps monitoring changes :ll app quits
Persistent: Automa:cally starts again on next relaunch
• Replicator runs in a background thread It detects online/offline, handles connec:on errors, retries…
You just see document-‐changed or query-‐changed no:fica:ons.
• Progress is observable through KVO or NSNo;fica;on
Monitoring Replica;onsDemoAppDelegate.m:353
// Called in response to replication-‐change notifications. Updates the progress UI. -‐ (void) replicationProgress: (NSNotificationCenter*)n { if (_pull.mode== kCBLReplicationActive || _push.mode== kCBLReplicationActive) { // Sync is active -‐-‐ aggregate progress of both replications: unsigned completed = _pull.completed + _push.completed; unsigned total = _pull.total + _push.total; [self showSyncStatus]; // Update the progress bar, avoiding divide-‐by-‐zero exceptions: progress.progress = (completed / (float)MAX(total, 1u)); } else { // Sync is idle -‐-‐ hide the progress bar and show the config button: [self showSyncButton]; } ! // Check for any change in error status and display new errors: NSError* error = _pull.error ? _pull.error : _push.error; if (error != _syncError) { _syncError = error; if (error) [self showErrorAlert: @"Error syncing" forError: error];
Beyond Grocery Sync
Models
Task
List
Task
Task
@interface Task : CBLModel !@property NSString*title; @property NSDate* created; @property bool checked; @property List* list; !@end
@interface List : CBLModel !@property NSString* title; @property NSArray* members; !@end
Document “doc23”
Document “doc82”
Document “doc99”
Document “doc3”
Models
• Kind of like NSManagedObject, but simpler
• Map JSON to na;ve @proper;es Scalar types (int, bool…), String, Date, Data (blob)
References to other doc models
Arrays of the above
• Proper;es are KV-‐observable
• Models provide mutable state -‐save: writes to underlying document
• No query-‐based rela;on support (yet)
Represen;ng Document Types
• There are no tables to separate different record types! Conven:on is to use a “type” property
• Map func;ons can pick out docs with the right type if (doc.type == “item”) emit(doc.created, doc.text);
To-‐Do List With ModelsFrom ToDoLite project
Task* task = [Task modelForDocument: row.document]; cell.textLabel.text = task.text; cell.textLabel.textColor = task.checked ? [UIColor grayColor] : [UIColor blackColor];
@interface Task : CBLModel !@property NSString*title; @property NSDate* created; @property bool checked; @property List* list; !@end
Querying With Mul;ple ListsFrom ToDoLite project
[view setMapBlock: MAPBLOCK({ if ([doc[@"type"] isEqualToString: kTaskDocType]) { id date = doc[@"created_at"]; NSString* listID = doc[@"list_id"]; emit(@[listID, date], doc); } }) reduceBlock: nil version: @"4"];
key docID
[“list1”, “2013-‐09-‐30”] “doc82”
[“list2”, “2013-‐06-‐02”] “doc62”
[“list2”, “2013-‐10-‐17”] “doc83”
[“list2”, “2013-‐10-‐28”] “doc90”
[“list3”, “2013-‐01-‐01”] “doc01”
Querying With Mul;ple ListsFrom ToDoLite project
CBLQuery* query = [view query]; query.descending = YES; NSString* myListId = self.document.documentID; query.startKey = @[myListId, @{}]; query.endKey = @[myListId];
key docID
[“list1”, “2013-‐09-‐30”] “doc82”
[“list2”, “2013-‐06-‐02”] “doc62”
[“list2”, “2013-‐10-‐17”] “doc83”
[“list2”, “2013-‐10-‐28”] “doc90”
[“list3”, “2013-‐01-‐01”] “doc01”
{
Whew!
hkp://couchbase.github.io/couchbase-‐lite-‐ios/docs/html/annotated.html
hkp://docs.couchbase.com/couchbase-‐lite/cbl-‐ios/
hkps://github.com/couchbaselabs/Grocery-‐Sync-‐iOS
Coming Up Next!
Tuesday, November 5, 10:00AM PST
“Developing With Android”