Couchbase Lite With React Native on iOS
In this tutorial, you will learn how to use Couchbase Lite in a React Native project on iOS and look at a data model.
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.
Architecture
The user Interface is written in JavaScript while the business logic and data model is written in native Swift. The data model uses Couchbase Lite as the embedded data persistence layer. React Native module acts as the bridging layer between the Javascript layer and the native Swift layer.
This architecture allows you to write the User Interface code once for both iOS and Android apps while leveraging Couchbase Lite’s native iOS framework for data management.
Data Model
The data model for the app is very straightforward. There are two types of documents:
- The "bookmarkedhotels" document that includes the list of Ids corresponding to the hotels that have been bookmarked
- The "hotel" document that contains the details of the bookmarked hotel. The bookmarkedhotels document references the hotel document.
Note that although we have modeled our data using a "by Reference" /normalized model, since Couchbase Lite is JSON Document store, you have the flexibility to embed the details of the hotels within the bookmarkedhotels hotels.
Prerequisites
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.
- Download the starter project.
- Unzip starter-project.zip.
- Open the starter-project/HotelFinder/ directory in the JavaScript editor of your choice (for example, Visual Studio Code or WebStorm).
- The User Interface code is located in ui/Bookmarks.js and ui/Search.js.
- Run the following commands in your Terminal.
Thecd HotelFinder npm install -g react-native-cli npm install react-native link
react-native link
command bundles native dependencies in your Xcode project. It is required in the react-native-elements installation process. - Start the React Native development server.
Thenpm run start
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...
- Open the Xcode project at HotelFinder/ios/HotelFinder.xcodeproj.
- Build and run.
- 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
- 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.
- 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
- 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.
- 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
- Download Couchbase Lite from here.
- Unzip the file and drag CouchbaseLiteSwift.framework to the Frameworks folder in the Xcode project navigator.
- Navigate to Project > General > Embedded Binary and drag CouchbaseLiteSwift.framework over the list.
- 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.
- Download travel-sample.cblite2.zip and drag it over the Xcode project navigator. Be sure to select the Copy items if needed checkbox.
- You will use the singleton pattern to set up 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 thedescription
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) } } }
- Next, add the following properties in HotelFinderNative.swift.
This code adds the database as an instance property on thelet database = DatabaseManager.sharedInstance().database let DOC_TYPE = "bookmarkedhotels"
HotelFinderNative
class. - Build and 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.
- First, we import the appropriate ReactNative module. For this, add the following to the top of HotelFinder/ui/Search.js.
Theimport { NativeModules } from 'react-native'; let HotelFinderNative = NativeModules.HotelFinderNative;
HotelFinderNative
constant corresponds to the native module that was created in the Swift/Objective-C Setup section. - 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. - 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 thedescriptionText
value in thedescription
property. This match expression is logically ANDed with anequalTo
comparison expression which looks for thelocationText
value in thecountry
,city
,state
oraddress
properties. This expression is then used in thewhere
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(); } }
- You can call the
search
swift method from Search.js. For this, add the following text to theonChangeText
method in Search.js.HotelFinderNative.search(descriptionText, locationText, err => { console.log(err); }, hotels => { this.setState({hotels: hotels}); });
- Build and run.
- Tap on the "Hotels" button to get to the "Search" screen.
- In the 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.
Bookmark Hotel
- Bookmarked hotel IDs are persisted in a separate document of type
bookmarkedhotels
. The first time a hotel is bookmarked, thebookmarkedhotels
document is created. Subsequently, every time a new hotel is bookmarked, the hotel ID is appended to thehotels
array of the existing document. You will add a method to find or create the document of typebookmarkedhotels
. Add the followingfindOrCreateBookmarkDocument
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); } }
- 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)
- 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) } }
- You can now call it from Search.js. Add the following to the
bookmark
method in Search.jsHotelFinderNative.bookmark(hotelId, err => { console.log(err); }, bookmarkIds => { this.setState({bookmarkIds: bookmarkIds}); });
- 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)
- 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]) }
- You can now call
queryBookmarkIds
java method from Search.js. For that, add the following to thequeryBookmarkIds
method in Search.jsHotelFinderNative.queryBookmarkIds(err => { console.log(err); }, hotels => { this.setState({bookmarkIds: hotels}); });
- Build and run.
- Click Hotels and search for a hotel (type "UK" in the country field for example).
- You can now swipe a table view row to bookmark a hotel. The bookmark icon is displayed.
In the next section, you will query the bookmarked hotels to display them on the Bookmarks screen.
List Bookmarks
- Insert a new method signature in HotelFinder-RCTBridge.m.
RCT_EXTERN_METHOD(queryBookmarkDocuments :(RCTResponseSenderBlock)errorCallback :(RCTResponseSenderBlock)successCallback)
- Implement the corresponding method natively in HotelFinderNative.swift.
To query bookmark documents, you will write a JOIN query between the document of type@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) } }
bookmarkedhotels
which contains hotel Ids and documents of typehotels
which contain all the other fields (name
,address
,phone
etc.) - On the JavaScript side, you must first import the
HotelFinderNative
ReactNative module. Add the following to the top of HotelFinder/ui/Bookmarks.js.
You can now call theimport {NativeModules} from 'react-native'; let HotelFinderNative = NativeModules.HotelFinderNative;
queryBookmarkDocuments
native method from Bookmarks.js. Add the following text to thequeryBookmarkDocuments
method in Bookmarks.js.HotelFinderNative.queryBookmarkDocuments(err => { console.log(err); }, bookmarks => { this.setState({bookmarkDocuments: bookmarks}); });
- Build and run.
- You should now see the hotel that was bookmarked in the Bookmark Hotel section listed in the bookmarks 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.
Comments