webinar - developing with couchbase lite and ios
DESCRIPTION
Learn how to develop with Couchbase Lite for iOS in this technical lecture led by Jens Alfke, lead Couchbase Lite iOS developer. The session will include a look into the Objective-C native APIs, using a walkthrough of a demo app. Other topics include: A 5-minute recap of the Couchbase Lite architecture A step-by-step demonstration on how to develop an iOS application with Couchbase Lite An explanation of the future plans for Couchbase Lite iOSTRANSCRIPT
“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/download#cb-‐mobile
Download Grocery Syncgithub.com/couchbaselabs/Grocery-‐Sync-‐iOS
Plug In The Framework
option
Run & Sync
Live Demo
A Tour of the Code and the API
IniFalizaFonDemoAppDelegate.m:64
// Initialize Couchbase Lite and find/create my database: NSError* error; self.database = [[CBLManager sharedInstance] databaseNamed: 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
• CollecFon of named databases
• Generally a singleton Unless you run on mulEple threads
• Manages database storage (local directory)
Database
• Namespace for documents
• Contains views and their indexes
• Contains validaFon funcFons
• Source and target of replicaFon
Document
• Has unique ID within its database
• Contains arbitrary* JSON object *except keys that start with “_” are reserved
There is no explicit, enforced schema
DenormalizaEon is OK — use arrays or dicEonaries
• May contain binary aaachments Data blobs, can be large, tagged with MIME type
• Versioned MulE-‐Version Concurrency Control (MVCC)
Every update creates a revision ID (based on digest of contents)
Revision ID history is stored
IniFalizaFonDemoAppDelegate.m:64
// Initialize Couchbase Lite and find/create my database: NSError* error; self.database = [[CBLManager sharedInstance] databaseNamed: kDatabaseName error: &error]; if (!self.database) [self showAlert: @"Couldn't open database" error: error fatal: YES];
CreaFng 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 relaEonal database
• App-‐defined map funcFon Called on every document
Can emit arbitrary key/value pairs into the index
• OpFonal reduce funcFon Data aggregaEon / grouping
• FuncFons are registered as naFve blocks/callbacks Not stored as JavaScript in “design document” (as in CouchDB)
CreaFng 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:69
// 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 sorEng
but compound keys (and clever emits) allow for some tricks
• LiveQuery subclass Monitors database, pushes noEficaEons
Uses KVO on iOS, can drive a UITableView
• iOS 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:69
// 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
Combining CreaFng+Querying
// Returns a query for all the lists in a database. + (CBLQuery*) queryListsInDatabase: (CBLDatabase*)db { CBLView* view = [db viewNamed: @"lists"]; if (!view.mapBlock) { // Register the map function, the first time we access the view: [view setMapBlock: MAPBLOCK({ if ([doc[@"type"] isEqualToString:kListDocType]) emit(doc[@"title"], nil); }) reduceBlock: nil version: @"1"]; // bump version any time you change the MAPBLOCK body! } return [view createQuery]; }
Wiring Up The Table ViewRootViewController.xib
Displaying Table CellsRootViewController.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 TapsRootViewController.m:167
-‐ (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 ItemsRootViewController.m:248
-‐(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 createDocument]; NSError* error; if (![doc putProperties: inDocument error: &error]) { [self showErrorAlert: @"Couldn't save new item" forError: error];
DeleFng ItemsRootViewController.m:189
-‐ (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?
CreaFng ReplicaFonsRootViewController.m:293
_pull = [self.database createPullReplication: newRemoteURL]; _push = [self.database createPushReplication: newRemoteURL]; _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]; [_push start]; [_pull start];
ReplicaFon
Database “db”
ReplicaFonDir: push Remote: hlp://server/db Auth: <token>
ReplicaFonDir: pull Remote: hlp://server/db Auth: <token>
notifications
ReplicaFon
• Each ReplicaFon is one-‐direcFonal (push or pull)
• ReplicaFons can be one-‐shot or conFnuous One-‐shot: Stops when complete.
ConEnuous: Keeps monitoring changes Ell app quits
• Replicator runs in a background thread It detects online/offline, handles connecEon errors, retries…
You just see document-‐changed or query-‐changed noEficaEons.
• Progress is observable through KVO or NSNoFficaFon
Monitoring ReplicaFonsRootViewController.m:355
// Called in response to replication-‐change notifications. Updates the progress UI. -‐ (void) replicationProgress: (NSNotificationCenter*)n { if (_pull.status==kCBLReplicationActive || _push.status==kCBLReplicationActive) { // Sync is active -‐-‐ aggregate progress of both replications: unsigned completed = _pull.completedChangesCount + _push.completedChangesCount; unsigned total = _pull.changesCount + _push.changesCount; [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.lastError ? _pull.lastError : _push.lastError; 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 naFve @properFes Scalar types (int, bool…), String, Date, Data (blob)
References to other doc models
Arrays of the above
• ProperFes are KV-‐observable
• Models provide mutable state -‐save: writes to underlying document
• No query-‐based relaFon support (yet)
RepresenFng Document Types
• There are no tables to separate different record types! ConvenEon is to use a “type” property
• Map funcFons 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 MulFple 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 MulFple 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!
hlp://developer.couchbase.com/mobile/develop/guides/couchbase-‐lite/index.html
hap://github.com/couchbaselabs/Grocery-‐Sync-‐iOS
haps://github.com/couchbaselabs/ToDoLite-‐iOS
Q & A