Build a React Native Module with Couchbase Lite on iOS

In this tutorial, you will learn how to use Couchbase Lite in a React Native project.

The sample project is an application that allows users to search and bookmark hotels from a Couchbase Lite database. The application contains 2 screens:

  • Bookmarks Screen: to list the bookmarked hotels. You can unbookmark a previously bookmarked hotel from this screen

  • Search Screen: to search for hotels by providing a country and/or full-text search query. You can bookmark (or unbookmark) a hotel from this screen.

ios flow

Pre-requisites

This tutorial requires the following components and versions to run successfully.

  • Xcode 10 or above

  • Swift 4.2

  • Couchbase Lite 2.1.1

The tutorial also assumes that the reader has a basic understanding of developing apps with React Native and Swift

Getting Started

The User Interface has already been implemented in the starter project. You will add the code to persist and query data.

  1. Download the starter project.

  2. Unzip starter-project.zip.

  3. Open the starter-project/HotelFinder/ directory in the JavaScript editor of your choice (for example, Visual Studio Code or WebStorm).

  4. The User Interface code is located in ui/Bookmarks.js and ui/Search.js.

  5. Run the following commands in your Terminal.

    cd HotelFinder
    npm install -g react-native-cli
    npm install
    react-native link

    The react-native link command bundles native dependencies in your Xcode project. It is required in the react-native-elements installation process.

  6. Start the React Native development server.

    npm run start

    The npm run start command starts a web server that bundles and serves the JavaScript code to the application. You should see the following in the output.

    Metro Bundler ready.
    
    Loading dependency graph...
  7. Open the Xcode project at HotelFinder/ios/HotelFinder.xcodeproj.

  8. Build and run.

  9. You can click on the Hotels button to run a search query. The result of the query will be empty.

In the next section, you will setup the Native Module interface which is the first step for establishing communication between native code and JavaScript.

Native Modules Setup

With Native Modules, you can write native code and have access to it from JavaScript. It is helpful when an app needs access to native APIs, and React Native doesn’t have a corresponding module yet. In this tutorial, you will use the Native Modules API to implement methods in Swift and call them from the JavaScript code. These methods will do the following:

  • Full Text Search for terms in a Couchbase Lite database.

  • Query documents in a Couchbase Lite database.

  • Create and Update documents in a Couchbase Lite database.

Swift/Objective-C Setup

  1. Select the File > New > File…​ > Objective-C File menu and create a new file called HotelFinder-RCTBridge.m. This file defines the methods that exported to the JS layer.

  2. Insert the following in HotelFinder-RCTBridge.m.

    #import <Foundation/Foundation.h>
    #import "React/RCTBridgeModule.h"
    
    @interface RCT_EXTERN_MODULE(HotelFinderNative, NSObject)
    
    /* code will be added here later. */
    
    + (BOOL)requiresMainQueueSetup
    {
      return YES;
    }
    @end
  3. Select the File > New > File…​ > Objective-C File menu and create a new file called HotelFinderNative.swift. This file contains the native implementation of the APIs that are exported to the JS layer.

  4. Insert the following in HotelFinderNative.swift.

    import Foundation
    
    @objc (HotelFinderNative)
    class HotelFinderNative: NSObject {
    
    	/* code will be added here later. */
    
    }

You are now ready to implement functionalities in Swift. The next step is to import the Couchbase Lite framework in your project.

Couchbase Lite Setup

  1. Download Couchbase Lite from here.

  2. Unzip the file and drag CouchbaseLiteSwift.framework to the Frameworks folder in the Xcode project navigator.

    drag framework
  3. Navigate to Project > General > Embedded Binary and drag CouchbaseLiteSwift.framework over the list.

    drag embedded
  4. Import the Swift framework in HotelFinderNative.swift.

    import CouchbaseLiteSwift

Database Setup

In our example, we will start with a pre-built Couchbase Lite database that contains a bunch of hotel documents. We will make our queries against the documents in this database. Note that in a real world application, the data could be synced down from other Couchbase Lite clients or from Sync Gateway in the cloud.

The pre-built database needs to be added to the Xcode project.

  1. Download travel-sample.cblite2.zip and drag it over the Xcode project navigator. Be sure to select the Copy items if needed checkbox.

  2. You will use the singleton pattern to setup the database instance. Create a new file named DatabaseManager.swift and insert the following.

    In this code, you first check if a database named "travel-sample" exists. If it doesn’t exist, the bundled database file is copied to the default Couchbase Lite directory. The database is then opened and the instance is set. The createIndex method creates the Full-Text Search index on the description property.

    import CouchbaseLiteSwift
    
    class DatabaseManager {
    
      private static var privateSharedInstance: DatabaseManager?
    
      var database: Database
    
      let DB_NAME = "travel-sample"
    
      class func sharedInstance() -> DatabaseManager   {
        guard let privateInstance = DatabaseManager.privateSharedInstance else {
          DatabaseManager.privateSharedInstance = DatabaseManager()
          return DatabaseManager.privateSharedInstance!
        }
        return privateInstance
      }
    
      private init() {
        let path = Bundle.main.path(forResource: self.DB_NAME, ofType: "cblite2")!
        if !Database.exists(withName: self.DB_NAME) {
          do {
            try Database.copy(fromPath: path, toDatabase: self.DB_NAME, withConfig: nil)
          } catch {
            fatalError("Could not copy database")
          }
        }
        do {
          self.database = try Database(name: "travel-sample")
          self.createIndex(database)
        } catch {
          fatalError("Could not copy database")
        }
      }
    
      func createIndex(_ database: Database) {
        do {
          try database.createIndex(IndexBuilder.fullTextIndex(items: FullTextIndexItem.property("description")).ignoreAccents(false), withName: "descFTSIndex")
        } catch {
          print(error)
        }
      }
    
    }
  3. Next, add the following properties in HotelFinderNative.swift.

    let database = DatabaseManager.sharedInstance().database
    let DOC_TYPE = "bookmarkedhotels"

    This code adds the database as an instance property on the HotelFinderNative class.

  4. Build & run. The project should build successfully.

In the next sections, you will use this instance variable to perform various operations.

Search Hotels

In this section, you will add the functionality to search for hotels.

  1. First, we import the appropriate ReactNative module. For this, add the following to the top of HotelFinder/ui/Search.js.

    import { NativeModules } from 'react-native';
    let HotelFinderNative = NativeModules.HotelFinderNative;

    The HotelFinderNative constant corresponds to the native module that was created in the Swift/Objective-C Setup section.

  2. Next, you must implement a method in the HotelFinderNative module before it can be accessed in JavaScript. Insert a new method signature in HotelFinder-RCTBridge.m.

    RCT_EXTERN_METHOD(search :(NSString *)description :(NSString *)location :(RCTResponseSenderBlock)errorCallback :(RCTResponseSenderBlock)successCallback)

    RCT_EXTERN_METHOD() is a React Native macro to specify that this method must be exported to JavaScript.

  3. Implement this method in HotelFinderNative.swift.

    This code creates the Full-text search query using the match() operator.

    In this particular example, the match expression looks for the descriptionText value in the description property. This match expression is logically ANDed with an equalTo comparison expression which looks for the locationText value in the country, city, state or address properties.This expression is then used in the where clause of the query in the usual way.

    @objc func search(_ description: String?, _ location: String = "", _ errorCallback: @escaping () -> Void, _ successCallback: @escaping ([[[AnyHashable : Any]]]) -> Void) {
    
      let locationExpression = Expression.property("country")
        .like(Expression.string("%\(location)%"))
        .or(Expression.property("city").like(Expression.string("%\(location)%")))
        .or(Expression.property("state").like(Expression.string("%\(location)%")))
        .or(Expression.property("address").like(Expression.string("%\(location)")))
    
      var searchExpression: ExpressionProtocol = locationExpression
      if let text = description {
        let descriptionFTSExpression = FullTextExpression.index("descFTSIndex").match(text)
        searchExpression = descriptionFTSExpression.and(locationExpression)
      }
    
      let query = QueryBuilder
        .select(
          SelectResult.expression(Meta.id),
          SelectResult.expression(Expression.property("name")),
          SelectResult.expression(Expression.property("address")),
          SelectResult.expression(Expression.property("phone"))
        )
        .from(DataSource.database(self.database))
        .where(
          Expression.property("type").equalTo(Expression.string("hotel"))
            .and(searchExpression)
      )
    
      do {
        let resultSet = try query.execute()
        var array: [[AnyHashable : Any]] = []
        for result in resultSet {
          let map = result.toDictionary()
          array.append(map)
        }
        successCallback([array])
      } catch {
        print(error)
        errorCallback();
      }
    }
  4. You can call the search swift method from Search.js. For this, add the following text to the onChangeText method in Search.js.

    HotelFinderNative.search(descriptionText, locationText, err => {
      console.log(err);
    }, hotels => {
      this.setState({hotels: hotels});
    });
  5. Build & run.

  6. Tap on "Hotels" button to get to the "Search" screen.

  7. In search screen. enter "UK" in the Country input field and press the Lookup button. You should now see a list of hotels in the search result.

    search hotel

Bookmark Hotel

  1. Bookmarked hotel IDs are persisted in a separate document of type bookmarkedhotels.

    The first time a hotel is bookmarked, the bookmarkedhotels document is created. Subsequently, every time a new hotel is bookmarked, the hotel ID is appended to the hotels array of the existing document. You will add a method to find or create the document of type bookmarkedhotels. Add the following findOrCreateBookmarkDocument method in HotelFinderNative.swift.

    func findOrCreateBookmarkDocument() -> MutableDocument {
      let query = QueryBuilder
        .select(
          SelectResult.expression(Meta.id),
          SelectResult.property("hotels")
        )
        .from(DataSource.database(database))
        .where(
          Expression.property("type")
            .equalTo(Expression.string(DOC_TYPE))
      )
    
      do {
        let resultSet = try query.execute()
        let array = resultSet.allResults()
        if (array.count == 0) {
          let mutableDocument = MutableDocument()
            .setString(DOC_TYPE, forKey: "type")
            .setArray(MutableArrayObject(), forKey: "hotels")
          try database.saveDocument(mutableDocument)
          return mutableDocument
        } else {
          let documentId = array[0].string(forKey: "id")!
          let document = database.document(withID: documentId)!
          return document.toMutable()
        }
      } catch {
        fatalError(error.localizedDescription);
      }
    }
  2. You will now add the method to update the document when a hotel is bookmarked. Insert a new method signature in HotelFinder-RCTBridge.m.

    RCT_EXTERN_METHOD(bookmark :(NSString *)hotelId :(RCTResponseSenderBlock)errorCallback :(RCTResponseSenderBlock)successCallback)
  3. Implement the corresponding method natively in HotelFinderNative.swift. Every time a new hotel is bookmarked, the hotel ID is appended to the hotels array and the update is saved to the database.

    @objc func bookmark(_ hotelId: String, _ errorCallback: @escaping ([Any]) -> Void, _ successCallback: @escaping ([Any]) -> Void) {
      let mutableDocument = findOrCreateBookmarkDocument()
      mutableDocument
        .array(forKey: "hotels")!
        .addString(hotelId)
      do {
        try database.saveDocument(mutableDocument)
        let array = mutableDocument.array(forKey: "hotels")!.toArray()
        successCallback([array])
      } catch {
        errorCallback([error.localizedDescription])
        fatalError(error.localizedDescription)
      }
    }
  4. You can now call it from Search.js. Add the following to the bookmark method in Search.js

    HotelFinderNative.bookmark(hotelId, err => {
      console.log(err);
    }, bookmarkIds => {
      this.setState({bookmarkIds: bookmarkIds});
    });
  5. While searching for hotels, the app should also display an icon on hotels that are previously bookmarked . To do so, you will add a new method to query hotel Ids. Insert a new method signature in HotelFinder-RCTBridge.m.

    RCT_EXTERN_METHOD(queryBookmarkIds :(RCTResponseSenderBlock)errorCallback :(RCTResponseSenderBlock)successCallback)
  6. Implement the corresponding method natively in HotelFinderNative.swift.

    @objc func queryBookmarkIds(_ errorCallback: @escaping ([Any]) -> Void, _ successCallback: @escaping ([Any]) -> Void) {
      let mutableDocument = findOrCreateBookmarkDocument()
      let array = mutableDocument.array(forKey: "hotels")!.toArray()
      successCallback([array])
    }
  7. You can now call queryBookmarkIds java method from Search.js. For that, add the following to the queryBookmarkIds method in Search.js

    HotelFinderNative.queryBookmarkIds(err => {
      console.log(err);
    }, hotels => {
      this.setState({bookmarkIds: hotels});
    });
  8. Build & run.

  9. Click Hotels and search for a hotel (type "UK" in the country field for example).

  10. You can now swipe a table view row to bookmark a hotel. The bookmark icon is displayed.

    swipe row

In the next section, you will query the bookmarked hotels to display them on the Bookmarks screen.

List Bookmarks

  1. Insert a new method signature in HotelFinder-RCTBridge.m.

    RCT_EXTERN_METHOD(queryBookmarkDocuments :(RCTResponseSenderBlock)errorCallback :(RCTResponseSenderBlock)successCallback)
  2. Implement the corresponding method natively in HotelFinderNative.swift.

    @objc func queryBookmarkDocuments(_ errorCallback: @escaping ([Any]) -> Void, _ successCallback: @escaping ([Any]) -> Void) {
      // Do a JOIN Query to fetch bookmark document and for every hotel Id listed
      // in the "hotels" property, fetch the corresponding hotel document
      let bookmarkDS = DataSource.database(database).as("bookmarkDS")
      let hotelsDS = DataSource.database(database).as("hotelsDS")
    
      let hotelsExpr = Expression.property("hotels").from("bookmarkDS")
      let hotelIdExpr = Meta.id.from("hotelsDS")
    
      let joinExpr = ArrayFunction.contains(hotelsExpr, value: hotelIdExpr)
      let join = Join.join(hotelsDS).on(joinExpr);
    
      let typeExpr = Expression.property("type").from("bookmarkDS")
    
      let bookmarkAllColumns = SelectResult.all().from("bookmarkDS")
      let hotelsAllColumns = SelectResult.all().from("hotelsDS")
    
      let query = QueryBuilder.select(bookmarkAllColumns, hotelsAllColumns)
        .from(bookmarkDS)
        .join(join)
        .where(typeExpr.equalTo(Expression.string(DOC_TYPE)));
    
      do {
        let resultSet = try query.execute()
        var array: [Any] = []
        for (_, item) in resultSet.enumerated() {
          let dictionary = item.dictionary(forKey: "hotelsDS")!
          array.append(dictionary.toDictionary())
        }
        successCallback([array])
      } catch {
        errorCallback([error.localizedDescription])
        fatalError(error.localizedDescription)
      }
    }

    To query bookmark documents, you will write a JOIN query between the document of type bookmarkedhotels which contains hotel Ids and documents of type hotels which contain all the other fields (name, address, phone etc.)

  3. On the JavaScript side, you must first import the HotelFinderNative ReactNative module. Add the following to the top of HotelFinder/ui/Bookmarks.js.

    import {NativeModules} from 'react-native';
    let HotelFinderNative = NativeModules.HotelFinderNative;

    You can now call the queryBookmarkDocuments native method from Bookmarks.js. Add the following text to the queryBookmarkDocuments method in Bookmarks.js.

    HotelFinderNative.queryBookmarkDocuments(err => {
      console.log(err);
    }, bookmarks => {
      this.setState({bookmarkDocuments: bookmarks});
    });
  4. Build and run.

  5. You should now see the hotel that was bookmarked in the Bookmark Hotel section listed in the bookmarks screen.

    home screen

By now, the pattern should seem very familiar and essentially consists of the following steps:

  • Declare the method to be exported in HotelFinder-RCTBridge.m

  • Implement the method natively in HotelFinderNative.swift. This layer will interact with the native iOS implementation of Couchbase Lite for data persistence functions.

  • Invoke the exported method from JavaScript (you will have to import the React Native module the very first time).

Conclusion

Well done! You have learned how to import Couchbase Lite in a React Native project, and how to add search and persistence functionalities to your application!

As an exercise, you can follow the same procedure to implement the functionality to:

  • Unbookmark a hotel on the Bookmarks screen.

  • Unbookmark a hotel on the Search screen.

You can find a working copy of the completed project in the final project zip file. Follow the instructions in the Couchbase Lite Setup section to integrate Couchbase Lite into your final project. To build and run the final project, follow the steps in the Getting Started section. The final project also implements the missing functionalities mentioned above.