Introduction

This guide provides information for developers who want to use Couchbase Lite to develop apps for iOS devices. To get an overview of Couchbase Lite, read the Couchbase Lite Concepts Guide.

This guide assumes you are familiar with developing software for iOS devices. If you are new to iOS development, read Apple’s iOS App Programming Guide before beginning to work with Couchbase Lite.

Getting Started

This section contains the information you need to start developing iOS apps with Couchbase Lite. It lists the system requirements, shows how to build a sample app, and explains how to add the Couchbase Lite iOS framework to your project.

System Requirements

To develop Couchbase Lite apps for iOS, you need:

Getting Started in 5 Minutes

This section shows you how to download and build a sample app named ToDo Lite. ToDo Lite is a shared to-do list app that demonstrates many Couchbase Lite features.

Before You Begin

Before you can build the ToDo Lite app, make sure you have the following tools installed on your computer. You might already have some of them installed.

  1. Download Xcode.

  2. Download Couchbase Lite for iOS and move it to a permanent location.

  3. Download and install Git.

Building Todo Lite

To build the Todo Lite app:

  1. Open the Terminal application.

    Terminal is usually located in the Applications/Utilities folder.

  2. Change to the directory that you want to store the ToDo Lite app in. For example:

    $ cd ~/dev
    
  3. Clone the Todo Lite app:

    $ git clone https://github.com/couchbaselabs/ToDoLite-iOS.git
    
  4. In the downloaded Couchbase Lite for iOS folder, find the CouchbaseLite.framework folder.

  5. Copy the CouchbaseLite.framework folder to the Frameworks folder inside the ToDoLite-iOS folder.

  6. In Xcode, select an iOS simulator build scheme.

  7. Click Run.

    The ToDo Lite app opens in the iOS simulator.

Adding Couchbase Lite To Your App

To add Couchbase Lite to your own app, you need to add the Couchbase Lite framework and other frameworks to your target and modify the build options, and then initialize Couchbase Lite in your app.

To add the frameworks:

  1. Download the latest release of Couchbase Lite for iOS and move it to a permanent location.

  2. From the Couchbase Lite folder, drag the CouchbaseLite.framework folder to the Frameworks group in the Xcode Project Navigator.

  3. In the Choose options for adding these files sheet, make sure that your app target is selected.

  4. Open the project editor for your app target and click the Build Settings tab.

  5. In the Linking section, on the Other Linker Flags row, add the flag -ObjC (be sure to use the capitalization shown).

  6. Click the Build Phases tab.

  7. In the Link Binary With Libraries section, click + and add the following items:

    • CFNetwork.framework
    • Security.framework
    • SystemConfiguration.framework
    • libsqlite3.dylib
    • libz.dylib
  8. Build your app to make sure there are no errors.

To initialize Couchbase Lite in your app:

You initialize Couchbase Lite in your app delegate, which is usually named YourPrefixAppDelegate, as follows:

  1. In the app delegate header file, add the following import directive:

     #import <CouchbaseLite/CouchbaseLite.h>
    
  2. In the app delegate header file, add the following property declaration:

     @property (strong, nonatomic) CBLDatabase *database;
    
  3. In the app delegate implementation file, add the following code to the application:didFinishLaunchingWithOptions: method:

     // create a shared instance of CBLManager
     CBLManager *manager = [CBLManager sharedInstance];
    
     // create a database
     NSError *error;
     self.database = [manager createDatabaseNamed: @"my-database" error: &error];
    

    You should also add appropriate error checking code after each call. If either call fails, you might need to display an error message and exit.

    The legal characters for the database name are: lowercase letters [a-z], digits [0-9], and special characters [$_()+-/].

Tutorial

This tutorial shows you how to use Couchbase Lite in your iOS apps. The tutorial assumes you already know how to develop iOS apps and have Xcode 5 or later installed on your computer.

If you need to learn about iOS programming, a good place to start is by reading Apple’s free tutorial, Start Developing iOS Apps Today, which provides a hands-on introduction to iOS app development. To access the Apple tutorial, you need to register for a free Apple developer account.

HelloCBL

The HelloCBL tutorial presents detailed steps for creating a Couchbase Lite app from scratch. The tutorial demonstrates:

  • Adding Couchbase Lite dependencies to an app.
  • Creating a database.
  • Creating a document.
  • Saving a document in the database.
  • Retrieving a document from the database.

To make the first example in the tutorial easier to understand, the program structure is simplified. Only one new method, sayHello, is created and all the Couchbase Lite APIs used in the example are placed within that method. The sayHello method is called from the application:didFinishLaunchingWithOptions: method in the application delegate class. All output is sent to the Xcode console, rather than the iPhone screen in the simulator. The example does not use any graphics and does not require setting up a user interface. Rest assured, the other sample iOS apps do not take these shortcuts—they incorporate standard iOS software design and development practices.

You can follow along with the tutorial and create your own HelloCBL, or you can download HelloCBL from GitHub.

Step 1: Create a new project

  1. Open Xcode and select File > New > Project.
  2. In the new project template sheet, click Empty Application and then click Next.
  3. In the new project options sheet, enter values for each field and then click Next.

    Here are the values used in the sample app:

    • Product Name—HelloCBL
    • Organization Name—Couchbase
    • Company Identifier—com.couchbase
    • Bundle Identifier—com.couchbase.HelloCBL
    • Class Prefix—HC
    • Devices—iPhone
    • Use Core Data—no
  4. Select a location for your new project, and then click Create.

Step 2: Add the Couchbase Lite dependencies

  1. Download the latest release of Couchbase Lite for iOS and move the unzipped Couchbase Lite folder to a permanent location.

  2. Open the Couchbase Lite folder and drag the CouchbaseLite.framework folder to the Frameworks group in the Xcode project navigator.

  3. In the Choose options for adding these files sheet, make sure that your app target is selected.

  4. In the navigator, click on the HelloCBL project file to open the project editor for your app, and then click the Build Settings tab.

  5. Scroll to the Linking section, find the Other Linker Flags row, and then add the flag -ObjC (be sure to use the capitalization shown).

    The Other Linker Flags row should look similar to the following screenshot:

  6. Click the Build Phases tab.

  7. Expand the Link Binary With Libraries section and add the following items:

    • CFNetwork.framework
    • Security.framework
    • SystemConfiguration.framework
    • libsqlite3.dylib
    • libz.dylib

    Click the + at the bottom of the section to add each item. When you are done, it should look similar to the following screenshot:

Step 3: Add the Hello Couchbase Lite code

  1. Open the HCAppDelegate.m file.

  2. Add the following code to the #import section:

     #import "CouchbaseLite/CouchbaseLite.h"
     #import "CouchbaseLite/CBLDocument.h"
    

    These statements import the Couchbase Lite framework headers needed by the sayHello method.

  3. Just before the return statement at the end of the application:didFinishLaunchingWithOptions: method, add the following code:

     // Run the method that creates a database, and then stores and retrieves a document
     BOOL result = [self sayHello];
     NSLog (@"This Hello Couchbase Lite run %@!", (result ? @"was a total success" : @"was a dismal failure"));
    

    The first line calls the sayHello method, which demonstrates the basic Couchbase Lite iOS APIs. The second line executes after the return from the sayHello method and prints a message on the console that indicates whether the run was successful.

  4. Just before the @end statement at the end of the file, add the following method:


// creates a database, and then creates, stores, and retrieves a document
- (BOOL) sayHello {
   
    // holds error error messages from unsuccessful calls
    NSError *error;
    
    // create a shared instance of CBLManager
    CBLManager *manager = [CBLManager sharedInstance];
    if (!manager) {
        NSLog (@"Cannot create shared instance of CBLManager");
        return NO;
    }
    
    // create a name for the database and make sure the name is legal
    NSString *dbname = @"my-new-database";
    if (![CBLManager isValidDatabaseName: dbname]) {
        NSLog (@"Bad database name");
        return NO;
    }
    
    // create a new database
    CBLDatabase *database = [manager databaseNamed: dbname error: &error];
    if (!database) {
        NSLog (@"Cannot create database. Error message: %@", error.localizedDescription);
        return NO;
    }
    
    // create an object that contains data for the new document
    NSDictionary *myDictionary =
        [NSDictionary dictionaryWithObjectsAndKeys:
            @"Hello Couchbase Lite!", @"message",
            [[NSDate date] description], @"timestamp",
            nil];
    
    // display the data for the new document
    NSLog (@"This is the data for the document: %@", myDictionary);
    
    // create an empty document
    CBLDocument* doc = [database createDocument];
    
    // write the document to the database
    CBLRevision *newRevision = [doc putProperties: myDictionary error: &error];
    if (!newRevision) {
        NSLog (@"Cannot write document to database. Error message: %@", error.localizedDescription);
    }
    
    // save the ID of the new document
    NSString *docID = doc.documentID;
    
    // retrieve the document from the database
    CBLDocument *retrievedDoc = [database documentWithID: docID];
    
    // display the retrieved document
    NSLog(@"The retrieved document contains: %@", retrievedDoc.properties);
    
    return YES;

}

The sayHello method creates a new database, and then creates a document, stores the document in the database, and retrieves the document. This section contains additional notes that supplement the comments in the code.

The sayHello method creates a shared CBLManager object that manages a collection of databases. The CBLManager object can be used only in a single thread.

After sayHello creates a name for the new database, it validates the name. A database name can consist of only lowercase alphabetic characters (a-z), digits (0-9) and a few special characters (_$()+-/), so it’s important to validate the name.

To create the database, it calls createDatabaseNamed:error, which is a method in the CBLManager class that returns a CBLDatabase object. Immediately after the call, it checks to make sure the database was created.

NSDictionary objects provide JSON-compatible representations of data that are suitable for creating documents that you can store in the database. The document created by sayHello is an NSDictionary object named myDictionary that contains only two keys, message and timestamp. message contains the string “Hello Couchbase Lite!”, and timestamp contains the time and date the document was created. The document content is written out to the console to show its content.

An empty CBLDocument object named doc is created, and then the document is saved to the database by using the CBLDocument class putProperties:error: method. This method returns a CBLRevision object, which is checked to make sure the document was written successfully.

When the document is saved to the database, Couchbase Lite generates a document identifier property named _id and a revision identifier property named _rev, and adds them to the document.

The saved document is retrieved from the database by using the CBLDatabase class documentWithID: method. The retrieved document is written out to the console to show its content, which now includes the _id and _rev properties created by Couchbase Lite.

Step 4: Build and run HelloCBL

  1. Set the active scheme to the iOS simulator for iPhone Retina (4-inch):

  2. Click Run.

    The iOS simulator opens, but you’ll just see a white screen. All output from the app is shown in the console.

  3. View the console output.

    The console output should look similar to the following screenshot. Don’t worry about the error message at the end of the console output that mentions the view controller—the view controller has been omitted from this bare-bones app.

Congratulations! You’ve just created your first Couchbase Lite app!

Developing Apps

This section shows how to use the native Objective-C API for common tasks. For detailed information about the native API, see the Couchbase Lite Objective-C API Reference Manual.

Working With Databases

Depending on your app design, you might need to set up an initial database the first time a user launches your app and then connect to the existing database upon subsequent launches. Each time the app launches, you must check whether the database exists in Couchbase Lite.

When your app is launched for the first time, you need to set up a database.

Database names must begin with a lowercase letter. The following characters are valid in database names:

  • lowercase letters: a-z
  • numbers: 0-9
  • special characters: _$()+-/

Setting Up the Initial Database

You can set up the initial database in your app by using any of the following methods:

Creating a Database in Your App

To create a database in your app, you need to create a CBLDatabase instance by using the databaseNamed:error: method provided in the CBLManager class. Typically, this is done in the app delegate header and implementation files. The following code fragments show an example.

In the AppDelegate.h file, import Couchbase Lite and use a property to declare the database object:

// AppDelegate.h file
    
#import <CouchbaseLite/CouchbaseLite.h>
    ...
@property (strong, nonatomic) CBLDatabase *database;

In the application:didFinishLaunchingWithOptions: method in the AppDelegate.m file, create the database:

// create a shared instance of CBLManager
CBLManager *manager = [CBLManager sharedInstance];
        
// create a database
NSError *error;
self.database = [manager databaseNamed: @"my-database" error: &error];

Pulling a Database From a Server

TBD

Installing a Prebuilt Database

For some use cases you might want to install a database along with your app. Consider the following pros and cons when deciding whether to include a database with your app:

Pros:

  • Generally, it’s faster to download a database as part of the app, rather than creating one through the replication protocol.
  • Shifts bandwidth away from your servers.
  • Improves the first-launch user experience.

Cons:

  • Changing the initial contents requires resubmitting the app to the App Store.
  • Including the database with the app increases its disk usage on the device.

To use a prebuilt database, you need to set up the database, build the database into your app bundle as a resource, and install the database during the initial launch.

Setting Up the Database

You need to make the database as small as possible. Couchbase Lite keeps a history of every document and that takes up space.

When creating the database locally, you can make it smaller by storing each document (via a PUT request) only once, rather than updating it multiple times. If the documents are updated only once, each document revision ID starts with 1-.

If you start with a snapshot of a live database from a server, then create a new local database and replicate the source database into it. If you didn’t start the replication with an empty local database, call -compact on it afterwards to get rid of any older revision and attachment data.

The Couchbase Lite Xcode project has a target called LiteServ that builds a small Mac app that does nothing but run the REST API. LiteServ is a useful tool for creating databases and running replications locally on your development machine.

Extracting and Building the Database

By default, the local database is in the Application Support directory tree of the app you used to create the database. The main database file has a .cblite extension. If your database has attachments, you also need the databasename attachments directory that’s adjacent to it.

Add the database file and the corresponding attachments directory to your Xcode project. If you add the attachments folder, make sure that in the Add Files sheet you select the Create folder references for any added folders radio button, so that the folder structure is preserved; otherwise, the individual attachment files are all added as top-level bundle resources.

Installing the Database

After your app launches and creates a CBLDatabase instance for its database, it needs to check whether the database exists. If the database does not exist, copy it from the app bundle. The code looks like this:

CBLManager* dbManager = [[CBLManager sharedInstance] init];
CBLDatabase* database = [dbManager databaseNamed: @"catalog"
                                           error: &error];
if (!database) {
    NSString* cannedDbPath = [[NSBundle mainBundle] pathForResource: @"catalog"
                                                             ofType: @"cblite"];
    NSString* cannedAttPath = [[NSBundle mainBundle] pathForResource: @"Catalog attachments"
                                                              ofType: @""];
    BOOL ok = [dbManager replaceDatabaseNamed: @"catalog"
                             withDatabaseFile: cannedDbPath
                              withAttachments: cannedAttPath
                                        error: &error];
    NSAssert(ok, @"Failed to install database: %@", error);
    CBLDatabase* database = [dbManager existingDatabaseNamed: @"catalog"
                                               error: &error];
    NSAssert(database, @"Failed to open database");
}

Connecting to an Existing Database

After the initial launch of your app, you need to connect to the existing database on the device each time the app is launched.

Working With Documents

The CBLDocument class represents a document. A CBLDocument knows its database and document ID, and can cache the document’s current contents. The contents are represented as parsed JSON—an NSDictionary, whose keys are NSString objects and whose values can be any of the classes NSString, NSNumber, NSNull, NSArray or NSDictionary. (Note that, unlike native Cocoa property lists, NSData and NSDate are not supported.)

Creating A Document

You create a new document when the user creates a persistent data item in your app, such as a reminder, a photograph or a high score. To save the data, construct a JSON-compatible representation of the data, instantiate a new CBLDocument and save the data to it.

Here’s an example from the Grocery Sync demo app:

NSDictionary *contents = 
     @{@"text"       : text,
       @"check"      : [NSNumber numberWithBool:NO],
       @"created_at" : [CBLJSON JSONObjectWithDate: [NSDate date]]};

Next, ask the CBLDatabase object, which you instantiated when you initialized Couchbase Lite, to create a new document. This doesn’t add anything to the database yet—just like the New command in a typical Mac or Windows app, the document is not stored on disk until you save some data into it. Continuing from the previous example:

CBLDocument* doc = [database createDocument];

When you create a document by calling the createDocument: method, Couchbase Lite generates a random unique identifier (a long string of hex digits) for it. You can choose your own identifier by calling [database documentWithID: someID] instead. Remember that your identifier has to be unique so you don’t get a conflict error when you save it.

Finally, save the contents to the document:

NSError* error;
if (![doc putProperties: contents error: &error]) {
   [self showErrorAlert: @"Couldn't save the new item"]
}

Reading A Document

If later on you want to retrieve the contents of a document, you need to obtain the CBLDocument object representing it and then get the contents from that object.

You can get a CBLDocument in the following ways:

  • If you know its ID (maybe you kept it in memory, maybe you got it from NSUserDefaults, or even from a property of another document), you can call [database existingDocumentWithID:]. This method loads the document from the database or returns nil if the document does not exist.

  • If you are iterating the results of a view query or allDocument, which is a special view, you can get it from the CBLQueryRow object document property.

After you get the document object, you can get its content in any of the following ways:

  • By accessing the properties property:

    CBLDocument document = [database existingDocumentWithID: documentID];
      NSDictionary contents = document.properties;
    
  • By using the shortcut propertyForKey: method to get one property at a time:

    NSString* text = [document propertyForKey: @"text"];
      BOOL checked = [[document propertyForKey: @"check"] boolValue];
    
  • By using the handy Objective-C collection indexing syntax (available in Xcode 4.5 or later):

    NSString* text = document[@"text"];
      BOOL checked = [document[@"check"] boolValue];
    

You might wonder which of these lines actually reads from the database. The answer is that the CBLDocument starts out empty, loads its contents on demand, and then caches them in memory — so it’s the call to document.properties in the first example, or the first propertyForKey: call in the second example. Afterwards, getting properties is as cheap as a dictionary lookup. For this reason it’s best not to keep references to huge numbers of CBLDocument objects, or you’ll end up storing all their contents in memory. Instead, rely on queries to look up documents as you need them.

Updating A Document

To update a document, you call putProperties: again. Couchbase Lite uses Multiversion Concurrency Control (MVCC) to manage changes to documents. When you update a document, you must tell Couchbase Lite which revision you updated so it can stop you if there were any updates to the document in the meantime. If it didn’t, you would wipe out those updates by overwriting them.

Documents contain a special property named _rev whose value is the current revision ID. The revision ID is a long, hex string. When you update a document, the new properties dictionary must contain a _rev key whose value is the ID of the revision that you’re updating.

The _rev property is already in the dictionary you got from the CBLDocument, so all you need to do is modify the properties dictionary and hand back the modified dictionary that still contains the _rev property to putProperties:.

The following example shows a document update:

// copy the document
NSMutableDictionary *contents = [[doc.properties mutableCopy] autorelease];

// toggle value of check property
BOOL wasChecked = [[contents valueForKey: @"check"] boolValue];
    [contents setObject: [NSNumber numberWithBool: !wasChecked] forKey: @"check"];

// save the updated document  
NSError* error;
if (![doc putProperties: contents error: &error])
   [self showErrorAlert: @"Couldn't update the item"];

In the example, the document is copied to a mutable dictionary object called contents that already contains the current revision ID in its _rev property. Then the document is modified by toggling the check property. Finally, you save the same way it was originally saved, by sending a message to the putProperties:error: method.

Handling Update Conflicts

Due to the realities of concurrent programming, the previous example code is vulnerable to a race condition. If something else updates the document in between the calls to the properties: and putProperties:error: methods, the operation fails. (The error domain is CBLHTTPErrorDomain and the error code is 409, which is the HTTP status code for Conflict.)

Even if your app is single-threaded, most Couchbase Lite apps use replication, which runs in the background. So it’s possible that one of your users might get unlucky and find that Couchbase Lite received a remote update to that very document, and inserted it a moment before he tried to save his own update. He’ll get an error about a conflict. Then he’ll try the operation again, and this time it’ll work because by now your CBLDocument has updated itself to the latest revision.

This is, admittedly, unlikely to happen in the above example because the elapsed time between getting and putting the properties is so short (microseconds, probably). It’s more likely in a situation where it takes the user a while to make a change. For example, in a fancier to-do list app the user might open an inspector view, make multiple changes, then commit them. The app would probably fetch the document properties when the user presses the edit button, let the user take as long as she wants to modify the UI controls, and then save when she returns to the main UI. In this situation, minutes might have gone by, and it’s much more likely that in the meantime the replicator pulled down someone else’s update to that same document.

We’ll show you how to deal with this, but for simplicity we’ll do it in the context of our example. The easiest way to deal with this is to respond to a conflict by starting over and trying again. By now the CBLDocument will have updated itself to the latest revision, so you’ll be making your changes to current content and won’t get a conflict.

First figure out what change to make. In this case, we want to save the new setting of the checkbox:

NSMutableDictionary *docContent = [[doc.properties mutableCopy] autorelease];
    BOOL wasChecked = [[docContent valueForKey:@"check"] boolValue];

Then we get the document contents, apply the change, and retry as long as there’s a conflict:

NSError* error = nil;
    do {
        docContent = [[doc.properties mutableCopy] autorelease];
        [docContent setObject:[NSNumber numberWithBool:!wasChecked] forKey:@"check"];
        [[doc putProperties: docContent] wait: &error];
    } while ([error.domain isEqualToString: CBLHTTPErrorDomain] && error.code == 409);

The example shows a second call to doc.properties, but it’s in the loop. The first call is redundant, but it’s vital if there’s a conflict and the loop has to execute a second time, so that docContent can pick up the new contents.

Note

A different type of document revision conflict arises as a result of replication. In that case, the conflict can't be detected in advance, so both conflicting revisions exist at once in the database and have to be merged. This type of conflict is covered in the replication section.

Deleting A Document

Deleting is a lot like updating. Instead of calling putProperties: you call deleteDocument:. Here’s an example:

NSError* error;
    if (![doc deleteDocument: &error])
        [self showErrorAlert: @"Couldn't delete the item"];

The same complications about conflicts apply. You won’t get a conflict if someone else deleted the document first, but you will if someone modified it. Then you need to decide which takes precedence, and either retry the delete or give up.

Working With Data Model Objects

Couchbase Lite has an object modeling layer that can make it easier to integrate its documents into your code. It’s similar to, though simpler than, the Apple Core Data framework NSManagedObject class. The idea is to create Objective-C classes whose instances represent documents in the database and whose native properties map to document properties.

Defining A Model Class

To create a model class, you subclass CBLModel. Here’s an example:

// ShoppingItem.h
#import <CouchbaseLite/CouchbaseLite.h>

@interface ShoppingItem : CBLModel
@property bool check;
@property (copy) NSString* text;
@property (strong) NSDate* created_at;
@end

Here’s the implementation:

// ShoppingItem.m
#import "ShoppingItem.h"

@implementation ShoppingItem
@dynamic check, text, created_at;
@end

The accessors for the properties are hooked up at runtime, and get and set the properties of the same names in the associated Couchbase Lite document. The @dynamic directive in the .m file is just there to keep the compiler from complaining that the properties aren’t implemented at compile time.

Using A Model Class

If you have a CBLDocument object, you can get a model object that corresponds to it by calling the modelForDocument: class method from the CBLModel class:

CBLDocument* doc = ...... ;
ShoppingItem* item = [ShoppingItem modelForDocument: doc];

You can create a new unsaved model object, as shown in the following example:

ShoppingItem* item = [[ShoppingItem alloc] initWithNewDocumentInDatabase: database];

You can access the document properties natively:

NSLog (@"Text is %@", item.text);
item.check = true;

Model properties support key-value coding and key-value observing. You can access any document property whether or not you’ve declared an Objective-C property for it. Just be aware that these values are always accessed in object form, without automatic conversion to and from scalar types:

NSNumber* priorityObj = [item getValueOfProperty: @"priority"];
int priority = priorityObj.intValue;

Saving Changes

Property changes affect the state of the CBLModel object but aren’t immediately saved to the document or the database. To save the property changes, you have to save the model:

NSError* error;
BOOL ok = [item save: &error];

To save property changes automatically, set the autosaves property of the CBLModel object to true. The changes are not saved after every property change, but they are saved within a brief time after one or more changes.

Finally, call deleteDocument to delete:

NSError* error;
BOOL ok = [item deleteDocument: &error];

Property Types

The automatic mapping from Objective-C types to JSON document properties is very useful, but it has some details and limitations that you should be aware of:

  • JSON types: All the object types used to represent JSON are supported: NSNumber, NSNull, NSString, NSArray, NSDictionary.
  • Scalars: Numeric properties can be declared as ordinary C numeric types like int or double. CBLModel automatically puts them into NSNumber objects.
  • bool vs. BOOL: Declare Boolean properties as type bool, not BOOL. The reason is that bool is a built-in C99 type, while BOOL is just a typedef for char. At run time it’s impossible to tell a BOOL value apart from an 8-bit integer, which means CBLModel stores them in JSON as 0 and 1, instead of false and true.

For convenience, you can also use the following non-JSON-compatible classes:

  • NSData: CBLModel saves an NSData property value by base64-encoding it into a JSON string, and reads it by base64-decoding the string. This is inefficient and expands the data size by about 50%. If you want to store large data blobs in a document, you should use attachments instead.
  • NSDate: NSDate-valued properties are converted to and from JSON strings using the ISO-8601 date format. Be aware that if you’re reading documents generated externally that didn’t store dates in ISO-8601, CBLModel won’t be able to parse them. You have to change the property type to NSString and use an NSDateFormatter object to do the parsing yourself.
  • NSDecimalNumber: This is a lesser-known Foundation class used for high-precision representation of decimal numbers without the round-off errors of floating-point. It’s used primarily for financial data. A property of this type is stored in JSON as a decimal numeric string.
  • CBLModel: You can have a property that points to another model object: a relation in database terminology. The value stored in the document is the other model’s document ID string. There are a lot of subtleties to this, so it’s explored in more detail later on.

Typed Array Properties

You can declare the type (class) of the items of an NSArray-valued property. This has the following purposes:

  • It enforces that all items of the array have that type. If a document’s JSON array contains mismatched or incorrect types, the property value will be nil and a warning will be logged. This can help avoid run-time exceptions in your code.
  • It lets you use supported non-JSON classes in arrays, namely NSData, NSDate and NSDecimalNumber. If you don’t specify the item class, the array items will just be NSString objects, just as they are in the JSON.
  • It lets you have arrays of other models, also known as to-many relationships. For example, a Book model class could have a property authors that’s an NSArray of Author models. (In the document the property value will be a JSON array of document-ID strings.)

To declare the item class of an array property, implement a class method whose name is of the form “propertyItemClass” that returns the appropriate class object. For example:

+ (Class) holidaysItemClass {return [NSDate class];}
+ (Class) authorsItemClass {return [Author class];}

Relationships Between Models

So far we’ve seen properties whose values are JSON types like integers, strings or arrays. CBLModel also supports properties whose values are pointers to other CBLModel objects. This creates what are generally called relationships between objects. For example, a blog comment would have a relationship to the post that it refers to.

In the actual document, a relationship is expressed by a property whose value is the ID of the target document. CBLModel knows this convention, so if you declare an Objective-C dynamic property whose type is a pointer to a CBLModel subclass, then at run time the property value is looked up like this:

Objective-C property --> document property --> database (by ID) --> document --> model

For example, you might have documents for blog comments and each blog comment has a post property whose value is the document ID of the blog post it refers to. You can model that like this:

@class BlogPost;

@interface BlogComment : CBLModel
@property (assign) BlogPost* post;
@end

In the implementation of BlogComment you declare the property as @dynamic, like any other model property.

Note that the declaration uses (assign) instead of the more typical (retain). This is because a relationship to another model doesn’t retain it to avoid creating reference-loops that can lead to memory leaks. Couchbase Lite takes care of reloading the destination model if necessary when you access the property. Also, Couchbase Lite does not deallocate models with unsaved changes.

Dynamic Subclassing and the CBLModelFactory

So far, if you declare a property’s type as being BlogPost*, the instantiated object is a BlogPost. But what if BlogPost has subclasses? In a Tumblr-style app, there might be different types of posts, such as text, image, and video, differentiated by the value of a type property, and you want these to be instantiated as subclasses like TextPost, ImagePost and VideoPost. How do you tell the property which class to instantiate for which document when the property type doesn’t narrow it down to one class?

The CBLModelFactory singleton object keeps a registry that maps document type property values to classes. If at launch time you register the type strings and the corresponding BlogPost subclasses, then CBLModel consults this when instantiating model-reference properties. So the value of the post property of a comment is a TextPost, ImagePost, or VideoPost depending on the document’s type.

After you’ve started using the CBLModelFactory, you’ll probably want to start instantiating models for existing documents by calling modelForDocument: on CBLModel itself, rather than a subclass. This is because

[CBLModel modelForDocument: doc]

uses the factory to decide at run time which class to instantiate based on the document’s contents, while

[BlogPost modelForDocument: doc]

always creates a BlogPost object, even if the document’s type indicates that it should get an ImagePost or VideoPost, which is probably not what you want.

Working With Views and Queries

The basic document API gets you pretty far, but most apps need to work with multiple documents. In a typical app, the top-level UI usually shows either all the documents or a relevant subset of them — in other words, the results of a query.

Querying a Couchbase Lite database involves first creating a view that indexes the keys you’re interested in and then running a query to get the results of the view for the key or range of keys you’re interested in. The view is persistent, like a SQL index.

Because there’s no fixed schema for the view engine to refer to and the interesting bits of a document that we want it to index could be located anywhere in the document (including nested values inside of arrays and subobjects), the view engine has to let us pick through each document to identify the relevant key (or keys) and values. That’s what the view’s map function is for: it’s an app-defined function that’s given a document’s contents and returns, or emits, zero or more key-value pairs. These key-value pairs get indexed, ordered by key, and can then be queried efficiently, again by key.

Example: An Address Book

For example, if you have an address book in a database, you might want to query the cards by first or last name, for display or filtering purposes. To do that, you create two views: one to grab the first-name field and return it as the key, and the other to return the last-name field as the key. (And what if you were originally just storing the full name as one string? Then your functions can detect that, split the full name at the space, and return the first or last name. That’s how schema evolution works.)

You might also want to be able to look up people’s names from phone numbers so you can do Caller ID on incoming calls. To do this, make a view whose keys are phone numbers. Now, a document might have multiple phone numbers in it, like the following example JSON document:

{
   "first":"Bob",
   "last":"Dobbs",
   "phone":{
      "home":"408-555-1212",
      "cell":"408-555-3774",
      "work":"650-555-8333"
   }
}

No problem—the map function just needs to loop over the phone numbers and emit each one. You then have a view index that contains each phone number, even if several of them map to the same document.

Getting All Documents

To start off, for simplicity, this section shows how you can retrieve all documents in the database without using a view.

To retrieve all the documents in the database, you need to create a CBLQuery object. The createAllDocumentsQuery method in the CBLDatabase class returns a new CBLQuery object that contains all documents in the database:

CBLQuery* query = [database createAllDocumentsQuery];

After you obtain the new CBLQuery object, you can customize it (this is similar to the SQL SELECT statement ORDER BY, OFFSET and LIMIT clauses). The following example shows how to retrieve the ten documents with the highest keys:

query.limit = 10;
query.descending = YES;

As a side effect you get the documents in reverse order, but you can compensate for that if it’s not appropriate. Now you can iterate over the results:

NSError *error;
CBLQueryEnumerator *rowEnum = [query run: &error];
for (CBLQueryRow* row in rowEnum) {
        NSLog(@"Doc ID = %@", row.key);
}

[query run: &error] evaluates the query and returns an NSEnumerator object that you can use with a for...in loop to iterate over the results. Each result is a CBLQueryRow object. You might expect the result to be a CBLDocument, but the key-value pairs emitted in views don’t necessarily correspond one-to-one to documents and a document might be present multiple times under different keys. If you want the document that emitted a row, you can get it from the row’s document property.

Creating Views

To create a view, define its map (and optionally its reduce) function. When you define the MapReduce functions, you also assign a version identifier to the function. If you change the MapReduce function later, you must remember to change the version so Couchbase Lite rebuilds the index.

Here’s how the Grocery Sync example app sets up its by-date view:

CBLView* view = [db viewNamed: @"byDate"];

[view setMapBlock: MAPBLOCK({
    id date = [doc objectForKey: @"created_at"];
    if (date) emit(date, doc);
}) version: @"1.0"];

The name of the view is arbitrary, but you need to use it later on when querying the view. The interesting part here is the MAPBLOCK expression, which is a block defining the map function. If you get an error about “too many arguments provided to function-like macro invocation,” it just means the preprocess is confused. Try putting parentheses around the expression with commas in it. MAPBLOCK is a preprocessor macro used to simplify the declaration of the block. Here’s what a block looks like without it:

^(NSDictionary* doc, void (^emit)(id key, id value)) {
    id date = [doc objectForKey: @"created_at"];
    if (date) emit(date, doc);
}

This is a block that takes the following parameters:

  • An NSDictionary—this is the contents of the document being indexed.
  • A function (a block) called emit that takes the parameters key and value. This is the function your code calls to emit a key-value pair into the view’s index.

After you get that, the example map block is straightforward: it looks for a created_at property in the document, and if it’s present, it emits it as the key, with the entire document contents as the value. Emitting the document as the value is fairly common. It makes it slightly faster to read the document at query time, at the expense of some disk space.

The view index then consists of the dates of all documents, sorted in order. This is useful for displaying the documents ordered by date (which Grocery Sync does), or for finding all documents created within a certain range of dates.

Any document without a created_at field is ignored and won’t appear in the view index. This means you can put other types of documents in the same database (such as names and addresses of of grocery stores) without them messing up the display of the shopping list.

Note

The view index itself is persistent, but the setMapBlock:reduceBlock:version: and setMapBlock:version: methods must be called every time the app starts, before the view is accessed. You need to call them because the map function is not persistent—it's an ephemeral block pointer that needs to be hooked up to Couchbase Lite at run time.

Querying Views

Now that the view is created, querying it is very much like querying createAllDocumentsQuery, except that you get the CBLQuery object from the view rather than the database:

CBLQuery* query = [[db viewNamed: @"byDate"] createQuery];

Every call to createQuery creates a new CBLQuery object, ready for you to customize. You can set a number of properties to specify key ranges, ordering, and so on. as described in the view and query design section. Then you run the query exactly as described previously in Getting All Documents.

Updating Queries

It can be useful to know whether the results of a query have changed. You might have generated some complex output, like a fancy graph, from the query rows, and would prefer to save the work of recomputing the graph if nothing has changed. You can accomplish this by keeping the CBLQueryEnumerator object around, and then later checking its stale property. This property returns true if the results are out of date:

if (rowEnum.stale) {
   rowEnum = [query run: &error];
}

Using Live Queries

Even better than checking for a query update is getting notified when one happens. Users expect apps to be live and don’t want to have to press a refresh button to see new data. This is especially true if data might arrive over the network at any time through synchronization — that new data needs to show up right away.

For this reason Couchbase Lite has a very useful subclass of CBLQuery called CBLLiveQuery, which has a rows property that updates automatically as the database changes. The rows property is observable using Cocoa’s key-value Observing (KVO) mechanism. That means you can register for immediate notifications when it changes, and use those to drive user-interface updates.

To create a CBLLiveQuery you just ask a regular query object for a live copy of itself. You can then register as an observer:

self.liveQuery = query.asLiveQuery;
[self.liveQuery addObserver: self forKeyPath: @"rows" options: 0 context: NULL];

Don’t forget to remove the observer when cleaning up. The observation method might look like this:

- (void) observeValueForKeyPath: (NSString*)keyPath 
                       ofObject: (id)object
                         change: (NSDictionary*)change
                        context: (void*)context
{
    if (object == self.liveQuery) {
        for (CBLQueryRow* row in object.rows) {
            // update the UI
        }
    }
}

Using an Automatic Table Source

And what’s even better than a live query? A live query that automatically acts as the data source of a UITableView. That’s what CBLUITableSource provides: it’s an implementation of UITableViewDataSource that observes a CBLLiveQuery and syncs the table with the view rows. To use it, you need to:

  1. Instantiate a CBLUITableSource object. One easy way is to put one in the same .xib as the table view.
  2. Set its tableView property to the UITableView. This is an IBOutlet so you can wire it up.
  3. Set its query property to a CBLLiveQuery.
  4. Set its labelProperty property to the name of a property in the view row’s value (or in the associated document). The value of this property is the text that is displayed in the table cell’s label.

If you want more control over the label, or want to use a fancier cell with more than just text, you can implement the CBLUITableDelegate protocol and set that object as the table source’s delegate. This gives you a number of optional methods you can implement that will allow you to substitute your own UITableCell and handle errors.

View and Query Design

All Matching Results

If you run the query without setting any key ranges, the result is all the emitted rows, in ascending order by key (date, in this example.) To reverse the order, set the query’s descending property.

Exact Queries

To get only the rows with specific keys, set the query’s keys property to an array of the desired keys:

query.keys = @[aSpecificDate];

The order of the keys in the array doesn’t matter; the results are returned in ascending-key order.

Key Ranges

To get a range of keys, set the query’s startKey and endKey properties. The range is inclusive, that is, the result includes the rows with key equal to endKey.

One common source of confusion is combining key ranges with descending order. Remember that you’re specifying the starting and ending keys, not the minimum and maximum values. In a descending query, the startKey should be the maximum value and the endKey the minimum value.

Compound Keys

The real power of views comes when you use compound keys. If your map function emits arrays as keys, they are sorted as you would expect: the first elements are compared, and if they’re equal the second elements are compared, and so forth. This lets you sort the rows by multiple criteria (like store and item), or group together results that share a criterion.

For example, if a map function emitted the document’s store and item properties as a compound key:

emit(@[doc[@"store"], doc[@"item"]], nil);

then the view’s index contains a series of keys ordered like this:

...
["Safeway", "goldfish crackers"]
["Safeway", "tonic water"]
["Trader Joe's", "chocolate chip cookies"]
["Whole Foods", "cruelty-free chakra lotion"]
...
Compound-Key Ordering

The ordering of compound keys depends entirely on how you want to query them; the broader criteria go to the left of the narrower ones. For some queries you might need a different ordering than for others. If so, you need to define a separate view for each ordering. For example, the above ordering is good for finding all the items to buy at a particular store. If instead you want to look up a specific item and see what stores to get it at, you’d want the compound keys in the opposite order. So you could define views called “stores” and “items”, and query whichever one is appropriate.

Compound-Key Ranges

The way you specify the beginning and end of compound-key ranges can be a bit unintuitive. For example, you have a view whose keys are of the form [store, item] and you want to find all the items to buy at Safeway. What are the startKey and endKey? Clearly, their first elements are @"Safeway", but what comes after that? You need a way to specify the minimum and maximum possible keys with a given first element. The code to set the start and keys looks like this:

query.startKey = @[ @"Safeway" ];
query.endKey = @[ @"Safeway", @{} ];

The minimum key with a given first element is just a length-1 array with that element. (This is just like the way that the word “A” sorts before any other word starting with “A”.)

The maximum key contains an empty NSDictionary object. Couchbase Lite defines a sorting or collation order for all JSON types, and JSON objects (also known as dictionaries) sort after everything else. An empty dictionary is kind of like a “Z” on steroids: it’s a placeholder that sorts after any string, number, or array. It looks weird at first, but it’s a useful idiom used in queries to represent the end of a range.

Working With Replications

This section describes how to work with replications in an iOS app. To learn more about replications, read about Replication in the Couchbase Lite Concepts Guide.

Creating replications

Replications are represented by CBLReplication objects. You create replication objects by calling one of the following methods on your local CBLDatabase object:

  • replicationFromURL: sets up a pull replication.
  • replicationToURL: sets up a push replication.
  • replicationsWithURL:exclusively: sets up a bidirectional replication.

Creating a replication object does not start the replication automatically. To start a replication, you need to send a start message to the replication object.

Newly created replications are noncontinuous. To change the settings, you need to immediately set their continuous properties.

It’s not strictly necessary to keep references to the replication objects, but you do need them if you want to monitor their progress.

replicationsWithURL:exclusively:, which creates a bidirectional replication, returns an NSArray that contains a pull replication in the first element and a push replication in the second element. The following example shows how to set up and start a bidirectional replication:

CBLReplication *pull;
CBLReplication *push;

NSArray* repls = [self.database replicationsWithURL: newRemoteURL 
                                        exclusively: YES];
                                        
self.pull = [repls objectAtIndex: 0];
self.push = [repls objectAtIndex: 1];

[self.pull start];
[self.push start];

The exclusively: YES option seeks out and removes any pre-existing replications with other remote URLs. This is useful if you sync with only one server at a time and just want to change the address of that server.

Monitoring replication progress

A replication object has several properties you can observe to track its progress. You can use the following CBLReplication class properties to monitor replication progress:

  • completedChangesCount—number of documents copied so far in the current batch
  • changesCount—total number of documents to be copied
  • lastError—set to an NSError if the replication fails or nil if there has not been an error since the replication started.
  • status—an enumeration named CBLReplicationStatus that indicates the current state of the replication. The status can be stopped, offline, idle or active. Stopped means the replication is finished or hit a fatal error. Offline means the server is unreachable over the network. Idle means the replication is continuous but there is currently nothing left to be copied. Active means the replication is currently transferring data.

In general, you can just observe the completedChangesCount property:

[self.pull addObserver: self 
            forKeyPath: @"completedChangesCount" 
               options: 0 
               context: NULL];

[self.push addObserver: self 
            forKeyPath: @"completedChangesCount" 
               options: 0 
               context: NULL];

Your observation method might look like this:

- (void) observeValueForKeyPath:(NSString *)keyPath
                       ofObject:(id)object
                         change:(NSDictionary *)change
                        context:(void *)context
{
    if (object == pull || object == push) {
        unsigned completed = pull.completedChangesCount + push.completedChangesCount;
        unsigned total = pull.changesCount + push.changesCount;
        if (total > 0 && completed < total) {
            [self showProgressView];
            [progressView setProgress: (completed / (float)total)];
        } else {
            [self hideProgressView];
        }
    }
}

In the example, progressView is a UIProgressView object that shows a bar graph of the current progress. The progress view is shown only while replication is active, that is, when total is nonzero.

Don’t expect the progress indicator to be completely accurate. It might jump around because the changesCount property changes as the replicator figures out how many documents need to be copied. It might not advance smoothly because some documents, such as those with large attachments, take longer to transfer than others. In practice, the progress indicator is accurate enough to give the user an idea of what’s going on.

Deleting replications

You can cancel continuous replications by deleting them. The following example shows how to delete a replication by deleting the associated CBLModel object, repl:

[repl deleteDocument: &error];

The following example shows how to delete all replications involving a database. In the example, db is a CBLDatabase object.

[db replicationsWithURL: nil exclusively: YES];

Document validation

Pulling from another database requires some trust because you are importing documents and changes that were created elsewhere. Aside from issues of security and authentication, how can you be sure the documents are even formatted correctly? Your application logic probably makes assumptions about what properties and values documents have, and if you pull down a document with invalid properties it might confuse your code.

The solution to this is to add a validation function to your database. The validation function is called on every document being added or updated. The function decides whether the document should be accepted, and can even decide which HTTP error code to return (most commonly 403 Forbidden, but possibly 401 Unauthorized).

Validation functions aren’t just called during replication—they see every insertion or update, so they can also be used as a sanity check for your own application code. If you forget this, you might occasionally be surprised by getting a 403 Forbidden error from a document update when a change is rejected by one of your own validation functions.

Here’s an example validation function definition from the Grocery Sync sample code. This is a real-life example of self-protection from bad data. At one point during development, the Android Grocery Sync app was generating dates in the wrong format, which confused the iOS app when it pulled down the documents created on Android. After the bug was fixed, the affected docs were still in server-side databases. The following validation function was added to reject documents that had incorrect dates:

[theDatabase setValidationNamed: @"created_at" asBlock: VALIDATIONBLOCK({
        if (newRevision.isDeletion)
            return;
        id date = (newRevision.properties)[@"created_at"];
        if (date && ! [CBLJSON dateWithJSONObject: date]) {
            [context rejectWithMessage: [@"invalid date " stringByAppendingString: [date description]]];
        }
    })];

The validation block ensures that the document’s created_at property, if present, is in valid ISO-8601 date format. The validation block takes the following parameters:

  • newRevision—a CBLRevision object that contains the new document revision to be approved
  • context— an id<CBLValidationContext> object that you can use to communicate with the database

A validation block should look at newRevision.properties, which is the document content, to determine whether the document is valid. If the revision is invalid, you can either call the rejectWithMessage: method on the context object to customize the error returned or just call the reject method.

The example validation block first checks whether the revision is deleted. This is a common idiom: a tombstone revision marking a deletion usually has no properties at all, so it doesn’t make sense to check their values. Another reason to check for deletion is to enforce rules about which documents are allowed to be deleted. For example, suppose you have documents that contain a property named status and you want to disallow deletion of any document whose status property was not first set to completed. Making that check requires looking at the current value of the status property, before the deletion. You can get the currently active revision from the currentRevision property of context. This is very useful for enforcing immutable properties or other restrictions on the changes can be made to a property. The CBLValidationContext property changedKeys is also useful for checking these types of conditions.

You can optionally define schemas for your documents by using the powerful JSON-Schema format and validate them programmatically. To learn how to do that, see Validating JSON Objects.

Filtered replications

You might want to replicate only a subset of documents, especially when pulling from a huge cloud database down to a limited mobile device. For this purpose, Couchbase Lite supports user-defined filter functions in replications. A filter function is registered with a name. It takes a document’s contents as a parameter and simply returns true or false to indicate whether it should be replicated.

Filtered pull

Filter functions are run on the source database. In a pull, that would be the remote server, so that server must have the appropriate filter function. If you don’t have admin access to the server, you are restricted to the set of already existing filter functions.

To use an existing remote filter function in a pull replication, set the replication’s filter property to the filter’s full name, which is the design document name, a slash, and then the filter name:

pull.filter = @"grocery/sharedItems";

Filtered pulls are how Couchbase Lite can encode the list of channels it wants Sync Gateway to replicate, although in the case of Sync Gateway, the implementation is based on indexes, not filters.

Filtered push

During a push, the filter function runs locally in Couchbase Lite. As with MapReduce functions, the filter function is specified at run time as a native block pointer. Here’s an example of defining a filter function that passes only documents with a "shared" property with a value of true:

database setFilterNamed: @"sharedItems"
                asBlock: FILTERBLOCK({
                   return [[doc objectForKey: @"shared"] booleanValue];
                })];

This function can then be plugged into a push replication by name:

push.filter = @"sharedItems";

Parameterized filters

Filter functions can be made more general-purpose by taking parameters. For example, a filter could pass documents whose "owner" property has a particular value, allowing the user name to be specified by the replication. That way there doesn’t have to be a separate filter for every user.

To specify parameters, set the filterParams property of the replication object. Its value is a dictionary that maps parameter names to values. The dictionary must be JSON-compatible, so the values can be any type allowed by JSON.

Couchbase Lite filter blocks get the parameters as a params dictionary passed to the block.

Deleting documents with filtered replications

Deleting documents can be tricky in the context of filtered replications. For example, assume you have a document that has a worker_id field, and you set up a filtered replication to pull documents only when the worker_id equals a certain value.

When one of these documents is deleted, it does not get synched in the pull replication. Because the filter function looks for a document with a specific worker_id, and the deleted document won’t contain any worker_id, it fails the filter function and therefore is not synched.

This can be fixed by deleting documents in a different way. Because a document is considered deleted as long as it has the special _deleted field, it is possible to delete the document while still retaining the worker_id field. Instead of using the DELETE verb, you instead use the PUT verb. You definitely need to set the _deleted field for the document to be considered deleted. You can then either retain the fields that you need for filtered replication, like the worker_id field, or you can retain all of the fields in the original document.

Replication conflicts

Replication is a bit like merging branches in a version control system (for example, pushing and pulling in Git). Just as in version control, you can run into conflicts if incompatible changes are made to the same data. In Couchbase Lite this happens if a replicated document is changed differently in the two databases, and then one database is replicated to the other. Now both of the changes exist there. Here’s an example scenario:

  1. Create mydatabase on device A.
  2. Create document ‘doc’ in mydatabase. Let’s say its revision ID is ‘1-foo’.
  3. Push mydatabase from device A to device B. Now both devices have identical copies of the database.
  4. On device A, update ‘doc’, producing new revision ‘2-bar’.
  5. On device B, update ‘doc’ differently, producing new revision ‘2-baz’. (No, this does not cause an error. The different revision 2 is in a different copy of the database on device A, and device B has no way of knowing about it yet.)
  6. Now push mydatabase from device A to device B again. (Transferring in the other direction would lead to similar results.)
  7. On device B, mydatabase now has two current revisions of ‘doc’: both ‘2-bar’ and ‘2-baz’.

You might ask why the replicator allows the two conflicting revisions to coexist, when a regular PUT doesn’t. The reason is that if the replicator were to give up and fail with a 409 Conflict error, the app would be in a bad state. It wouldn’t be able to resolve the conflict because it doesn’t have easy access to both revisions (the other revision is on the other device). By accepting conflicting revisions, the replicator allows apps to resolve the conflicts by operating only on local data.

What happens on device B now when the app tries to get the contents of ‘doc’? For simplicity, Couchbase Lite preserves the illusion that one document ID maps to one current revision. It does this by choosing one of the conflicting revisions, ‘2-baz’ as the “winner” that is returned by default from API calls. If the app on device B doesn’t look too close, it thinks that only this revision exists and ignores the one from device A.

You can detect conflicts in the following ways:

  • Call -[CBLDocument getConflictingRevisions:] and check for multiple returned values.
  • Create a view that finds all conflicts by having its map function look for a _conflicts property and emit a row if it’s present.

After a conflict is detected, you resolve it by deleting the revisions you don’t want and optionally storing a revision that contains the merged contents. In the example, if the app wants to keep one revision it can just delete the other one. More likely it needs to merge parts of each revision, so it can do the merge, delete revision ‘2-bar’, and put the new merged copy as a child of ‘2-baz’.

Authenticating With a Remote Database

Most remote databases require authentication (particularly for a push replication because the server is unlikely to accept anonymous writes). To sync with a remote database that requires authentication, your app needs to register credentials ahead of time so the replicator can log in to the remote server on your behalf. The registered credentials get used automatically when there’s a connection to the server.

Security Tip

Because Basic authentication sends the password in an easily readable form, it is only safe to use it over an HTTPS (SSL) connection or over an isolated network you're confident has full security. Before configuring authentication, make sure the remote database URL has the https: scheme.

Hard-coded user name and password

The simplest but least secure way to store credentials is to use the standard syntax for embedding them in the URL of the remote database:

https://frank:s33kr1t@sync.example.com/database/

This URL specifies the user name frank and password s33kr1t. If you use this as the remote URL when creating a replication, Couchbase Lite uses the included credentials. The drawback is that the password is easily readable by anything with access to your app’s data files.

Cocoa credential storage

The best way to store credentials is by using the Cocoa URL loading system, which can store credentials either in memory or in the keychain by using its authentication and credentials classes. The keychain is a secure place to store secrets. it’s encrypted with a key derived from the user’s iOS passcode and managed by a single trusted OS process. Credentials stored in the keychain get used automatically when there’s a connection to the matching server because the NSURLConnection class, Cocoa’s underlying HTTP client engine, finds it when it needs to authenticate with that same server.

You should store the credentials right after the user enters a name and password in your configuration UI. The following example shows how to register a credential for a remote database.

First, create a NSURLCredential object that contains the user name and password, and an indication of the persistence with which they should be stored:

NSURLCredential* cred;
cred = [NSURLCredential 
   credentialWithUser: @"frank"
             password: @"s33kr1t"
          persistence: NSURLCredentialPersistencePermanent];

Because this example specifies permanent persistence, the credential store writes the password securely to the keychain. If you don’t want the credentials stored in the keychain, use NSURLCredentialPersistenceForSession for the persistence setting. If you don’t store the credentials in the keychain, you need to set up the credentials each time the app is launched.

Next, create a NSURLProtectionSpace object that defines the URLs to which the credential applies:

NSURLProtectionSpace* space;

space = [[[NSURLProtectionSpace alloc] 
        initWithHost: @"sync.example.com"
                port: 443
            protocol: @"https"
               realm: @"realm name"
authenticationMethod: NSURLAuthenticationMethodDefault]
         autorelease];

The realm is an attribute of the server’s HTTP authentication configuration. It’s global to the server and not specific to a database. If you specify the wrong realm, the HTTP authentication will fail with a 401 status (as will your replication).

If you need to determine the actual realm string for the server, you can use curl or another HTTP client tool to find the realm name, which is included in the WWW-Authenticate header of the response when there is an authentication failure. Here’s an example that uses curl to get the name of the realm for a database named dbname hosted at sync.example.com/:

$ curl -i -X POST http://sync.example.com/dbname/
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="realm name"

If you try this and the server does not return a WWW-Authenticate header, you need to change the configuration to enable it.

Note

The OS is picky about the parameters of the protection space. If they don’t match exactly—including the port number and the realm string—the credentials are not used and the sync fails with a 401 error. When troubleshooting authentication failures, double-check the spelling and values for each parameter in the NSURLCredential and NSURLProtectionSpace objects.

Finally, register the credential for the protection space:

[[NSURLCredentialStorage sharedCredentialStorage]
   setDefaultCredential: cred
     forProtectionSpace: space];

OAuth

OAuth is a complex protocol that, among other things, allows a user to use an identity from one site (such as Google or Facebook) to authenticate to another site (such as a Sync Gateway server) without having to trust the relaying site with the user’s password.

Sync Gateway supports OAuth version 1 (but not the newer OAuth 2) for client authentication. If OAuth has been configured in your upstream database, you can replicate with it by providing OAuth tokens:

replication.OAuth = 
   @{ @"consumer_secret": consumerSecret,
      @"consumer_key": consumerKey,
      @"token_secret": tokenSecret,
      @"token": token };

Getting the values is somewhat tricky and involves authenticating with the origin server (the site at which the user has an account or identity). Usually you use an OAuth client library to do the hard work, such as a library from Google or Facebook.

OAuth tokens expire after some time. If they’re updated, you need to update them in the replication settings.

Validating JSON Objects

JSON-Schema is a way of defining the allowed structure and content of a JSON object in a machine-readable format, which is itself JSON. If you have a schema, you can programmatically validate JSON objects against the schema to find out if they match it.

Couchbase Lite includes a JSON-Schema validator class. However, to keep code size down, it’s not a built-in part of the framework, but the source code is included in the distribution so you can compile it into your app.

Adding the Validator Class to Your App

  1. Locate the CBLJSONValidator.m and CBLJSONValidator.h files.

    If you have the prebuilt binary distribution of Couchbase Lite, they’re in the Extras folder. If you checked out the source, they’re in the Source directory.

  2. In Xcode, add both files to your app’s target.

  3. If your app’s target uses Automatic Reference Counting (ARC), you are done setting up the validator class. Otherwise, you need to continue following these steps to enable ARC for the .m file you just added.

  4. Go to the Build Phases tab of your target’s editor.

  5. Open the Compile Sources group.

  6. Double-click in the Compiler Flags column of the CBLJSONValidator.m row.

  7. In the bubble that opens, type -fobjc-arc.

Defining a Schema

You probably want to store the schema in a JSON file, so create a new empty file in your target and give it a .json file extension. Make sure the file is included in the target’s “Copy Bundle Resources” file list.

Now fill in your schema. Note that the current implementation of the validator class follows JSON Schema Draft 4.

Validating Objects

Define a document validation block, as shown in the following example:

NSURL* url = [[NSBundle mainBundle] URLForResource: resourceName withExtension: @"json"];
    NSError* error;
    CBLJSONValidator* v = [CBLJSONValidator validatorForSchemaAtURL: url error: &error];
    NSAssert(v != nil, @"Couldn't load JSON validator: %@", error);
  #if TEST_SCHEMA
    NSAssert([v selfValidate: &error], @"Validator is invalid: %@", error);
 #endif

    [db defineValidation: @"schema" asBlock: VALIDATIONBLOCK{
        return [v validateJSONObject: newRevision.body error: NULL];
    }];

If you want, you can get the NSError returned by a validation failure and return that as the validation error message.

The call to -selfValidate: is very useful during development to catch mistakes in your schema, but you shouldn’t include it in any real builds of the app because it will load the JSON-Schema meta-schema over HTTP from json-schema.org. This will at best slow down launch, and at worst fail (triggering an assertion failure and crash) if your app is launched while offline.

Handling Deletions

Document deletion is an important special case that all validators need to handle. To Couchbase Lite a deletion is just a special revision, sometimes called a tombstone, that contains a property "_deleted": true. Typically, a deletion revision has no other properties.

Your schema needs to recognize a tombstone as a special case, otherwise it will inadvertently prevent all deletions. You do this by giving the top level of the schema a type property whose value is an array — meaning that values can have any of the given types — and make one element of the array a schema definition specifying an object with a required _deleted property whose value must be true.

Detecting Invalid Changes

A schema can only identify whether a document is structurally invalid, it can’t identify an invalid change in a document, or a valid change that the user doesn’t have permission to make. Such invalid changes are usually crucial to detect for security reasons. Some examples of invalid changes:

  • A new expense report, created by a user account that doesn’t have permission to file expense reports.
  • A revision that changes the dollar value of an existing expense report, which is supposed to be immutable.
  • A revision that changes the approval status of an expense report, submitted by a user account that doesn’t have permission to do so (e.g. isn’t in the originator’s management hierarchy.)

Cases like these still need to be checked with custom logic. CBLValidationContext has some convenience methods for this, like changedKeys and enumerateChanges:.

s

Concurrency Support

The Couchbase Lite API is mostly synchronous. That makes it simpler and more efficient. Most of the API calls are quick enough that it’s not a problem to run them on the main thread, but some things might become too slow, especially with large databases, so you might want to offload the processing to a background thread. You have a couple of options.

Warning

Couchbase Lite objects are not thread-safe. You cannot call them from multiple threads.

This means you can't solve latency problems just by calling part of your app code that uses Couchbase Lite on a background thread or dispatch queue. If you're using the same Couchbase Lite instances that you use on the main thread, you will crash or corrupt the app state.

Asynchronous Queries

View queries slow down as the database grows, especially when the view’s index needs to be updated after the database changes. You can prevent this from blocking your UI by running the query asynchronously.

The easiest way to do this is just to use CBLLiveQuery. It always runs its queries in the background, and then posts a notification on the main thread after the query is complete.

If you use a regular CBLQuery directly, though, you might find its .rows accessor getting slow. You can access the result rows asynchronously like this:

[query runAsync: ^(CBLQueryEnumerator* rows) {
        for (CBLQueryRow* row in rows) {
            // operate on row...
        }
    }];

The -runAsync method returns immediately, but performs the query on a background thread. When the query finishes, your block is called (on the original thread) with the query result as its parameter.

Error checking is a bit different. In synchronous mode query.rows returns nil if there was an error, and then query.error returns the error. The async API is stateless, so instead it always passes you a non-nil enumerator, but the enumerator has an .error property you can check. (On the other hand, errors from queries are very unlikely.)

If you ever need to debug async and live queries, there’s a logging key “Query” that will log when queries start and finish.

General-Purpose Async Calls

Also, there’s a more general-purpose method for doing async operations, which a few people have asked for. You can now use the CBLManager to perform any operation in the background. Here’s an example that deletes a bunch of documents given an array of IDs:

CBLManager* mgr = [CBLManager sharedInstance];
        [mgr asyncTellDatabaseNamed: @"mydb" to: ^(CBLDatabase *bgdb) {
            for (NSString* docID in docIDs) {
                [[bgdb documentWithID: docID] deleteDocument: nil];
        }];

You have to be careful with this, though! CBL objects are per-thread, and your block runs on a background thread, so:

  • You can’t use any of the CBL objects (databases, documents, models…) you were using on the main thread. Instead, you have to use the CBLDatabase object passed to the block, and the other objects reachable from it.
  • You can’t save any of the CBL objects in the block and then call them on the main thread. (For example, if in the block you allocated some CBLModels and assigned them to properties of application objects, bad stuff would happen if they got called later on by application code.)
  • And of course, since the block is called on a background thread, any application or system APIs you call from it need to be thread-safe.

In general, it’s best to do only very limited things using this API, otherwise it becomes too easy to accidentally use main-thread CBL objects in the block, or store background-thread CBL objects in places where they’ll be called on the main thread.

Create A Background CBLManager

If you want to do lots of Couchbase Lite stuff in the background, the best way to do it is to start your own background thread and use a new instance of CBLManager on it. Just don’t get objects mixed up between threads.

Note: Don’t call [CBLManager sharedInstance] on a background thread. That instance is for the main thread. Instead, call -copy on your main manager on the main thread, and then use that instance on the background thread.

CBLManager* bgMgr = [[CBLManager sharedInstance] copy];
    [NSThread detachNewThreadSelector: @selector(runBackground:) toTarget: self withObject: bgMgr];

Working With the REST API

Couchbase Lite has an optional HTTP-based REST API interface that provides access to nearly all Couchbase Lite functionality through HTTP requests. The API is useful when you want to:

  • Support embedded web applications that access the REST API through AJAX. These apps can easily be packaged up into native apps by using a development framework such as PhoneGap.
  • Enable apps written in other languages such as C#, which have client libraries available that can communicate with the REST API.
  • Support peer-to-peer replication over Wi-Fi between apps on two devices.

The HTTP API follows the architectural style known as REST (Representational State Transfer), in which resources identified by URLs act as objects and the basic HTTP verbs act as methods. This maps very well to the fundamental create, read, update, and delete (CRUD) operations of a database. It’s also similar to the read-write extension of HTTP, [WebDAV][WEBDAV].

Enabling the REST API

The code that handles this API isn’t in the core Couchbase Lite library, for size reasons. Instead it’s in an additional framework called CouchbaseLiteListener. To use the REST API, you need to link this framework into your app.

Once you’ve done this, you can call the method -internalURL on the top-level CBLManager instance, or on any CBLDatabase instance, to get its equivalent URL. You can then derive other entities' URLs relative to it and send them requests.

Couchbase Lite doesn’t communicate with your app over TCP sockets because it’s already built into the app; instead it fakes it by registering a fake hostname (which ends with .couchbase.). As long as your app uses the platform-standard URL access API (NSURLConnection on iOS or Mac OS), it can make HTTP requests using this fake hostname and they’ll be routed directly to the Couchbase Lite thread.

You don’t need to know the details of the hostname, and shouldn’t hardcode it as it might change (and has changed twice already!) Instead, the internalURL property of a CBLDatabase or the CBLManager gives you the URL to use.

Serving the REST API

If you want external clients to be able to connect to the REST API, to enable peer-to-peer replication, you’ll need to instantiate a CBLListener object.

#import <CouchbaseLiteListener/CBLListener.h>

CBLManager* manager = [CBLManager sharedInstance];
_listener = [[CBLListener alloc] initWithManager: manager port: 0];
_listener.readOnly = YES;  // Do this to prevent writes to your databases
_listener.passwords = @{@"naomi": @"letmein"};
BOOL ok = [_listener start];
UInt16 actualPort = _listener.port;  // the actual TCP port it's listening on

You’re now running a live HTTP server. Other devices can connect to it and replicate with your databases. You will definitely want to lock this down to prevent anything malicious from reading or changing your app’s data! Note how the above snippet makes the listener read-only, so remote clients can only pull from, not push to, your databases; it then sets a username and password, which implicitly disables anonymous access.

Being a Client

For another instance of your app to be able to connect to your listener and replicate with your databases, first it has to be able to find it. The easiest way to enable that is to turn on the Bonjour service on the listener:

[_listener setBonjourName: @"John Smith" type: @"_myshoppinglist._http._tcp"];

On the client side, you can now use an NSNetServiceBrowser to browse for services of type _myshoppinglist._http._tcp, and registered listeners will show up. Typically you display them in a table view in the UI and let the user pick the right one by name.

To connect, you resolve the chosen service’s IP address and port, construct an HTTP URL from them, append the database name, and then use that as the remote URL of a CBLReplication. Don’t forget to add credentials, since the listener is probably password-protected; you’ll probably have to prompt the user to enter them the first time.

Deployment Considerations

TBD

Troubleshooting

This section contains information to help you troubleshoot the apps you develop with Couchbase Lite.

Logging

Couchbase Lite includes some logging messages that you can enable for troubleshooting purposes. Serious and unexpected problems are always logged with a message that starts with the text “WARNING:”, but logging for serious problems needs to be enabled via the user defaults system or command-line arguments.

The Logging API

Couchbase Lite uses the logging API from my MYUtilities library. There’s a Log(@"...") function that’s used just like NSLog. It produces no output by default, but there’s a master switch to turn it on (the boolean user default also called Log.)

Beyond that, the LogTo(Channel, @"...") function logs to an arbitrary named “channel”. Channels are off by default but are individually enabled by user defaults. For example, the default LogFoo turns on the Foo channel (but only if the master log switch is enabled too.

Enabling Logging

You can turn these flags on programmatically with the NSUserDefaults class, or persistently from the command-line using the defaults tool (using your app’s bundle ID, of course). During development, the most convenient way is to set them from Xcode’s scheme editor. This lets you toggle them with GUI checkboxes. Here’s how:

  1. Pull down the scheme menu in the toolbar and choose “Edit Scheme…”
  2. Choose “Run” from the source list on the left side of the sheet.
  3. Choose the “Arguments” tab.
  4. Click the “+” button at the bottom of the “Arguments Passed On Launch” list.
  5. In the new list item that appears, type -Log YES.

This adds two command-line arguments when Xcode launches your app. An NSUserDefaults object parses these at launch time and temporarily sets the value of Log to YES (aka true.) This is persistent as long as you run your app from Xcode, but it’s not stored in the system or device user defaults so it has no effect on launching your app normally. Moreover, you can easily turn it off by using the checkbox next to its list item.

Enabling “channels” works the same way. Add another argument item whose value is, for example, -LogFoo YES to turn on channel Foo. (Remember that you also need to have the -Log YES item enabled or no logs will appear at all.)

Useful Logging Channels

Most of Couchbase Lite’s logging goes to specific channels. Here are some useful ones to turn on:

  • Sync — High-level status of sync/replication.
  • SyncVerbose — More detailed info about sync/replication.
  • RemoteRequest — The individual HTTP requests the replicator sends to the remote server.
  • View — View indexing and queries.
  • ChangeTracker — The _changes feed listener.
  • CBLRouter — If using the REST API, this logs info about the URL requests being handled.

Diagnosing Exceptions

If you hit an assertion failure or other exception in Couchbase Lite (or any other Cocoa code for that matter), here’s how to capture information about it. This will make it a lot easier to debug.

Enable a breakpoint on exceptions:

  1. In Xcode, open the breakpoints navigator (or press Cmd-6).
  2. At the bottom of the pane, click the + button.
  3. From the pop-up menu, select Add Exception Breakpoint.
  4. In the breakpoint bubble, change the top pop-up from All to Objective-C.
  5. Click Done.

The breakpoint is persistent, so you need to do this only once per project.

Capture the backtrace:

From now on, when an exception is thrown in this project, you drop into the Xcode debugger. You can of course look at the stack in the debugger GUI, but to report the exception it’s best to get a textual form of the stack backtrace. To do this, make the debugger console visible (the right-hand pane of the debugger) and enter “bt” at the “(gdb)” or “(lldb)” prompt. Then copy the output.

Contributing

If you want to contribute to Couchbase Lite for iOS, please follow the guidelines for coding and testing described in this section. You can find the latest source code for Couchbase Lite iOS on GitHub.

Coding Style

Please use the coding style guidelines and naming conventions described in this section.

Source Files

  • Put an Apache license at the top of all .m files and other files containing actual code. Update the year if necessary.

  • For indents, use 4 spaces (do not use tabs).

  • Use the following pattern for naming category files: CBLClassName+CategoryName.m.

  • In headers, use @class or @protocol forward declarations, when possible, instead of importing the class headers.

  • Try to limit lines to 100 characters wide.

  • Try to keep source files short. Under 500 lines is best and don’t go over 1000 if you can help it. If a class gets bigger, consider breaking it up into topical categories, as was done with CBLDatabase.

General Style

In general, go with Apple’s style. However, we have idiosyncrasies and would prefer that you:

  • Put spaces after the colons in messages. For example:

      [foo bar: 1 baz: 0]
    
  • Put spaces after the colons in the method declarations also. For example:

      - (void) bar: (int)bar baz: (BOOL)baz;
    
  • Put the opening curly-brace of a method or function at the end of the declaration line (not on a separate line). unless the declaration is multi-line.

  • Don’t put braces around single-line if blocks. You can use braces if you want, but please don’t go on a clean-up mission and “fix” all the existing ones.

  • Use modern Objective-C syntax, including the new shorthand for object literals and collection indexing.

The following guidelines are mandatory:

  • Declare instance variables in the @interface. If you don’t, the Mac build will fail because it still supports the old 32-bit Mac Obj-C runtime.

  • Do not declare private methods in the @interface.

  • Declare internal methods (those not part of a class’s API but needed by another source file, such as a category) in a category in CBLInternal.h, not in the public @interface.

Name Prefixes

Use the following object naming conventions:

  • Classes: CBL (CBL_ is used for some private classes to prevent name conflicts with public classes.)

  • Instance variables: _

  • Category methods on external classes: cbl_

  • Constants: kCBL (do not use ALL_CAPS)

  • Static variables: s (even if defined inside a function/method!)

  • Static functions: No prefix, just lowercase.

Testing

Couchbase Lite uses the MYUtilities unit-test framework, whose API you can find in vendor/MYUtilities/Test.h. It isn’t fully integrated with Xcode 4’s nice test support, so you can’t just choose the Product > Test menu command to run the tests.

Testing and Testability

  • Adding unit tests is encouraged! Unlike other test frameworks, MYUtilities lets you put unit tests (TestCase(Foo){ ... }) in any source file. For simple tests, you can put them at the end of the source file containing the code you’re testing. Larger test suites should go into their own source file, whose name should end with _Tests.m.
  • If you need to create classes or static functions for use by unit tests, make sure to wrap them and the tests in #if DEBUG, so they don’t take up space in a release build.
  • Use Assert() and CAssert() fairly liberally in your code, especially for checking parameters.
  • Use Warn() wherever something happens that seems wrong but shouldn’t trigger a failure.

Running Tests

Tests run when an executable target launches; they’re not a separate target the way Xcode’s regular tests are. So you’ll need to select a scheme that builds something runnable, like “Mac Demo” or “iOS Demo”.

Tests are enabled by command-line arguments whose names start with Test_. (If you don’t know how to configure the argument list, see below.)

  • If a test case is implemented in the source code as TestCase(Foo) {...}, you enable it with argument Test_Foo. You can add any number of such arguments.
  • As a shortcut, you can enable all tests via the argument Test_All.
  • By default, the app will launch normally after the unit tests pass. To disable this you can add Test_Only.

Then run the target. Test output appears in the debugger console, of course. If an assertion fails, the test will log a message, raise an exception and exit. Subsequent tests will still run, though. At the end of the run you’ll get a list of which tests failed.

Pro tip: As a shortcut to enable multiple tests, you can create an aggregate test that uses the RequireTest() macro (see below) to invoke the tests you want to run. Then you just have to enable the aggregate test.

Configuring Command-Line Arguments

  1. Select Product > Scheme > Edit Scheme (keyboard shortcut: Cmd-Shift-comma).
  2. From the list on the left side of the sheet, click the Run entry.
  3. Click the Arguments tab.
  4. In the Arguments Passed On Launch section, click + to add an argument (or multiple args separated by spaces).

Pro tip: You can disable arguments by unchecking them, so it’s very easy to toggle tests on and off.

Writing New Tests

Unit tests are basically functions, declared with special syntax:

    #import "Test.h"

    TestCase(Arithmetic) {
        CAssertEq(2 + 2, 4);
    }

This means you can put them anywhere; they don’t have to go into separate files. It is convenient to put small unit tests at the end of the source file that implements the feature being tested. That means you don’t have to jump between files so much while testing, and the tests can call static functions and internal methods without having to jump through hoops. But for larger test suites it’s cleaner to make a separate source file (named like “XXX_Tests.m”.)

Tests use a custom set of assertion macros. This isn’t strictly necessary — you can use NSAssert if you want — but I like mine better. Their names start with CAssert…, the “C” meaning that they’re callable from C functions (there are plain Assert… macros too, but they assume the existence of self and _cmd.) There’s CAssert, CAssertEq (for scalars), CAsssertEqual (for objects), etc. You can see them all in the header Testing.h.

You can use these assertion macros anywhere in the code, not just in unit tests. You can sprinkle in plenty of them.

A test can require another test as a precondition. That way it can assume that the things already tested work and doesn’t have to add assertions for them. To do this, begin a test with one or more RequireTest(Foo) calls, where Foo is the name of the test to require. (Don’t put the name in quotes.)

Precommit Smoke Test:

Before committing any code:

  • Build both the Mac and iOS demo app targets, to catch platform- or architecture-specific code.

  • Run the static analyzer (Cmd-Shift-B). There should be no issues with our code (there might be one or two issues with third-party code.)

  • Run the unit tests on both platforms: run the demo app with custom arguments Test_All and Test_Only. (This is really easy to do using Cmd-Opt-R.) All the tests must pass.

  • Review your patch hunk-by-hunk to make sure you’re not checking in unintended changes.

  • Are you fixing an issue filed in Github? If so, put something like Fixes #999 in the commit message, and Github will automatically close the issue and add a comment linking to your commit.

Release Notes

1.0 Beta 3 (March 2014)

This is the third Beta release of Couchbase Lite iOS 1.0.

Couchbase Lite is an ultra-lightweight, reliable, secure JSON database built for all your online and offline mobile application needs. The 1.0 version features native APIs, REST APIs, JSON support, and sync capability. The beta release is available to all community-edition customers.

Features

The primary focus of this release is to continue adding performance enhancements, push API name changes to follow our spec, and introduce a few minor features, for example:

  • CoreData adapter now available The CBLIncrementalStore class lets you use CoreData with Couchbase Lite (instead of SQLite) as its database.

  • Persistent replication is no longer supported We found that they were only marginally useful, confusing to understand and greatly complicated the replicator implementation. Most apps create their replications at launch time; if you do so, just remove the line that set the .persistent property and you will be fine.

  • Support for Views as changes-feed Map blocks can now determine the sequence number of a document by examining the _local_seq property of the document dictionary. This can be used to build Views that act like changes-feeds.

  • Improved feature support in CBLQuery CBLQuery improved with .startKeyDocID and .endKeyDocID now added (see 111)

  • WebSockets used in continuous sync The replicator now uses WebSockets to receive the continuous changes feed from the Sync Gateway. This should improve the performance of pull replications.

  • Major performance enhancements The replicator and Sync Gateway now GZip-compress a lot of their traffic, which saves bandwidth, and CBLDatabase.lastSequenceNumber is a lot faster to determine now.

We have also done another round of API name changes to further keep in-sync with our API spec. A complete list of these API name changes is available here.

Fixes in Beta 3

We currently do not have fixes outside of our aforementioned highlighted feature work that should be noted separately in this release.

Known Issues

We currently do not have any new known issues to highlight for this release.

1.0 Beta 2 (December 2013)

This is the second Beta release of Couchbase Lite iOS 1.0. Couchbase Lite is an ultra-lightweight, reliable, secure JSON database built for all your online and offline mobile application needs. The 1.0 version features native APIs, REST APIs, JSON support, and sync capability. The beta release is available to all community-edition customers.

Features

  • Replication enhancements—The replicator can now sync specific sets of documents, and pull replications can now optionally poll. Replications run on a separate background thread so they won’t block the main thread or regular Couchbase Lite operations (except unavoidably while writing to the database).
  • Revision management enhancements—Database compaction now prunes the oldest revisions after a document’s revision history grows beyond a maximum length.
  • CBLModel enhancements—CBLModel now supports all remaining scalar property types and custom object classes as properties.
  • Performance enhancements—A number of performance enhancements were added, including tuning of the CBLBatcher class, incremental parsing of the _changes feed, and significant speeds to SQLite database access.
  • API name changes—API names changed to follow a language-neutral API spec in preparation to make development for other platforms simple and consistent.
  • Flexible concurrency—Made concurrency more flexible by allowing Couchbase Lite to run on a dispatch queue and adding a way to call it from any thread.
  • Support for 64-bit iOS apps.

Fixes in Beta 2

  • Indexing and Querying for JSON
    • The CBLQueryRow class equality test now compares sequence numbers if emitted values are nil or reflect no change.
    • During database compaction, excessively-deep revision trees have their oldest revisions removed entirely. The depth limit can be configured.

    Issues: 115, 118

  • Replication
    • The property docs_id is now supported.

    Issues: 102

Known Issues

  • API names in Beta 2

    • As mentioned in the Beta 2 feature list, we’ve made API changes to support a language-neutral spec. In Beta 2 the old method and property names are still available, but marked as deprecated. Xcode issues warnings for these, and the warning messages tell you what name to use instead.
  • Indexing and querying for JSON

    • The querying parameter startkey_docid is not yet implemented.

    Issues: 111

  • Third-party Compatibility

    • We do not recommend using Couchbase Lite with the Dropbox Sync SDK at the same time in development.

    Issues: 199

    • Server header modification is required when attempting to use _bulk_get with a reverse proxy.

    Issues: 215

  • Sync Gateway support

    • To share a cookie with an application, you need to set the path option in the cookie header with Path=/.

    Issues: 212

1.0 Beta (September 2013)

This is the Beta release of Couchbase Lite iOS 1.0. Couchbase Lite is an ultra-lightweight, reliable, secure JSON database built for all your online and offline mobile application needs. The 1.0 version features Native APIs, REST APIs, JSON support, and sync capability. The beta release is available to all community-edition customers.

Features

  • Native API—The native API enables you to manage your mobile database by using APIs optimized specifically for iOS.

  • REST APIs—REST APIs provide an alternative access method based on your development needs.

  • JSON support—JSON support provides a flexible data model designed for mobile object-oriented apps. Adapt to your application needs with immediacy and little impact.

  • Easy sync with Couchbase Sync Gateway—Easy sync enables you to focus on application development, not syncing. You can be sync-ready with just a few lines of code.

Fixes

None.

Known Issues

  • Indexing and querying for JSON

    • Working on a JavaScript equivalent of the CouchDB MapReduce sum() function, which adds up the numeric values of all arguments.

    • If a document value is updated to nil, or any value that the client is interested in, CBLLiveQuery does not notify observers that the document has changed.

    Issues: 75, 115

  • Easy sync with Couchbase Sync Gateway

    • The revs list being sent from the client to the server can continually grow and cause a performance impact.

    Issues: 118