Couchbase Mobile Peer-2-Peer Sync

Ravichandran

In this article, we will take you through implementing a simple application for contact sync between peer-to-peer Couchbase lite databases. The basic idea of this sample application is to demonstrate the use of Couchbase replication which transfers the local database changes to remote database, in this case it is peer to peer. For peer-to-peer replication, we will be using HTTP listener component from Couchbase Lite called Couchbase Lite Listener, that can receive the connection from other devices running Couchbase Lite and exchange the data between them.

To get started, you will require the below things installed.

  • Xcode 8.0 or above
  • CocoaPods

Follow below steps to create initial project setup.

  • Open Xcode and create a Single View Application project and and name it as ContactSync.
  • Open terminal and change directory to your project location and create a pod file using the command ‘pod init’
  • In your Pod file, add pod ‘couchbase-lite-ios’ and run pod install. This will install all the dependencies.
  • Open xcworkspace project file. In the Xcode Build Settings -> Linking-> Other Linker Flags row and then add $(inherited) so that it pulls in any linking settings included in the .xcconfig file from your CocoaPods.

xcode

For Swift projects, we need to import Couchbase library in the bridging header.

Follow below steps to add bridging header to your project in Xcode

  • Add a new header file to you project and name it as ContactSync-Bridging-Header.h.
  • Under the Build Settings of your Xcode project, update the Objective-C Bridging Header section with the relative path for the header file ContactSync-Bridging-Header.h

swift compiler

This sample application will be having following functionalities:

For sharing the contacts between peer-to-peer, one of the device will be on the sending edge while the other will be on the receiving side. So the sender has to select the contacts and the peers to share to. Receiver will receive the contacts and update the UI. It is the same application which will either be working in sending mode or receiving mode. For simplicity, we will not be considering any form of authentication and encryption, though it is easy to integrate using Couchbase built in facilities.

In summary, we need to

  • Display list of contacts for selection and sharing it with peers.
  • Display list of peers to share the contacts to.
  • On the receiver side, UI to display the contacts received.

For displaying the contact list, we will be using the CNContactPicker from the ContactsUI module. Sender can select contacts for sharing and selected contacts will appear in the list.

For identifying the nearby peers, we will be using MultipeerConnectivity module. Sender will display list of nearby peers and selected contacts will be synced to selected peers.

In the Xcode navigator, open the Build Phases tab and link with Contacts and ContactsUI framework to the project.

link library

Open the Main.storyboard file and layout the view controllers as below.

storyboard file and layout

This application will be working in sending mode and receiving mode based on user selection.

Lets begin with the sending part first…

Add new file to project and name it as SendingViewController.swift

Update the file with following variables

1
class SenderViewController: UIViewController {

//1
fileprivate var contactsSelected = [CNContact]()
fileprivate let contactsPicker = CNContactPickerViewController()

//2
fileprivate let serviceType = “contactsync”
fileprivate var browserViewController : MCBrowserViewController! = nil
fileprivate var peerId : MCPeerID! = nil
fileprivate var session : MCSession!

//3
fileprivate var database : CBLDatabase?
fileprivate var replicator : CBLReplication?
fileprivate var pushURL : URL?


}

  1. ContactPicker displays the UI for selecting the contacts from the local contact store. Selected contacts will be saved in contactsSelected array.
  2. These are the variable for configuring the peer discovery. we will discuss each of this in the coming section.
  3. Couchbase database and replication instances for pushing the local changes to remote. We also need a push URL which is the URL of the Couchbase remote database where we need to push the local changes. This will be received from the browser discovery info from the Receiver which we will discuss later.

Update SenderViewController.swift with the following functions.

1
override func viewDidLoad() {

configureDatabase()

configurePeerDiscovery()

configureUI()

}

 

//1

func configureDatabase() {

 

let manager = CBLManager.sharedInstance()

 

// Create a database. make sure the name starts with small character

// This will first create a new empty database if one doesn’t already

// exist with that name

 

self.database = try! manager.databaseNamed(“contactsdb”)

if self.database == nil {

print(“Cannot create database”)

}

}

 

//2

func configurePeerDiscovery() {

 

self.peerId = MCPeerID(displayName: UIDevice.current.name)

self.session = MCSession(peer: peerId, securityIdentity: nil, encryptionPreference: .none)

 

self.browserViewController = MCBrowserViewController(serviceType: serviceType,

session: session)

 

self.session.delegate = self

self.browserViewController.delegate = self

}

 

//3

func configureUI() {

 

// For selecting the contacts from the ContactPicker

let button = UIBarButtonItem(barButtonSystemItem: .add,

target: self,

action: #selector(SenderViewController.selectContacts(_:)))

 

self.navigationItem.rightBarButtonItem = button

self.navigationItem.title = “Contacts”

 

contactsPicker.delegate = self

}

 

  1. Creates a local database. We will be writing the selected contacts to this local database and the same will be synced to remote database.
  2. Configuring the multi-peer discovery. For simplicity, security configurations are not considered for this sample. Multi-peer browser searches for the service type which the receiver is advertising and creates a session for the peer.
  3. We also need to configure UI option for selecting the contacts from the local Contact Store.

When user taps on add button, display the contactPicker and implement the Contact picker delegate to receive update on selected contacts.

We also require UI for displaying user selected contacts which will be shared to remote database. Lets add a tableview to display the selected contacts. Add table view in storyboard and its outlet in SenderViewController.

1
SenderViewController : CNContactPickerDelegate {

 

 

    @IBOutlet weak var tableview: UITableView!

}

 

 

// 1

@IBAction func selectContacts(_ sender: Any) {

 

self.present(contactsPicker, animated: true, completion: nil)

}

 

// 2

extension SenderViewController : CNContactPickerDelegate {

 

func contactPicker(_ picker: CNContactPickerViewController, didSelect contacts: [CNContact]) {

 

contactsSelected = contacts

self.tableview.reloadData()

}

}

Now we have the contacts selected from the local contact store, we need to implement table view’s data source to display the selected contacts.

1
extension SenderViewController : UITableViewDataSource {

 

func numberOfSections(in tableView: UITableView) -> Int {

return 1

}

 

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

return contactsSelected.count

}

 

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

 

var cell =  tableView.dequeueReusableCell(withIdentifier: “contactcell”)

if (cell ==  nil) {

cell = UITableViewCell(style: .subtitle, reuseIdentifier: “contactcell”) as UITableViewCell

}

 

let contact = contactsSelected[indexPath.row]

cell?.textLabel?.text = contact.givenName

 

if contact.phoneNumbers.count > 0 {

cell?.detailTextLabel?.text = contact.phoneNumbers[0].value.stringValue

} else {

cell?.detailTextLabel?.text = “NA”

}

 

if (contact.imageDataAvailable){

cell?.imageView?.image = UIImage(data: contact.imageData!)

}

else {

cell?.imageView?.image = UIImage(named: “UserImage”)

}

return cell!

}

}

Now we have the contacts to sync. We have to connect to peer before sending the contacts. Add a button in the toolbar for displaying the peer browser and connect its action to selectPeers.

1
@IBAction func selectPeers(_ sender: Any) {>

self.present(self.browserViewController, animated: true, completion: nil)

}

When user taps on select peers button, we will display browser which will list all the peers which are advertising the same service type, in this case “contactsync”. We have to implement the delegate methods of MCBrowserViewController and validate the appropriate peers.

1
extension SenderViewController : MCBrowserViewControllerDelegate {

 

func browserViewController(_ browserViewController: MCBrowserViewController, shouldPresentNearbyPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) -> Bool {

 

if let url = info?[“url”] {

if url.hasSuffix(database!.name)

{

pushURL = URL(string: url)

return true

}

}

return false

}

}

Browser will display the receiver if it finds the discovery information of the receiver and it has the same database name as the sender database name. Receiver will be sharing the url of the database when advertising. Sender will use this url to push the local changes to remote database. Save the pushURL instance variable with remote database url to push the local changes.

Now we have selected contacts and peers to share the contacts with, next is to push the local changes to remote. When sender selects a peer, it will establish a session with the peer and status of the connection is updated through the session delegate. We need to track the connection status and push the contacts to remote if the connection is established.

1
extension SenderViewController : MCSessionDelegate {

 

func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {

if state == .connected {

self.browserViewController.dismiss(animated: true, completion: nil)

sendContactsToConnectedPeers()

}

}

}

 

func sendContactsToConnectedPeers() {

 

// validate if any contacts selected

if self.contactsSelected.count <= 0  {

print(“No contacts selected…”)

return

}

 

DispatchQueue.main.async {

 

var docIds: [String] = []

for contact in self.contactsSelected {

 

let doc = self.database!.createDocument()

let rev = doc.newRevision()

rev.setAttachmentNamed(“contact”,

withContentType: “application/octet-stream”,

content: NSKeyedArchiver.archivedData(withRootObject: contact))

do {

try rev.save()

docIds.append(doc.documentID)

} catch let error as NSError {

NSLog(“Cannot save document: %@”, error)

}

}

 

// Check if any documents are updated to local database

if docIds.count > 0 {

 

// push the local changes to remote url

if let url = self.pushURL {

 

self.replicator = self.database!.createPushReplication(url)

self.replicator?.documentIDs = docIds

self.replicator?.start()

}

}

}

}

}

Once the session is created and connection established, we will update  the local database with the selected contacts and push the local changes to remote. For simplicity, we are pushing all the selected contacts to remote database.

Let’s move on to receiver part now. Add a new swift file to the project and name it as ReceiverViewController.swift and update the following.

1
class ReceiverViewController: UIViewController {

 

// For storing the received contacts

fileprivate var contactsRecieved = [CNContact]()

 

// should match with the same service as that of sender

fileprivate let serviceType = “contactsync”

 

// For advertising the service

fileprivate var peerId : MCPeerID!

fileprivate var serviceAdvertiser : MCAdvertiserAssistant!

fileprivate var session : MCSession!

 

// Couchbase database

fileprivate var database : CBLDatabase?

 

// Listener for database change

fileprivate var listener : CBLListener?

fileprivate var listenerURL : URL?

 

// for displaying received contacts

@IBOutlet weak var tableview: UITableView!

 

}

We have to advertise the service as soon as the view controller is loaded.

1
override func viewDidLoad() {

super.viewDidLoad()

 

// 1

configureDatabase()

 

// 2

advertiseService()

 

// 3

registerDatabaseChangeNotification()

 

}

 

func configureDatabase() {

 

self.database = try! CBLManager.sharedInstance().databaseNamed(“contactsdb”)

if self.database == nil {

print(“Cannot create database”)

}

}

 

func advertiseService() {

 

self.peerId = MCPeerID(displayName: UIDevice.current.name)

self.session = MCSession(peer: peerId, securityIdentity: nil, encryptionPreference: .none)

 

// Port 0 automatically picks the available port

listener = CBLListener(manager: CBLManager.sharedInstance(), port: 0)

 

// No Auth

listener?.requiresAuth = false

 

var syncURL:URL? = nil

 

// start listening for changes

do {

try listener?.start()

syncURL = URL(string: database!.name, relativeTo: listener?.url)

listenerURL = syncURL

} catch {

listenerURL = nil

}

 

// Discovery info for Sender to push the database changes to

self.serviceAdvertiser = MCAdvertiserAssistant(serviceType: serviceType,

discoveryInfo: [“url”:(syncURL?.absoluteString)!],

session: session)

 

// start advertising

self.serviceAdvertiser.start()

}

 

func registerDatabaseChangeNotification() {

NotificationCenter.default.addObserver(forName: NSNotification.Name.cblDatabaseChange,

object: database, queue: nil)

{

(notification) -> Void in

if let changes = notification.userInfo![“changes”] as? [CBLDatabaseChange] {

 

DispatchQueue.main.async(execute: {

for change in changes {

if let doc = self.database!.existingDocument(withID: change.documentID) {

if let attachment = doc.currentRevision?.attachmentNamed(“contact”) {

var contact = NSKeyedUnarchiver.unarchiveObject(with: attachment.content!) as! CNContact

self.contactsRecieved.append(contact)

 

}

}

}

 

self.tableview.reloadData()

})

 

}

}

}

  1. Configure the database for receiving the remote data changes. Database will be updated through replication.
  2. Listener is a HTTP server that provides remote access to CouchbaseLite REST api. Start listener on the local database. Configure the service advertisement for the ‘contactsync’ service. Note that while advertising the service, we are passing the discovery info with the URL for the local database. This information is used by the sender to replicate the changes.
  3. When sender replicate the changes to receiver, we need a way of identifying the changes on receiver database. We need to register the notification for database changes and update the tableview for the new contacts. Note that sender is updating the database with contacts as an attachment. Attachments contain the archived contact. We have to unarchive the attachment content for retrieving the original contact object.

We have received the contacts updated by sender through push replication. These has to be updated in tableview.

1
extension ReceiverViewController : UITableViewDataSource {

 

func numberOfSections(in tableView: UITableView) -> Int {

return 1

}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

 

return contactsRecieved.count

}

 

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

 

var cell =  tableView.dequeueReusableCell(withIdentifier: “contactcell”)

if (cell ==  nil) {

cell = UITableViewCell(style: .default, reuseIdentifier: “contactcell”) as UITableViewCell

}

 

let contact = contactsRecieved[indexPath.row]

 

cell?.textLabel?.text = contact.givenName

cell?.detailTextLabel?.text = contact.familyName

 

if (contact.imageDataAvailable){

cell?.imageView?.image = UIImage(data: contact.imageData!)

}

else {

cell?.imageView?.image = UIImage(named: “UserImage”)

}

 

return cell!

}

}

We have made all the changes in Sender and Receiver source files. To test this application, you have to run two instances on different simulators or with simulator and a device both on the same network.

Conclusion:

Couchbase is available on major mobile platforms with native support which we can leverage to build complex applications. It takes very little code to write sync functionalities between databases and  easy to integrate access control and encryption.

Further Reading:

Couchbase developer website has a detail information from installation to implementation . Please take some time to go through the developer documents for more details.

http://developer.couchbase.com/documentation/mobile/1.3/installation/index.html

http://developer.couchbase.com/documentation/mobile/1.3/guides/couchbase-lite/native-api/replication/index.html

Multipeer connectivity is apple’s built in framework which supports discovery of services provided by nearby devices. More information is available at apple’s developer site.

https://developer.apple.com/reference/multipeerconnectivity

https://developer.apple.com/reference/contactsui

about the author

Ravichandran