Objective-C

Getting Started

Xcode project

Create or open an existing Xcode project and install Couchbase Lite using one of the following methods.

  • Frameworks

  • Carthage

  • CocoaPods

  • Download Couchbase Lite.

  • Drag CouchbaseLite.framework from your Finder to the Xcode navigator.

  • Click on Project > General > Embedded Binary and add CouchbaseLite.framework to this section.

  • Import the framework and start using it in your project.

    #include <CouchbaseLite/CouchbaseLite.h>
    ...
  1. Install Carthage

  2. In your Cartfile, add the following.

    Couchbase Lite Community Edition
    binary "https://packages.couchbase.com/releases/couchbase-lite-ios/carthage/CouchbaseLite-Community.json" ~> 2.1.1
    Couchbase Lite Enterprise Edition
    binary "https://packages.couchbase.com/releases/couchbase-lite-ios/carthage/CouchbaseLite-Enterprise.json" ~> 2.1.1
  3. Run carthage update --platform ios.

  4. Drag CouchbaseLite.framework from Carthage/Build/ to the Xcode navigator.

  5. Click on Project > General > Embedded Binary and add CouchbaseLite.framework to this section.

  1. Install Cocoapods

  2. In your Podfile, add the following.

    Couchbase Lite Community Edition
    target '<your target name>' do
      use_frameworks!
      pod 'CouchbaseLite', '~> 2.1.1'
    end
    Couchbase Lite Enterprise Edition
    target '<your target name>' do
      use_frameworks!
      pod 'CouchbaseLite-Enterprise', '~> 2.1.1'
    end
  3. Install the pods and open the .xcworkspace file generated by Cocoapods.

    pod install

Starter code

Open ViewController.m in Xcode and copy the following code in the viewDidLoad method. This snippet demonstrates how to run basic CRUD operations, a simple Query and running bi-directional replications with Sync Gateway.

// Get the database (and create it if it doesn’t exist).
NSError *error;
CBLDatabase *database = [[CBLDatabase alloc] initWithName:@"mydb" error:&error];

// Create a new document (i.e. a record) in the database.
CBLMutableDocument *mutableDoc = [[CBLMutableDocument alloc] init];
[mutableDoc setFloat:2.0 forKey:@"version"];
[mutableDoc setString:@"SDK" forKey:@"type"];

// Save it to the database.
[database saveDocument:mutableDoc error:&error];

// Update a document.
CBLMutableDocument *mutableDoc2 = [[database documentWithID:mutableDoc.id] toMutable];
[mutableDoc2 setString:@"Swift" forKey:@"language"];
[database saveDocument:mutableDoc2 error:&error];

CBLDocument *document = [database documentWithID:mutableDoc2.id];
// Log the document ID (generated by the database)
// and properties
NSLog(@"Document ID :: %@", document.id);
NSLog(@"Learning %@", [document stringForKey:@"language"]);

// Create a query to fetch documents of type SDK.
CBLQueryExpression *type = [[CBLQueryExpression property:@"type"] equalTo:[CBLQueryExpression string:@"SDK"]];
CBLQuery *query = [CBLQueryBuilder select:@[[CBLQuerySelectResult all]]
                                      from:[CBLQueryDataSource database:database]
                                     where:type];

// Run the query
CBLQueryResultSet *result = [query execute:&error];
NSLog(@"Number of rows :: %lu", (unsigned long)[[result allResults] count]);

// Create replicators to push and pull changes to and from the cloud.
NSURL *url = [[NSURL alloc] initWithString:@"ws://localhost:4984/example_sg_db"];
CBLURLEndpoint *targetEndpoint = [[CBLURLEndpoint alloc] initWithURL:url];
CBLReplicatorConfiguration *replConfig = [[CBLReplicatorConfiguration alloc] initWithDatabase:database target:targetEndpoint];
replConfig.replicatorType = kCBLReplicatorTypePushAndPull;

// Add authentication.
replConfig.authenticator = [[CBLBasicAuthenticator alloc] initWithUsername:@"john" password:@"pass"];

// Create replicator.
CBLReplicator *replicator = [[CBLReplicator alloc] initWithConfig:replConfig];

// Listen to replicator change events.
[replicator addChangeListener:^(CBLReplicatorChange *change) {
    if (change.status.error) {
        NSLog(@"Error code: %ld", change.status.error.code);
    }
}];

// Start replication
[replicator start];

Build and run. You should see the document ID and property printed to the console. The document was successfully persisted to the database.

getting started ios

Before synchronizing documents to Sync Gateway you will need to disable App Transport Security. In the Xcode navigator, right-click on Info.plist and open it as a source file.

info plist

Append the following inside of the <dict> XML tags to disable ATS.

<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key><true/>
</dict>

Couchbase Lite Framework Size

Although the size of the Couchbase Lite framework that is downloaded is around 50MB, note that the framework would add only around 3.5MB to the size of the app when it is installed from the App Store. This is because when the user installs your app from the App Store, only the bits that are relevant to the device architecture are delivered.

Why is the original size so large?

The Couchbase Lite framework includes a "fat" binary that contains slices for both device (armv7, arm64) and simulator (i386 and x86_64) CPU architectures. The fat binary allows you to link your app to the same framework and run your app on the simulator or a real device. In addition, the bitcode that is included with the framework contributes to the majority of the download size. Bitcode is an intermediate code representation that allows Apple to recompile the app after App submission and to deliver a thin version of the app specific to the device architecture.

Architecture Stripping

When submitting a build of your application to the App Store you must ensure that it doesn’t contain any simulator architecture otherwise the upload will fail with the error message "Unsupported Architecture. Your executable contains unsupported architecture '[x86_64, i386]'."

The steps to remove the simulator architecture (x86_64) from CouchbaseLite.framework are outlined below. They depend on the method you chose to install Couchbase Lite:

  • Frameworks

  • Carthage

The Couchbase Lite framework available on the downloads page contains a build for both the simulator (x86_64) and iOS devices (ARM). The following steps describe how to set up a build phase in Xcode to do this automatically.

  1. In Xcode, open the Build Phases tab, then select the + > Add Run Script Build Phase option.

    run script phase

  2. Copy the contents of strip_framework.sh in the Run Script editor window.

    run script copy

That’s it, now every time you build and run your application, Xcode will remove binary architectures that do not match the target’s architecture type (emulator or device).

The following link describes how to set up a build phase in Xcode and run a Carthage script in order to remove the simulator architecture (x86_64).

Should you disable bitcode?

Although you can disable bitcode within your app and strip away bitcode from the Couchbase Lite framework, it is not necessary to do so. In fact, it is probably best to leave it enabled to be future proof. This is because the bitcode is never downloaded by the user even though it is uploaded during App submission.

Other resource to reduce the size of your app

More information is available on this Apple Q&A page.

Supported Versions

Platform Minimum OS version

iOS

9.0

macOS

10.9

Upgrading

Xcode

The API has changed in Couchbase Lite 2.0 and will require porting an application that is using Couchbase Lite 1.x API to the Couchbase Lite 2.0 API. To update an Xcode project built with Couchbase Lite 1.x:

  • Remove the existing CouchbaseLite.framework dependency from the Xcode project.

  • Remove all the Couchbase Lite 1.x dependencies (see the 1.4@objectivec.adoc#getting-started).

  • Install the Couchbase Lite 2.0 framework in your project (see the Getting Started section). At this point, there will be many compiler warnings. Refer to the examples on this page to learn about the new API.

  • Build & run your application.

Database Upgrade

Databases that were created with Couchbase Lite 1.2 or later can be used with Couchbase Lite 2.0. Upon detecting it is a 1.x database file, Couchbase Lite will automatically upgrade it to 2.0. This feature is only available for the default storage type (i.e., not a ForestDB database). Additionally, the automatic migration feature does not support encrypted databases, so if the 1.x database is encrypted you will first need to disable encryption using the Couchbase Lite 1.x API (see the 1.4@objectivec.adoc#database-encryption).

Handling of Existing Conflicts

For conflicts in the 1.x database, the automatic upgrade process copies the default winning revision to the new 2.0 database and does NOT copy any conflicting revisions. This functionality is related to the way conflicts are being handled in Couchbase Lite 2.0 (see Handling Conflicts). Optionally, existing conflicts in the 1.x database can be resolved with the 1.4@objectivec.adoc#resolving-conflicts prior to the database being upgraded to 2.0.

Handling of Existing Attachments

Attachments that were persisted in the 1.x database will be copied to the 2.0 database. In Couchbase Lite 2.0, the Attachment API has been renamed to Blob API. The functionally is identical but the internal schema for attachments has changed. In 1.x they were stored under the _attachments field and in Couchbase Lite 2.0 they are stored anywhere in the document like other value types. The automatic upgrade functionality will not update the internal schema for attachments, so they will still be accessible under the _attachments field. The following example shows how to retrieve an attachment that was created in a 1.x database with the 2.0 API.

CBLDictionary *attachments = [document dictionaryForKey:@"_attachments"];
CBLBlob *avatar = [attachments blobForKey:@"avatar"];
NSData *content = [avatar content];

Replication Compatibility

The replication protocol used in Couchbase Lite 2.0 has been re-designed from the ground up and it is not backwards compatible with the 1.x replication protocol. Therefore, to use replication with Couchbase Lite 2.x, the target Sync Gateway instance must also be upgraded to 2.x.

Sync Gateway 2.x will continue to accept clients that connect through the 1.x protocol. It will automatically use the 1.x replication protocol when a Couchbase Lite 1.x client connects through http://localhost:4984/db and the 2.0 replication protocol when a Couchbase Lite 2.0 client connects through ws://localhost:4984/db. This allows for a smoother transition to get all your user base onto a version of your application built with Couchbase Lite 2.x.

Database

New Database

As the top-level entity in the API, new databases can be created using the Database class by passing in a name, configuration, or both. The following example creates a database using the Database(name: String) method.

NSError *error;
CBLDatabase *database = [[CBLDatabase alloc] initWithName:@"my-database" error:&error];
if (!database) {
    NSLog(@"Cannot open the database: %@", error);
}

Just as before, the database will be created in a default location. Alternatively, the Database(string name, DatabaseConfiguration config) initializer can be used to provide specific options in the DatabaseConfiguration object such as the database directory.

Database Encryption

Enterprise Edition only
Database encryption is an Enterprise Edition feature.

The Couchbase Lite 2.1 release includes the ability to encrypt Couchbase Lite databases. This allows mobile applications to secure the data at rest, when it is being stored on the device. The algorithm used to encrypt the database is 256-bit AES.

To enable encryption, you must set the DatabaseConfiguration.encryptionKey property with the encryption key of your choice. The encryption key is then required every time the database is opened.

CBLDatabaseConfiguration *config = [[CBLDatabaseConfiguration alloc] init];
config.encryptionKey = [[CBLEncryptionKey alloc] initWithPassword:@"secretpassword"];

NSError *error;
CBLDatabase *database = [[CBLDatabase alloc] initWithName:@"my-database" config:config error:&error];
if (!database) {
    NSLog(@"Cannot open the database: %@", error);
}

Couchbase Lite does not persist the key. It is the application’s responsibility to manage the key and store it in a platform specific secure store such as Apple’s Keychain or Android’s Keystore.

An encrypted database can only be opened with the same language SDK that was used to encrypt it in the first place (Swift, C#, Java or Objective-C). For example, if a database is encrypted with the Swift SDK and then exported, it will only be readable with the Swift SDK.

Upgrading from 1.x when Encryption is Enabled

If you’re migrating an application from Couchbase Lite 1.x to 2.x, note that the automatic database upgrade functionality is not supported for encrypted databases. Thus, to upgrade an encrypted 1.x database, you should do the following:

  1. Disable encryption using the Couchbase Lite 1.x framework (see 1.4@objectivec.adoc#database-encryption)

  2. Open the database file with encryption enabled using the Couchbase Lite 2.x framework (see database encryption).

Since it is not possible to package Couchbase Lite 1.x and Couchbase Lite 2.x in the same application this upgrade path would require two successive upgrades. If you are using Sync Gateway to synchronize the database content, it may be preferable to run a pull replication from a new 2.x database with encryption enabled and delete the 1.x local database.

Finding a Database File

When the application is running on the iOS simulator, you can locate the application’s sandbox directory using the SimPholders utility.

CLI tool

cblite is a command-line tool for inspecting and querying Couchbase Lite 2.x databases. To use it, download Couchbase Lite from the downloads page and navigate to the Tools folder.

cblite tool

Instructions for various commands are available in Tools/README.md.

Logging

The log messages are split into different domains (LogDomains) which can be tuned to different log levels. The following example enables verbose logging for the replicator and query domains.

[CBLDatabase setLogLevel: kCBLLogLevelVerbose domain: kCBLLogDomainReplicator];
[CBLDatabase setLogLevel: kCBLLogLevelVerbose domain: kCBLLogDomainQuery];

Loading a pre-built database

If your app needs to sync a lot of data initially, but that data is fairly static and won’t change much, it can be a lot more efficient to bundle a database in your application and install it on the first launch. Even if some of the content changes on the server after you create the app, the app’s first pull replication will bring the database up to date.

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. After your app launches, it needs to check whether the database exists. If the database does not exist, the app should copy it from the app bundle using the [CBLDatabase copyFromPath:toDatabase:withConfig:error:] method as shown below.

// Note: Getting the path to a database is platform-specific.
// For iOS you need to get the path from the main bundle.
if (![CBLDatabase databaseExists:@"travel-sample" inDirectory:nil]) {
    NSError*error;
    NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:@"travel-sample" ofType:@"cblite2"];
    if (![CBLDatabase copyFromPath:path toDatabase:@"travel-sample" withConfig:nil error:&error]) {
        [NSException raise:NSInternalInconsistencyException
                    format:@"Could not load pre-built database: %@", error];
    }
}

Document

In Couchbase Lite, a document’s body takes the form of a JSON object — a collection of key/value pairs where the values can be different types of data such as numbers, strings, arrays or even nested objects. Every document is identified by a document ID, which can be automatically generated (as a UUID) or specified programmatically; the only constraints are that it must be unique within the database, and it can’t be changed.

Initializers

The following methods/initializers can be used:

  • The MutableDocument() initializer can be used to create a new document where the document ID is randomly generated by the database.

  • The MutableDocument(withID: String) initializer can be used to create a new document with a specific ID.

  • The database.document(withID: String) method can be used to get a document. If it doesn’t exist in the database, it will return nil. This method can be used to check if a document with a given ID already exists in the database.

The following code example creates a document and persists it to the database.

CBLMutableDocument *newTask = [[CBLMutableDocument alloc] init];
[newTask setString:@"task" forKey:@"task"];
[newTask setString:@"todo" forKey:@"owner"];
[newTask setString:@"task" forKey:@"createdAt"];
[database saveDocument:newTask error:&error];

Mutability

By default, when a document is read from the database it is immutable. The document.toMutable() method should be used to create an instance of the document which can be updated.

CBLDocument *document = [database documentWithID:@"xyz"];
CBLMutableDocument *mutableDocument = [document toMutable];
[mutableDocument setString:@"apples" forKey:@"name"];
[database saveDocument:mutableDocument error:&error];

Changes to the document are persisted to the database when the saveDocument method is called.

Typed Accessors

The Document class now offers a set of property accessors for various scalar types, including boolean, integers, floating-point and strings. These accessors take care of converting to/from JSON encoding, and make sure you get the type you’re expecting.

In addition, as a convenience we offer Date accessors. Dates are a common data type, but JSON doesn’t natively support them, so the convention is to store them as strings in ISO-8601 format. The following example sets the date on the createdAt property and reads it back using the document.date(forKey: String) accessor method.

[newTask setValue:[NSDate date] forKey:@"createdAt"];
NSDate *date = [newTask dateForKey:@"createdAt"];

If the property doesn’t exist in the document it will return the default value for that getter method (0 for getInt, 0.0 for getFloat etc.).

If you need to determine whether a given property exists in the document, you should use the [Document contains:key] method.

Batch operations

If you’re making multiple changes to a database at once, it’s faster to group them together. The following example persists a few documents in batch.

[database inBatch:&error usingBlock:^{
    for (int i = 0; i < 10; i++) {
        CBLMutableDocument *doc = [[CBLMutableDocument alloc] init];
        [doc setValue:@"user" forKey:@"type"];
        [doc setValue:[NSString stringWithFormat:@"user %d", i] forKey:@"name"];
        [doc setBoolean:NO forKey:@"admin"];
        [database saveDocument:doc error:nil];
    }
}];

At the local level this operation is still transactional: no other Database instances, including ones managed by the replicator can make changes during the execution of the block, and other instances will not see partial changes. But Couchbase Mobile is a distributed system, and due to the way replication works, there’s no guarantee that Sync Gateway or other devices will receive your changes all at once.

Blobs

We’ve renamed "attachments" to "blobs", for clarity. The new behavior should be clearer too: a Blob is now a normal object that can appear in a document as a property value. In other words, you just instantiate a Blob and set it as the value of a property, and then later you can get the property value, which will be a Blob object. The following code example adds a blob to the document under the avatar property.

UIImage *appleImage = [UIImage imageNamed:@"avatar.jpg"];
NSData *imageData = UIImageJPEGRepresentation(appleImage, 1.0);

CBLBlob *blob = [[CBLBlob alloc] initWithContentType:@"image/jpeg" data:imageData];
[newTask setBlob:blob forKey:@"avatar"];
[database saveDocument:newTask error:&error];

CBLDocument *savedTask = [database documentWithID: @"task1"];
CBLBlob *taskBlob = [savedTask blobForKey:@"avatar"];
UIImage *taskImage = [UIImage imageWithData:taskBlob.content];

The Blob API lets you access the contents as in-memory data (a Data object) or as a InputStream. It also supports an optional type property that by convention stores the MIME type of the contents.

In the example above, "image/jpeg" is the MIME type and "avatar" is the key which references that Blob. That key can be used to retrieve the Blob object at a later time.

On Couchbase Lite, blobs can be arbitrarily large, and are only read on demand, not when you load a Document object. On Sync Gateway, the maximum content size is 20 MB per blob. If a document’s blob is over 20 MB, the document will be replicated but not the blob.

When a document is synchronized, the Couchbase Lite replicator will add an _attachments dictionary to the document’s properties if it contains a blob. A random access name will be generated for each Blob which is different to the "avatar" key that was used in the example above. On the image below, the document now contains the _attachments dictionary when viewed in the Couchbase Server Admin Console.

attach replicated

A blob also has properties such as "digest" (a SHA-1 digest of the data), "length" (the length in bytes), and optionally "content_type" (the MIME type). The data is not stored in the document, but in a separate content-addressable store, indexed by the digest.

This Blob can be retrieved on the Sync Gateway REST API at http://localhost:4984/justdoit/user.david/blob_1. Notice that the blob identifier in the URL path is "blob_1" (not "avatar").

Query

Database queries have changed significantly. Instead of the map/reduce views used in 1.x, they’re now based on expressions, of the form "return ____ from documents where ____, ordered by ____", with semantics based on Couchbase’s N1QL query language.

There are several parts to specifying a query:

SELECT

Specifies the projection, which is the part of the document that is to be returned.

FROM

Specifies the database to query the documents from.

JOIN

Specifies the matching criteria in which to join multiple documents.

WHERE

Specifies the query criteria that the result must satisfy.

GROUP BY

Specifies the query criteria to group rows by.

ORDER BY

Specifies the query criteria to sort the rows in the result.

SELECT statement

With the SELECT statement, you can query and manipulate JSON data. With projections, you retrieve just the fields that you need and not the entire document.

A SelectResult represents a single return value of the query statement. You can specify a comma separated list of SelectResult expressions in the select statement of your query. For instance the following select statement queries for the document _id as well as the type and name properties of all documents in the database. In the query result, we print the _id and name properties of each row using the property name getter method.

{
    "_id": "hotel123",
    "type": "hotel",
    "name": "Apple Droid"
}
CBLQuerySelectResult *name = [CBLQuerySelectResult property:@"name"];
CBLQuery *query = [CBLQueryBuilder select:@[name]
                                     from:[CBLQueryDataSource database:database]
                                    where:[[[CBLQueryExpression property:@"type"] equalTo:[CBLQueryExpression value:@"user"]] andExpression:
                                           [[CBLQueryExpression property:@"admin"] equalTo:[CBLQueryExpression boolean:NO]]]];

NSEnumerator* rs = [query execute:&error];
for (CBLQueryResult *result in rs) {
    NSLog(@"user name :: %@", [result stringAtIndex:0]);
}

The SelectResult.all() method can be used to query all the properties of a document. In this case, the document in the result is embedded in a dictionary where the key is the database name. The following snippet shows the same query using SelectResult.all() and the result in JSON.

CBLQuery *query = [CBLQueryBuilder select:@[[CBLQuerySelectResult all]]
                                     from:[CBLQueryDataSource database:database]];
[
    {
        "travel-sample": {
            "callsign": "MILE-AIR",
            "country": "United States",
            "iata": "Q5",
            "icao": "MLA",
            "id": 10,
            "name": "40-Mile Air",
            "type": "airline"
        }
    },
    {
        "travel-sample": {
            "callsign": "TXW",
            "country": "United States",
            "iata": "TQ",
            "icao": "TXW",
            "id": 10123,
            "name": "Texas Wings",
            "type": "airline"
        }
    }
]

WHERE statement

Similar to SQL, you can use the where clause to filter the documents to be returned as part of the query. The select statement takes in an Expression. You can chain any number of Expressions in order to implement sophisticated filtering capabilities.

Comparison

The comparison operators can be used in the WHERE statement to specify on which property to match documents. In the example below, we use the equalTo operator to query documents where the type property equals "hotel".

{
    "_id": "hotel123",
    "type": "hotel",
    "name": "Apple Droid"
}
CBLQuery *query = [CBLQueryBuilder select:@[[CBLQuerySelectResult all]]
                                     from:[CBLQueryDataSource database:database]
                                    where:[[CBLQueryExpression property:@"type"] equalTo:[CBLQueryExpression string:@"hotel"]]
                                  groupBy:nil having:nil orderBy:nil
                                    limit:[CBLQueryLimit limit:[CBLQueryExpression integer:10]]];

NSEnumerator* rs = [query execute:&error];
for (CBLQueryResult *result in rs) {
    CBLDictionary *dict = [result valueForKey:@"travel-sample"];
    NSLog(@"document name :: %@", [dict stringForKey:@"name"]);
}

Collection Operators

Collection operators are useful to check if a given value is present in an array.

CONTAINS Operator

The following example uses the Function.arrayContains to find documents whose public_likes array property contain a value equal to "Armani Langworth".

{
    "_id": "hotel123",
    "name": "Apple Droid",
    "public_likes": ["Armani Langworth", "Elfrieda Gutkowski", "Maureen Ruecker"]
}
CBLQuerySelectResult *id = [CBLQuerySelectResult expression:[CBLQueryMeta id]];
CBLQuerySelectResult *name = [CBLQuerySelectResult property:@"name"];
CBLQuerySelectResult *likes = [CBLQuerySelectResult property:@"public_likes"];

CBLQueryExpression *type = [[CBLQueryExpression property:@"type"] equalTo:[CBLQueryExpression string:@"hotel"]];
CBLQueryExpression *contains = [CBLQueryArrayFunction contains:[CBLQueryExpression property:@"public_likes"]
                                                         value:[CBLQueryExpression string:@"Armani Langworth"]];

CBLQuery *query = [CBLQueryBuilder select:@[id, name, likes]
                                     from:[CBLQueryDataSource database:database]
                                    where:[type andExpression: contains]];

NSEnumerator* rs = [query execute:&error];
for (CBLQueryResult *result in rs) {
    NSLog(@"public_likes :: %@", [[result arrayForKey:@"public_likes"] toArray]);
}
IN Operator

The IN operator is useful when you need to explicitly list out the values to test against. The following example looks for documents whose first, last or username property value equals "Armani".

NSArray *values = @[[CBLQueryExpression property:@"first"],
                   [CBLQueryExpression property:@"last"],
                   [CBLQueryExpression property:@"username"]];

[CBLQueryBuilder select:@[[CBLQuerySelectResult all]]
                   from:[CBLQueryDataSource database:database]
                  where:[[CBLQueryExpression string:@"Armani"] in:values]];

Like Operator

The like operator can be used for string matching. It is recommended to use the like operator for case insensitive matches and the regex operator (see below) for case sensitive matches.

In the example below, we are looking for documents of type landmark where the name property exactly matches the string "Royal engineers museum". Note that since like does a case insensitive match, the following query will return "landmark" type documents with name matching "Royal Engineers Museum", "royal engineers museum", "ROYAL ENGINEERS MUSEUM" and so on.

CBLQuerySelectResult *id = [CBLQuerySelectResult expression:[CBLQueryMeta id]];
CBLQuerySelectResult *country = [CBLQuerySelectResult property:@"country"];
CBLQuerySelectResult *name = [CBLQuerySelectResult property:@"name"];

CBLQueryExpression *type = [[CBLQueryExpression property:@"type"] equalTo:[CBLQueryExpression string:@"landmark"]];
CBLQueryExpression *like = [[CBLQueryExpression property:@"name"] like:[CBLQueryExpression string:@"Royal engineers museum"]];

CBLQuery *query = [CBLQueryBuilder select:@[id, country, name]
                                     from:[CBLQueryDataSource database:database]
                                    where:[type andExpression: like]];

NSEnumerator* rs = [query execute:&error];
for (CBLQueryResult *result in rs) {
    NSLog(@"name property :: %@", [result stringForKey:@"name"]);
}

Wildcard Match

We can use % sign within a like expression to do a wildcard match against zero or more characters. Using wildcards allows you to have some fuzziness in your search string.

In the example below, we are looking for documents of type "landmark" where the name property matches any string that begins with "eng" followed by zero or more characters, the letter "e", followed by zero or more characters. The following query will return "landmark" type documents with name matching "Engineers", "engine", "english egg" , "England Eagle" and so on. Notice that the matches may span word boundaries.

CBLQuerySelectResult *id = [CBLQuerySelectResult expression:[CBLQueryMeta id]];
CBLQuerySelectResult *country = [CBLQuerySelectResult property:@"country"];
CBLQuerySelectResult *name = [CBLQuerySelectResult property:@"name"];

CBLQueryExpression *type = [[CBLQueryExpression property:@"type"] equalTo:[CBLQueryExpression string:@"landmark"]];
CBLQueryExpression *like = [[CBLQueryExpression property:@"name"] like:[CBLQueryExpression string:@"eng%e%"]];

CBLQueryLimit *limit = [CBLQueryLimit limit:[CBLQueryExpression integer:10]];

CBLQuery *query = [CBLQueryBuilder select:@[id, country, name]
                                     from:[CBLQueryDataSource database:database]
                                    where:[type andExpression: like]
                                  groupBy:nil having:nil orderBy:nil
                                    limit:limit];

Wildcard Character Match

We can use an _ sign within a like expression to do a wildcard match against a single character.

In the example below, we are looking for documents of type "landmark" where the name property matches any string that begins with "eng" followed by exactly 4 wildcard characters and ending in the letter "r". The following query will return "landmark" type documents with the name matching "Engineer", "engineer" and so on.

CBLQuerySelectResult *id = [CBLQuerySelectResult expression:[CBLQueryMeta id]];
CBLQuerySelectResult *country = [CBLQuerySelectResult property:@"country"];
CBLQuerySelectResult *name = [CBLQuerySelectResult property:@"name"];

CBLQueryExpression *type = [[CBLQueryExpression property:@"type"] equalTo:[CBLQueryExpression string:@"landmark"]];
CBLQueryExpression *like = [[CBLQueryExpression property:@"name"] like:[CBLQueryExpression string:@"eng____r"]];

CBLQueryLimit *limit = [CBLQueryLimit limit:[CBLQueryExpression integer:10]];

CBLQuery *query = [CBLQueryBuilder select:@[id, country, name]
                                     from:[CBLQueryDataSource database:database]
                                    where:[type andExpression: like]
                                  groupBy:nil having:nil orderBy:nil
                                    limit:limit];

Regex Operator

The regex expression can be used for case sensitive matches. Similar to wildcard like expressions, regex expressions based pattern matching allow you to have some fuzziness in your search string.

In the example below, we are looking for documents of type "landmark" where the name property matches any string (on word boundaries) that begins with "eng" followed by exactly 4 wildcard characters and ending in the letter "r". The following query will return "landmark" type documents with name matching "Engine", "engine" and so on. Note that the \b specifies that the match must occur on word boundaries.

CBLQuerySelectResult *id = [CBLQuerySelectResult expression:[CBLQueryMeta id]];
CBLQuerySelectResult *name = [CBLQuerySelectResult property:@"name"];

CBLQueryExpression *type = [[CBLQueryExpression property:@"type"] equalTo:[CBLQueryExpression string:@"landmark"]];
CBLQueryExpression *regex = [[CBLQueryExpression property:@"name"] regex:[CBLQueryExpression string:@"\\bEng.*e\\b"]];

CBLQueryLimit *limit = [CBLQueryLimit limit:[CBLQueryExpression integer:10]];

CBLQuery *query = [CBLQueryBuilder select:@[id, name]
                                     from:[CBLQueryDataSource database:database]
                                    where:[type andExpression: regex]
                                  groupBy:nil having:nil orderBy:nil
                                    limit:limit];

JOIN statement

The JOIN clause enables you to create new input objects by combining two or more source objects.

The following example uses a JOIN clause to find the airline details which have routes that start from RIX. This example JOINS the document of type "route" with documents of type "airline" using the document ID (_id) on the "airline" document and airlineid on the "route" document.

CBLQuerySelectResult *name = [CBLQuerySelectResult expression:[CBLQueryExpression property:@"name" from:@"airline"]];
CBLQuerySelectResult *callsign = [CBLQuerySelectResult expression:[CBLQueryExpression property:@"callsign" from:@"airline"]];
CBLQuerySelectResult *dest = [CBLQuerySelectResult expression:[CBLQueryExpression property:@"destinationairport" from:@"route"]];
CBLQuerySelectResult *stops = [CBLQuerySelectResult expression:[CBLQueryExpression property:@"stops" from:@"route"]];
CBLQuerySelectResult *airline = [CBLQuerySelectResult expression:[CBLQueryExpression property:@"airline" from:@"route"]];

CBLQueryJoin *join = [CBLQueryJoin join:[CBLQueryDataSource database:database as:@"route"]
                                     on:[[CBLQueryMeta idFrom:@"airline"] equalTo:[CBLQueryExpression property:@"airlineid" from:@"route"]]];

CBLQueryExpression *typeRoute = [[CBLQueryExpression property:@"type" from:@"route"] equalTo:[CBLQueryExpression string:@"route"]];
CBLQueryExpression *typeAirline = [[CBLQueryExpression property:@"type" from:@"airline"] equalTo:[CBLQueryExpression string:@"airline"]];
CBLQueryExpression *sourceRIX = [[CBLQueryExpression property:@"sourceairport" from:@"route"] equalTo:[CBLQueryExpression string:@"RIX"]];

CBLQuery *query = [CBLQueryBuilder select:@[name, callsign, dest, stops, airline]
                                     from:[CBLQueryDataSource database:database as:@"airline"]
                                     join:@[join]
                                    where:[[typeRoute andExpression:typeAirline] andExpression:sourceRIX]];

GROUP BY statement

You can perform further processing on the data in your result set before the final projection is generated. The following example looks for the number of airports at an altitude of 300 ft or higher and groups the results by country and timezone.

{
    "_id": "airport123",
    "type": "airport",
    "country": "United States",
    "geo": { "alt": 456 },
    "tz": "America/Anchorage"
}
CBLQuerySelectResult *count = [CBLQuerySelectResult expression:[CBLQueryFunction count:[CBLQueryExpression all]]];
CBLQuerySelectResult *country = [CBLQuerySelectResult property:@"country"];
CBLQuerySelectResult *tz = [CBLQuerySelectResult property:@"tz"];

CBLQueryExpression *type = [[CBLQueryExpression property:@"type"] equalTo:[CBLQueryExpression string:@"airport"]];
CBLQueryExpression *geoAlt = [[CBLQueryExpression property:@"geo.alt"] greaterThanOrEqualTo:[CBLQueryExpression integer:300]];

CBLQuery *query = [CBLQueryBuilder select:@[count, country, tz]
                                     from:[CBLQueryDataSource database:database]
                                    where:[type andExpression: geoAlt]
                                  groupBy:@[[CBLQueryExpression property:@"country"],
                                            [CBLQueryExpression property:@"tz"]]];
There are 138 airports on the Europe/Paris timezone located in France and above 300 ft
There are 29 airports on the Europe/London timezone located in United Kingdom and above 300 ft
There are 50 airports on the America/Anchorage timezone located in United States and above 300 ft
There are 279 airports on the America/Chicago timezone located in United States and above 300 ft
There are 123 airports on the America/Denver timezone located in United States and above 300 ft

ORDER BY statement

It is possible to sort the results of a query based on a given expression result. The example below returns documents of type equal to "hotel" sorted in ascending order by the value of the title property.

CBLQuerySelectResult *id = [CBLQuerySelectResult expression:[CBLQueryMeta id]];
CBLQuerySelectResult *title = [CBLQuerySelectResult property:@"title"];

CBLQuery *query = [CBLQueryBuilder select:@[id, title]
                                     from:[CBLQueryDataSource database:database]
                                    where:[[CBLQueryExpression property:@"type"] equalTo:[CBLQueryExpression string:@"hotel"]]
                                  orderBy:@[[[CBLQueryOrdering property:@"title"] descending]]];
Aberdyfi
Achiltibuie
Altrincham
Ambleside
Annan
Ardèche
Armagh
Avignon

Indexing

Before we begin querying documents, let’s briefly mention the importance of having a query index. A query can only be fast if there’s a pre-existing database index it can search to narrow down the set of documents to examine.

The following example creates a new index for the type and name properties.

{
    "_id": "hotel123",
    "type": "hotel",
    "name": "Apple Droid"
}
CBLValueIndexItem *type = [CBLValueIndexItem property:@"type"];
CBLValueIndexItem *name = [CBLValueIndexItem property:@"name"];
CBLIndex* index = [CBLIndexBuilder valueIndexWithItems:@[type, name]];
[database createIndex:index withName:@"TypeNameIndex" error:&error];

If there are multiple expressions, the first one will be the primary key, the second the secondary key, etc.

Every index has to be updated whenever a document is updated, so too many indexes can hurt performance. Thus, good performance depends on designing and creating the right indexes to go along with your queries.

Change Events

A live query stays active and monitors the database for changes. When there’s a change it re-runs itself automatically, and if the query results changed it notifies any observers. A live query is a great way to build reactive user interfaces, especially table/list views, that keep themselves up to date. For example, as the replicator runs and pulls new data from the server, a live query-driven UI will automatically update to show the data without the user having to manually refresh. This helps your app feel quick and responsive.

id<CBLListenerToken> token = [query addChangeListener:^(CBLQueryChange * _Nonnull change) {
    for (CBLQueryResultSet *result in [change results])
    {
        NSLog(@"%@", result);
        /* Update UI */
    }
}];

The live query starts immediately and changes are posted asynchronously.

The following example stops the live query with the token from the previous example.

[query removeChangeListenerWithToken:token];

To run a full-text search (FTS) query, you must have created a full-text index on the expression being matched. Unlike regular queries, the index is not optional. The following example inserts documents and creates an FTS index on the name property.

// Insert documents
NSArray *tasks = @[@"buy groceries", @"play chess", @"book travels", @"buy museum tickets"];
for (NSString *task in tasks) {
    CBLMutableDocument *doc = [[CBLMutableDocument alloc] init];
    [doc setString:@"task" forKey:@"type"];
    [doc setString:task forKey:@"name"];
    [database saveDocument:doc error:&error];
}

// Create index
CBLFullTextIndex *index = [CBLIndexBuilder fullTextIndexWithItems:@[[CBLFullTextIndexItem property:@"name"]]];
index.ignoreAccents = NO;
[database createIndex:index withName:@"nameFTSIndex" error:&error];

Multiple properties to index can be specified in the Index.fullTextIndex(withItems: [FullTextIndexItem]) method.

With the index created, an FTS query on the property that is being indexed can be constructed and ran. The full-text search criteria is defined as a FullTextExpression. The left-hand side is the full-text index to use and the right-hand side is the pattern to match.

CBLQueryExpression *where = [[CBLQueryFullTextExpression indexWithName:@"nameFTSIndex"] match:@"'buy'"];
CBLQuery *query = [CBLQueryBuilder select:@[[CBLQuerySelectResult expression:[CBLQueryMeta id]]]
                                     from:[CBLQueryDataSource database:database]
                                    where:where];

NSEnumerator* rs = [query execute:&error];
for (CBLQueryResult *result in rs) {
    NSLog(@"document id %@", [result stringAtIndex:0]);
}

In the example above, the pattern to match is a word, the full-text search query matches all documents that contain the word "buy" in the value of the doc.name property.

Full-text search is supported in the following languages: danish, dutch, english, finnish, french, german, hungarian, italian, norwegian, portuguese, romanian, russian, spanish, swedish and turkish.

The pattern to match can also be in the following forms:

prefix queries

The query expression used to search for a term prefix is the prefix itself with a "*" character appended to it. For example:

"'lin*'"
-- Query for all documents containing a term with the prefix "lin". This will match
-- all documents that contain "linux", but also those that contain terms "linear",
--"linker", "linguistic" and so on.
overriding the property name that is being indexed

Normally, a token or token prefix query is matched against the document property specified as the left-hand side of the match operator. This may be overridden by specifying a property name followed by a ":" character before a basic term query. There may be space between the ":" and the term to query for, but not between the property name and the ":" character. For example:

'title:linux problems'
-- Query the database for documents for which the term "linux" appears in
-- the document title, and the term "problems" appears in either the title
-- or body of the document.
phrase queries

A phrase query is a query that retrieves all documents that contain a nominated set of terms or term prefixes in a specified order with no intervening tokens. Phrase queries are specified by enclosing a space separated sequence of terms or term prefixes in double quotes ("). For example:

"'"linux applications"'"
-- Query for all documents that contain the phrase "linux applications".
NEAR queries

A NEAR query is a query that returns documents that contain a two or more nominated terms or phrases within a specified proximity of each other (by default with 10 or less intervening terms). A NEAR query is specified by putting the keyword "NEAR" between two phrase, token or token prefix queries. To specify a proximity other than the default, an operator of the form "NEAR/" may be used, where is the maximum number of intervening terms allowed. For example:

"'database NEAR/2 "replication"'"
-- Search for a document that contains the phrase "replication" and the term
-- "database" with not more than 2 terms separating the two.
AND, OR & NOT query operators

The enhanced query syntax supports the AND, OR and NOT binary set operators. Each of the two operands to an operator may be a basic FTS query, or the result of another AND, OR or NOT set operation. Operators must be entered using capital letters. Otherwise, they are interpreted as basic term queries instead of set operators. For example:

'couchbase AND database'
-- Return the set of documents that contain the term "couchbase", and the
-- term "database". This query will return the document with docid 3 only.

When using the enhanced query syntax, parenthesis may be used to specify the precedence of the various operators. For example:

'("couchbase database" OR "sqlite library") AND linux'
-- Query for the set of documents that contains the term "linux", and at least
-- one of the phrases "couchbase database" and "sqlite library".

Ordering results

It’s very common to sort full-text results in descending order of relevance. This can be a very difficult heuristic to define, but Couchbase Lite comes with a ranking function you can use. In the OrderBy array, use a string of the form Rank(X), where X is the property or expression being searched, to represent the ranking of the result.

Replication

Couchbase Mobile 2.0 uses a new replication protocol based on WebSockets.

Compatibility

The new protocol is incompatible with CouchDB-based databases. And since Couchbase Lite 2 only supports the new protocol, you will need to run a version of Sync Gateway that supports it.

To use this protocol with Couchbase Lite 2.0, the replication URL should specify WebSockets as the URL scheme (see the "Starting a Replication" section below). Mobile clients using Couchbase Lite 1.x can continue to use http as the URL scheme. Sync Gateway 2.0 will automatically use the 1.x replication protocol when a Couchbase Lite 1.x client connects through http://localhost:4984/db and the 2.0 replication protocol when a Couchbase Lite 2.0 client connects through ws://localhost:4984/db.

Starting Sync Gateway

Download Sync Gateway and start it from the command line with the configuration file created above.

~/Downloads/couchbase-sync-gateway/bin/sync_gateway

For platform specific installation instructions, refer to the Sync Gateway installation guide.

Starting a Replication

Replication objects are now bidirectional, this means you can start a push/pull replication with a single instance. The replication’s parameters can be specified through the ReplicatorConfiguration object; for example, if you wish to start a push only or pull only replication.

The following example creates a pull replication with Sync Gateway.

@interface MyClass : NSObject
@property (nonatomic) CBLDatabase *database;
@property (nonatomic) CBLReplicator *replicator; (1)
@end

@implementation MyClass
@synthesize database=_database;
@synthesize replicator=_replicator;

- (void) startReplication {
    NSURL *url = [NSURL URLWithString:@"ws://localhost:4984/db"]; (2)
    CBLURLEndpoint *target = [[CBLURLEndpoint alloc] initWithURL: url];
    CBLReplicatorConfiguration *config = [[CBLReplicatorConfiguration alloc] initWithDatabase:_database
                                                                                       target:target];
    config.replicatorType = kCBLReplicatorTypePull;
    _replicator = [[CBLReplicator alloc] initWithConfig:config];
    [_replicator start];
}
@end
1 A replication is an asynchronous operation. To keep a reference to the replicator object, you can set it as an instance property.
2 The URL scheme for remote database URLs has changed in Couchbase Lite 2.0. You should now use ws:, or wss: for SSL/TLS connections.

To verify that documents have been replicated, you can:

  • Monitor the Sync Gateway sequence number returned by the database endpoint (GET /{db}/). The sequence number increments for every change that happens on the Sync Gateway database.

  • Query a document by ID on the Sync Gateway REST API (GET /{db}/{id}).

  • Query a document from the Query Workbench on the Couchbase Server Console.

Couchbase Lite 2.0 uses WebSockets as the communication protocol to transmit data. Some load balancers are not configured for WebSocket connections by default (NGINX for example); so it might be necessary to explicitly enable them in the load balancer’s configuration (see Load Balancers).

By default, the WebSocket protocol uses compression to optimize for speed and bandwidth utilization. The level of compression is set on Sync Gateway and can be tuned in the configuration file (replicator_compression).

Replication Ordering

To optimize for speed, the replication protocol doesn’t guarantee that documents will be received in a particular order. So we don’t recommend to rely on that when using the replication or database change listeners for example.

Troubleshooting

As always, when there is a problem with replication, logging is your friend. The following example increases the log output for activity related to replication with Sync Gateway.

// Replicator
[CBLDatabase setLogLevel:kCBLLogLevelVerbose domain:kCBLLogDomainReplicator];
// Network
[CBLDatabase setLogLevel:kCBLLogLevelVerbose domain:kCBLLogDomainNetwork];

Replication Status

The replication.status.activity property can be used to check the status of a replication. For example, when the replication is actively transferring data and when it has stopped.

[replicator addChangeListener:^(CBLReplicatorChange *change) {
    if (change.status.activity == kCBLReplicatorStopped) {
        NSLog(@"Replication stopped");
    }
}];

The following table lists the different activity levels in the API and the meaning of each one.

State Meaning

STOPPED

The replication is finished or hit a fatal error.

OFFLINE

The replicator is offline as the remote host is unreachable.

CONNECTING

The replicator is connecting to the remote host.

IDLE

The replication caught up with all the changes available from the server. The IDLE state is only used in continuous replications.

BUSY

The replication is actively transferring data.

Handling Network Errors

If an error occurs, the replication status will be updated with an Error which follows the standard HTTP error codes. The following example monitors the replication for errors and logs the error code to the console.

[replicator addChangeListener:^(CBLReplicatorChange *change) {
    if (change.status.error) {
        NSLog(@"Error code: %ld", change.status.error.code);
    }
}];

When a permanent error occurs (i.e., 404: not found, 401: unauthorized), the replicator (continuous or one-shot) will stop permanently. If the error is temporary (i.e., waiting for the network to recover), a continuous replication will retry to connect indefinitely and if the replication is one-shot it will retry for a limited number of times. The following error codes are considered temporary by the Couchbase Lite replicator and thus will trigger a connection retry.

  • 408: Request Timeout

  • 429: Too Many Requests

  • 500: Internal Server Error

  • 502: Bad Gateway

  • 503: Service Unavailable

  • 504: Gateway Timeout

  • 1001: DNS resolution error

Custom Headers

Custom headers can be set on the configuration object. And the replicator will send those header(s) in every request. As an example, this feature can be useful to pass additional credentials when there is an authentication or authorization step being done by a proxy server (between Couchbase Lite and Sync Gateway).

CBLReplicatorConfiguration *config = [[CBLReplicatorConfiguration alloc] initWithDatabase:database target:endpoint];
config.headers = @{@"CustomHeaderName" : @"Value"};

Replication Checkpoint Reset

Checkpoints are used by the replicator to keep track of documents that are sent to the target database. Without checkpoints, Couchbase Lite would send the entire database content every time it connects to the target database, even though the target database may already have the majority of the database content from previous replications.

This functionality is generally not a concern to application developers. But if you require the replication to start again from zero then you may call the checkpoint reset method before starting the replicator.

[replicator resetCheckpoint];
[replicator start];

Handling Conflicts

In Couchbase Lite 2.0, document conflicts are automatically resolved. This functionality aims to simplify the default behavior of conflict handling and save disk space (conflicting revisions will no longer be stored in the database). There are 2 different save method signatures to specify how to handle a possible conflict:

  • save(document: MutableDocument): when concurrent writes to an individual record occur, the conflict is automatically resolved and only one non-conflicting document update is stored in the database. The Last-Write-Win (LWW) algorithm is used to pick the winning revision.

  • save(document: MutableDocument, concurrencyControl: ConcurrencyControl): attempts to save the document with a concurrency control. The concurrency control parameter has two possible values:

    • lastWriteWins: The last operation wins if there is a conflict.

    • failOnConflict: The operation will fail if there is a conflict.

Similarly to the save operation, the delete operation also has two method signatures to specify how to handle a possible conflict:

  • delete(document: Document): The last write will win if there is a conflict.

  • delete(document: Document, concurrencyControl: ConcurrencyControl): attemps to delete the document with a concurrency control. The concurrency control parameter has two possible values:

    • lastWriteWins: The last operation wins if there is a conflict.

    • failOnConflict: The operation will fail if there is a conflict.

Database Replicas

Database replicas is available in the Enterprise Edition only (https://www.couchbase.com/downloads). Starting in Couchbase Lite 2.0, replication between two local databases is now supported. It allows a Couchbase Lite replicator to store data on secondary storage. It would be especially useful in scenarios where a user’s device is damaged and the data needs to be moved to a different device. Note that the code below won’t compile if you’re running the Community Edition of Couchbase Lite.

CBLDatabaseEndpoint *targetDatabase = [[CBLDatabaseEndpoint alloc] initWithDatabase:database2];
CBLReplicatorConfiguration *config = [[CBLReplicatorConfiguration alloc] initWithDatabase:database target:targetDatabase];
config.replicatorType = kCBLReplicatorTypePush;

CBLReplicator *replicator = [[CBLReplicator alloc] initWithConfig:config];
[replicator start];

Certificate Pinning

Couchbase Lite supports certificate pinning. Certificate pinning is a technique that can be used by applications to "pin" a host to it’s certificate. The certificate is typically delivered to the client by an out-of-band channel and bundled with the client. In this case, Couchbase Lite uses this embedded certificate to verify the trustworthiness of the server and no longer needs to rely on a trusted third party for that (commonly referred to as the Certificate Authority).

The openssl command can be used to create a new self-signed certificate and convert the .pem file to a .cert file (see creating your own self-signed certificate). You should then have 3 files: cert.pem, cert.cer and key.pem.

The cert.pem and key.pem can be used in the Sync Gateway configuration (see installing the certificate).

On the Couchbase Lite side, the replication must be configured with the cert.cer file.

NSData *data = [self dataFromResource: @"cert" ofType: @"cer"];
SecCertificateRef cert = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)data);
CBLReplicatorConfiguration *config = [[CBLReplicatorConfiguration alloc] initWithDatabase:database
                                                                                   target:target];
config.pinnedServerCertificate = (SecCertificateRef)CFAutorelease(cert);

This example loads the certificate from the application sandbox, then converts it to the appropriate type to configure the replication object.

Peer-to-Peer Sync

Enterprise Edition only
Peer-to-peer sync is an Enterprise Edition feature.

Peer-to-peer sync allows devices running Couchbase Lite to directly sync data with each other. As part of this, Couchbase Lite is responsible for storing the data and keeping track of the data exchange, but isn’t responsible for the data transfer itself. Sending and receiving data must be handled by the platform APIs or a third party framework. In this section, we will refer to these third party frameworks as communication frameworks.

On iOS, Apple’s Multipeer Connectivity Framework would be a good choice for the Communication Framework. This framework will handle sending and receiving messages and supports multiple transport technologies — WiFi and Bluetooth and uses whichever is available to establish a connection between devices.

Thus, to enable peer-to-peer sync with Couchbase Lite, the application must use the Communication Framework with Couchbase Lite. The following sections describe a typical peer-to-peer workflow. Where applicable, we discuss how to integrate Couchbase Lite into the workflow.

In Couchbase Lite, a peer can take on one of these two roles:

Active Peer

The peer that initializes the connection and replication (i.e the "client" side).

Passive Peer

The passive side reacts to things that it receives but does not initiate any communication on its own (i.e. the "server" side).

Peer Discovery

Peer discovery is the first step. The communication framework will generally include a peer discovery API for devices to advertise themselves on the network and to browse for other peers.

discovery

Active Peer

As shown on the diagram above, the first step is to initialize the Couchbase Lite database.

Passive Peer

In addition to initializing the database, the passive peer must initialize the MessageEndpointListener. The MessageEndpointListener acts as as a listener for incoming connections.

CBLDatabase *database = [[CBLDatabase alloc] initWithName:@"mydb" error:nil];

CBLMessageEndpointListenerConfiguration *config =
[[CBLMessageEndpointListenerConfiguration alloc] initWithDatabase:database
                                                     protocolType:kCBLProtocolTypeMessageStream];
_messageEndpointListener = [[CBLMessageEndpointListener alloc] initWithConfig:config];

Peer Selection and Connection Setup

Once a peer device is found, it is the application code’s responsibility to decide whether it should establish a connection with that peer. This step includes inviting a peer to a session and peer authentication.

This is handled by the Communication Framework.

selection

Once the remote peer has been authenticated, the next step is to connect with that peer and initialize the Message Endpoint API.

Replication Setup

connection

Active Peer

When the connection is established, the active peer must instantiate a MessageEndpoint object corresponding to the remote peer.

CBLDatabase *database = [[CBLDatabase alloc] initWithName:@"dbname" error:nil];

// The delegate must implement the `CBLMessageEndpointDelegate` protocol.
NSString* id = @"";
CBLMessageEndpoint *endpoint =
[[CBLMessageEndpoint alloc] initWithUID:@"UID:123"
                                 target:id
                           protocolType:kCBLProtocolTypeMessageStream
                               delegate:self];

The MessageEndpoint initializer takes the following arguments.

  1. uid: a unique ID that represents the local active peer.

  2. target: This represents the remote passive peer and could be any suitable representation of the remote peer. It could be an Id, URL etc. If using the MultiPeerConnectivity Framework, this could be the MCPeerID.

  3. protocolType: specifies the kind of transport you intend to implement. There are two options.

    • The default (MessageStream) means that you want to "send a series of messages", or in other words the Communication Framework will control the formatting of messages so that there are clear boundaries between messages.

    • The alternative (ByteStream) means that you just want to send raw bytes over the stream and Couchbase should format for you to ensure that messages get delivered in full.

      Typically, the Communication Framework will handle message assembly and disassembly so you would use the MessageType option in most cases.

  4. delegate: the delegate that will implement the MessageEndpointConnection protocol.

Then, a Replicator is instantiated with the initialized MessageEndpoint as the target.

CBLReplicatorConfiguration *config =
[[CBLReplicatorConfiguration alloc] initWithDatabase:database target: endpoint];

// Create the replicator object.
CBLReplicator *replicator = [[CBLReplicator alloc] initWithConfig: config];
[replicator start];

Next, Couchbase Lite will call back the application code through the MessageEndpointDelegate.createConnection interface method. When the application receives the callback, it must create an instance of MessageEndpointConnection and return it.

- (id<CBLMessageEndpointConnection>)createConnectionForEndpoint:(CBLMessageEndpoint *)endpoint {
    return [[ActivePeerConnection alloc] init];
}

Next, Couchbase Lite will call back the application code through the MessageEndpointConnection.open method.

/* implementation of CBLMessageEndpointConnection */
- (void)open:(nonnull id<CBLReplicatorConnection>)connection completion:(nonnull void (^)(BOOL, CBLMessagingError * _Nullable))completion {
    _replicatorConnection = connection;
    completion(YES, nil);
}

The connection argument is then set on an instance variable. The application code must keep track of every ReplicatorConnection associated with every MessageEndpointConnection.

The MessageError argument in the completion block is used to specify if the error is recoverable or not. If it is a recoverable error, the replicator will kick off a retry process which will result to creating a new MessageEndpointConnection instance.

Passive Peer

The first step after connection establishment on the passive peer is to initialize a new MessageEndpointConnection and pass it to the listener. This tells the listener to accept incoming data from that peer.

PassivePeerConnection *connection = [[PassivePeerConnection alloc] init]; /* implements CBLMessageEndpointConnection */
[_messageEndpointListener accept: connection];

messageEndpointListener is the instance of the MessageEndpointListener that was created in the first step (Peer Discovery)

Couchbase Lite will then call back the application code through the MessageEndpointConnection.open method.

/* implementation of CBLMessageEndpointConnection */
- (void)open:(nonnull id<CBLReplicatorConnection>)connection completion:(nonnull void (^)(BOOL, CBLMessagingError * _Nullable))completion {
    _replicatorConnection = connection;
    completion(YES, nil);
}

The connection argument is then set on an instance variable. The application code must keep track of every ReplicatorConnection associated with every MessageEndpointConnection.

At this point, the connection is established and both peers are ready to exchange data.

Push/Pull Replication

Typically, an application needs to send data and receive data. Directionality of the replication could be any of the following.

  • Push only: The data is pushed from the local database to the remote database.

  • Pull only: The data is pulled from the remote database to the local database.

  • Push and Pull: The data is exchanged both ways.

Usually, the remote is a Sync Gateway database which is identified through a URL. In the context of peer-to-peer syncing, the remote is another Couchbase Lite database.

replication

The replication lifecycle is handled through the MessageEndpointConnection.

Active Peer

When Couchbase Lite calls back the application code through the MessageEndpointConnection.send method, you should send that data to the other peer using the communication framework.

/* implementation of CBLMessageEndpointConnection */
- (void)send:(nonnull CBLMessage *)message completion:(nonnull void (^)(BOOL, CBLMessagingError * _Nullable))completion {
    NSData *data = [message toData];
    NSLog(@"%@", data);
    /* send the data to the other peer */
    /* ... */
    /* call the completion handler once the message is sent */
    completion(YES, nil);
}

Once the data is sent, call the completion block to acknowledge the completion. You can use the MessageError in the completion block to specify if the error is recoverable or not. If it is a recoverable error, the replicator will kick off a retry process which will result to creating a new MessageEndpointConnection.

When data is received from the passive peer via the Communication Framework, you call the ReplicatorConnection.receive method.

CBLMessage *message = [CBLMessage fromData:data];
[_replicatorConnection receive:message];

The replication connection’s receive method is called which then processes the data in order to persist it to the local database.

Passive Peer

As in the case of the active peer, the passive peer must implement the MessageEndpointConnection.send method to send data to the other peer.

/* implementation of CBLMessageEndpointConnection */
- (void)send:(nonnull CBLMessage *)message completion:(nonnull void (^)(BOOL, CBLMessagingError * _Nullable))completion {
    NSData *data = [message toData];
    NSLog(@"%@", data);
    /* send the data to the other peer */
    /* ... */
    /* call the completion handler once the message is sent */
    completion(YES, nil);
}

Once the data is sent, call the completion block to acknowledge the completion. You can use the MessageError in the completion block to specify if the error is recoverable or not. If it is a recoverable error, the replicator will kick off a retry process which will result to creating a new MessageEndpointConnection.

When data is received from the active peer via the Communication Framework, you call the ReplicatorConnection.receive method.

CBLMessage *message = [CBLMessage fromData:data];
[_replicatorConnection receive:message];

Connection Teardown

When a peer disconnects from a peer-to-peer network, all connected peers are notified. The disconnect notification is a good opportunity to close and remove a replication connection. The steps to teardown the connection are slightly different depending on whether it is the active or passive peer that disconnects first. We will cover each case below.

Initiated by Active Peer

dis active
Active Peer

When an active peer disconnects, it must call the ReplicatorConnection.close method.

[_replicatorConnection close:nil];

Then, Couchbase Lite will call back your code through the MessageEndpointConnection.close to give the application a chance to disconnect with the Communication Framework.

/* implementation of CBLMessageEndpointConnection */
- (void)close:(nullable NSError *)error completion:(nonnull void (^)(void))completion {
    /* disconnect with communications framework */
    /* ... */
    /* call completion handler */
    completion();
}
Passive Peer

When the passive peer receives the corresponding disconnect notification from the Communication Framework, it must call the ReplicatorConnection.close method.

[_replicatorConnection close:nil];

Then, Couchbase Lite will call back your code through the MessageEndpointConnection.close to give the application a chance to disconnect with the Communication Framework.

/* implementation of CBLMessageEndpointConnection */
- (void)close:(nullable NSError *)error completion:(nonnull void (^)(void))completion {
    /* disconnect with communications framework */
    /* ... */
    /* call completion handler */
    completion();
}

Initiated by Passive Peer

dis passive
Passive Peer

When the passive disconnects, it must class the MessageEndpointListener.closeAll method.

[_messageEndpointListener closeAll];

Then, Couchbase Lite will call back your code through the MessageEndpointConnection.close to give the application a chance to disconnect with the Communication Framework.

/* implementation of CBLMessageEndpointConnection */
- (void)close:(nullable NSError *)error completion:(nonnull void (^)(void))completion {
    /* disconnect with communications framework */
    /* ... */
    /* call completion handler */
    completion();
}
Active Peer

When the active peer receives the corresponding disconnect notification from the Communication Framework, it must call the ReplicatorConnection.close method.

[_replicatorConnection close:nil];

Then, Couchbase Lite will call back your code through the MessageEndpointConnection.close to give the application a chance to disconnect with the Communication Framework.

/* implementation of CBLMessageEndpointConnection */
- (void)close:(nullable NSError *)error completion:(nonnull void (^)(void))completion {
    /* disconnect with communications framework */
    /* ... */
    /* call completion handler */
    completion();
}

Thread Safety

The Couchbase Lite API is thread safe except for calls to mutable objects: MutableDocument, MutableDictionary and MutableArray.

Release Notes

2.1

Performance Improvements
  • #2127 CBLReplicator always runs on main queue

Enhancements
  • #1610 2.0: Provide a script to strip simulator architecture frameworks for app submission

  • #2117 Implement Database Encryption (EE feature)

  • #2118 Support Peer-to-Peer replication through MessageEndpoint API (EE feature)

  • #2139 Implement Replicator reset checkpoint feature

Bugs
  • #1926 Swift Replicator.addChangeListener crashes

  • #2140 2.0 : TIC Read Status 1:57 error spam

  • #2149 Swift DictionaryObject and ArrayObject not having toMutable() method

  • #2162 2.1 DB1: Crash when inserting image Blobs to objects in embedded array in Swift

  • #2188 Replicator not getting stopped when using MessageEndpoint

Known Issues
  • #2112 HTTP proxy support

  • #2122 IOS 9 - Got LiteCore error: connection closed abnormally - eventual crash

2.0.2

2.0.0

Bugs
  • #1701 2:0 swift expression based query returning empty results

  • #1836 2.0: Database Listener callbacks not always getting called on main thread

  • #1854 2.0 : Dictionary, Array, and Fragment have duplicate definitions

  • #1862 2.0: CBLQueryExpression "and:" and "or:" methods cause syntax errors in Obj-C++

  • #2005 2.0: Make CBLDatabase query/replication tracking thread-safe

  • #2035 Cross Join Not Returning expected results

  • #2061 Swift 2.0: LiteCore error trying to copy prebuilt DB

  • #2085 Crashed in CBLWebSocket

  • #2094 TestSelfSignedSSLFailure_SG failed on iOS 10.2

  • #2104 Set Immutable Dictionary or Array to Document causes toDictionary() to crash in Swift

Known Issues
  • #2112 HTTP proxy support

  • #2122 IOS 9 - Got LiteCore error: connection closed abnormally - eventual crash

  • #2196 Crash with wifi disabled and bad network

  • #2197 WebSocket C4 dispatch crash during sync