Note: While you can make this work with Xcode 10, you cannot use Swift 4.2. is installed as a pre-compiled framework built with Swift 4.1, thereby requiring the whole project to be built with Swift 4.1. Until is updated, we recommend you use Xcode 9 for this tutorial.

Introduction

Couchbase is a NoSQL data platform that extends from servers down to mobile devices. It’s a document-based data platform in use worldwide by a large variety of companies and organizations.

Couchbase Mobile is the portion of their platform built for mobile devices. It provides:

  • Couchbase Lite 2.0, an embedded database, which incorporates:

    • N1QL, (“nickel”, not “n-one-q-l”) a new Query API
    • Full-text search
    • Automatic conflict resolution
    • Database replication using the WebSocket protocol
  • Sync Gateway, a secure web gateway for connecting to Couchbase Server.

In this Couchbase tutorial, you’ll develop an app to run Ray’s new pizzeria. The app will allow customers to place orders and will also let Chef Ray monitor pizza production and update his fabulous menu.

Couchbase Tutorial - Pizzeria demo

By the end of this tutorial, you’ll know how to:

  • Use a prebuilt Couchbase Lite database
  • Model and query data
  • Sync data using Sync Gateway
  • Simulate a multi-user environment using multiple simulators

Getting

Use the Download Materials button at the top or bottom of this tutorial to download the starter project.

Note: Because this project uses Couchbase and Fakery CocoaPods, you need to open RaysPizzeria.xcworkspace, not RaysPizzeria.xcproject.

Build and run the project in Xcode to see the basic UI.

Couchbase Tutorial - Starter Build and Run

The UI has two tabs: Menu and Kitchen, but not much functionality.

Select a pizza from the Menu tab and you’ll see an empty customer screen. Tap + to generate a customer, which then gets added to the UITableView. When you select the customer cell, a UIAlertController pops up asking you to confirm the order.

Note: The customer data is supplied using Fakery, a framework intended for unit testing.

Go back to the menu and select another pizza. Notice that your previous order and customer are no longer there. Oh no!

Now look at the Kitchen tab. Ray and his kitchen staff have no idea what pizza to make or who placed the order. To solve this problem, Ray asks you to implement a database-driven app.

With a database-driven app, you can:

  • Simplify order entry
  • Track order processing
  • Easily modify the menu

So what are you waiting for? It’s time to get cookin’.

Using a Pre-loaded Database

Open MenuViewController.swift and take a look around. It’s a basic UIViewController with a UITableView that displays the menu. In viewWillAppear(_:), the menu is loaded with menu = DataManager.shared.getMenu().

Open DataManager.swift and locate getMenu(). This loops thru the Pizza.PizzaType enum, creates a Pizza object and then adds it to the menu. This is fine until you need to change the menu items and their prices. To solve this problem, you’ll load the menu from a database instead.

Near the top of the file, replace:

import Foundation

With:

import CouchbaseLiteSwift

Add the following to the properties section, just under static var shared = DataManager():


private let database: Database
private let databaseName = "pizza-db"

To load a pre-built menu database, add the following to init():


// 1
if let path = Bundle.main.path(forResource: databaseName, ofType: "cblite2"), 
   !Database.exists(withName: databaseName) {
  do {
    try Database.copy(fromPath: path, toDatabase: databaseName, withConfig: nil)
  } catch {
    fatalError("Could not load pre-built database")
  }
}

// 2
do {
  database = try Database(name: databaseName)
} catch {
  fatalError("Error opening database")
}

Here’s the code breakdown:

  1. Get the path to the pre-loaded database from the app bundle, a read-only location, and copy it to a read-write location. Database.copy(fromPath:toDatabase:withConfig:) copies it to the app’s Application Support directory.
  2. Open the pre-loaded database.

Now, replace the contents of getMenu() with:


var menu: [Pizza] = []

// 1
let query = QueryBuilder
   .select(SelectResult.all(), SelectResult.expression(Meta.id))
   .from(DataSource.database(database))
   .where(Expression.property("type")
     .equalTo(Expression.string(DocTypes.pizza.rawValue)))
   .orderBy(Ordering.property(PizzaKeys.price.rawValue))

// 2
do {
  for result in try query.execute() {
    guard let dict = result.dictionary(forKey: databaseName),
      let name = dict.string(forKey: PizzaKeys.name.rawValue),
      let id = result.string(forKey: PizzaKeys.id.rawValue) else {
        continue
      }

      let pizza = Pizza(id: id, name: name, 
                        price: dict.double(forKey: PizzaKeys.price.rawValue))
      menu.append(pizza)
    }
  } catch {
    fatalError("Error running the query")
  }

  return menu

Here’s what this does:

  1. Build a query that retrieves all pizza documents from the database and orders them by price. SelectResult.all() returns all property data from a pizza document. The document id (used in future queries) is not part of the property data, but rather is part of the document metadata, so you use SelectResult.expression(Meta.id) to retrieve it.
  2. Execute the query and loop thru the results. Get the name, price and id from the database and create a Pizza object. Append each Pizza object to menu and return menu.

Build and run.

Couchbase Tutorial - Build and Run with pre-loaded db

Perfect! The menu loads from the database, but customers and their orders are not getting saved. You’ll fix that next.

Storing Customers and Orders

Still in DataManager.swift, add the following to the extension for Customer data:


func getCustomers() -> [Customer] {
  var customers: [Customer] = []

  // 1
  let query = QueryBuilder
    .select(SelectResult.all(), SelectResult.expression(Meta.id))
    .from(DataSource.database(database))
    .where(Expression.property("type")
      .equalTo(Expression.string(DocTypes.customer.rawValue)))

  // 2
  do {
    for result in try query.execute() {
      guard let dict = result.dictionary(forKey: databaseName),
        let name = dict.string(forKey: CustomerKeys.name.rawValue),
        let street = dict.string(forKey: CustomerKeys.street.rawValue),
        let city = dict.string(forKey: CustomerKeys.city.rawValue),
        let state = dict.string(forKey: CustomerKeys.state.rawValue),
        let postalCode = dict.string(forKey: CustomerKeys.postalCode.rawValue),
        let phoneNumber = dict.string(forKey: CustomerKeys.phoneNumber.rawValue),
        let id = result.string(forKey: CustomerKeys.id.rawValue) else {
          continue
      }

      // 3
      let customer = Customer(
        id: id, 
        name: name, 
        street: street, 
        city: city, 
        state: state, 
        postalCode: postalCode, 
        phoneNumber: phoneNumber)
      customers.append(customer)
    }
  } catch {
    fatalError("Error running the query")
  }

  return customers
}

Here’s what’s happening:

  1. Similar to getMenu(), you build a query to get all the customer data from the database.
  2. Execute the query and get the data values from the result dictionary.
  3. Create a Customer object and add it to customers.

Add the following to same section:


func add(customer: Customer) {
  // 1
  let mutableDoc = MutableDocument()
    .setString(customer.name, forKey: CustomerKeys.name.rawValue)
    .setString(customer.street, forKey: CustomerKeys.street.rawValue)
    .setString(customer.city, forKey: CustomerKeys.city.rawValue)
    .setString(customer.state, forKey: CustomerKeys.state.rawValue)
    .setString(customer.postalCode, forKey: CustomerKeys.postalCode.rawValue)
    .setString(customer.phoneNumber, forKey: CustomerKeys.phoneNumber.rawValue)
    .setString(DocTypes.customer.rawValue, forKey: "type")

  do {
    // 2
    try database.saveDocument(mutableDoc)
  } catch {
    fatalError("Error saving document")
  }
}

Here’s what this does:

  1. Create a MutableDocument and store the Customer properties in it.
  2. Save the document to the database, creating the document’s metadata: id.

Open CustomerViewController.swift, and in loadData(_:), add:

customers = DataManager.shared.getCustomers()

In addCustomer(), replace:

customers?.append(customer)

With:


DataManager.shared.add(customer: customer)
customers = DataManager.shared.getCustomers()

Build and run.

Select a pizza and add a few customers. Then, go back to the Menu tab, and select the pizza again; notice your previously added customers are now there.

Couchbase Tutorial - Customer build and run

Saving Orders

Open DataManager.swift and add the following to the Order data extension:


func add(order: Order) {
  // 1
  let mutableDoc = MutableDocument()
    .setString(order.pizzaId, forKey: OrderKeys.pizzaId.rawValue)
    .setString(order.customerId, forKey: OrderKeys.customerId.rawValue)
    .setInt(order.state.rawValue, forKey: OrderKeys.state.rawValue)
    .setString(DocTypes.order.rawValue, forKey: "type")

  do {
    // 2
    try database.saveDocument(mutableDoc)
  } catch {
    fatalError("Error saving document")
  }
}

Just like above, you:

  1. Create a MutableDocument and store the Order properties in it.
  2. Save the document to the database.

Back in CustomerViewController.swift, in showReviewOrderAlert(for:with:at:), replace:


let placeOrder = UIAlertAction(title: "Place Order", style: .default) { (action) in
  let _ = Order(id: "", pizzaId: pizza.id, customerId: customer.id, state: .notStarted)
}

With:


let placeOrder = UIAlertAction(title: "Place Order", 
                               style: .default) { [tableView] (action) in
  let order = Order(
    id: "", 
    pizzaId: pizza.id, 
    customerId: customer.id, 
    state: .notStarted)
  DataManager.shared.add(order: order)
  tableView?.reloadData()
}

This adds the new order to the database and reloads the table view with the new order data.

Now, in DataManager.swift, add the following in the Order extension:


func getOrderCount(for customer: Customer) -> Int {
  let query = QueryBuilder
    // 1
    .select(SelectResult.expression(Function.count(Expression.all())))
    .from(DataSource.database(database))
    .where(Expression.property("type")
      .equalTo(Expression.string(DocTypes.order.rawValue))
      // 2
      .and(Expression.property(OrderKeys.customerId.rawValue)
        .equalTo(Expression.string(customer.id))))

  do {
    // 3
    let results = try query.execute()
    return results.allResults().map { $0.int(forKey: "$1") }.reduce(0) { $0 +  $1 }
  } catch {
    fatalError("Error running the query")
  }
}

That’s correct… another query function, but this one returns only the number of orders for a specific customer. Here’s how it works:

  1. SelectResult.expression(Function.count(Expression.all())) requests the count of all items instead of returning the items themselves.
  2. .and(Expression.property(OrderKeys.customerId.rawValue).equalTo(Expression.string(customer.id)))) is part of the where clause, and indicates to only return documents where the customerId property is equal to customer.id.
  3. results is of type ResultSet, which you can iterate over to compute the counts. But results.allResults() returns [Result] and you can use that for map-reduce to compute the count. Technically, there should only be one Result in the allResults() array, but using map-reduce works whether you have 0, 1 or more than 1 results. Note: Since count() is an aggregate function and not a property, it does not have a key, but you use the provision key “$1”

Open CustomerCell.swift and add the following to bind(with:):


let orderCount = DataManager.shared.getOrderCount(for: customer)
orderCountLabel.text = "(orderCount)n(orderCount == 1 ? "Pizza" : "Pizzas")"

Build and run.

Couchbase Tutorial - Customer order build and run

The Menu tab is fully functional now — a customer can place a pizza order. However, the kitchen staff still doesn’t know about the orders.

Displaying Orders in Kitchen

In DataManager.swift, add the following in the Order extension:


func getOrders() -> [Order] {
  var orders: [Order] = []

  // 1
  let query = QueryBuilder
    .select(SelectResult.all(),
            SelectResult.expression(Meta.id))
    .from(DataSource.database(database))
    .where(Expression.property("type")
      .equalTo(Expression.string(DocTypes.order.rawValue)))

  // 2
  do {
    for result in try query.execute() {
      guard let dict = result.dictionary(forKey: databaseName),
        let pizzaId = dict.string(forKey: OrderKeys.pizzaId.rawValue),
        let customerId = dict.string(forKey: OrderKeys.customerId.rawValue),
        let id = result.string(forKey: OrderKeys.id.rawValue) else {
          continue
      }

      let state = dict.int(forKey: OrderKeys.state.rawValue)
      let orderState = Order.OrderState(rawValue: state) ?? .notStarted

      let order = Order(
        id: id, 
        pizzaId: pizzaId, 
        customerId: customerId, 
        state: orderState)
      orders.append(order)
    }
  } catch {
    fatalError("Error running the query")
  }

  return orders
}

Just like in getMenu() and getCustomers(), this:

  1. Creates a query for all orders.
  2. Loops thru each document and gets all of the property values and id.
  3. Creates an Order object and adds it to the orders array.

Open KitchenViewController.swift, and in loadData(_:), add:

orders = DataManager.shared.getOrders()

This loads all of the orders. If you look at Order.swift, you’ll see the properties: id, pizzaId, customerId and state. To display order information, you need to retrieve the pizza and customer data using their respective id’s.

Open DataManager.swift and add the following to the Pizza data extension:


func getPizza(id: String) -> Pizza? {
  guard let doc = database.document(withID: id),
    let name = doc.string(forKey: PizzaKeys.name.rawValue) else {
      return nil
  }

  return Pizza(id: id, name: name, price: doc.double(forKey: PizzaKeys.price.rawValue))
}

This queries the database for the document with the specified id and returns a Pizza object.

Add the following to the Customer data extension:


func getCustomer(id: String) -> Customer? {
  guard let doc = database.document(withID: id),
    let name = doc.string(forKey: CustomerKeys.name.rawValue),
    let street = doc.string(forKey: CustomerKeys.street.rawValue),
    let city = doc.string(forKey: CustomerKeys.city.rawValue),
    let state = doc.string(forKey: CustomerKeys.state.rawValue),
    let postalCode = doc.string(forKey: CustomerKeys.postalCode.rawValue),
    let phoneNumber = doc.string(forKey: CustomerKeys.phoneNumber.rawValue) else {
      return nil
  }

  return Customer(
    id: id, 
    name: name, 
    street: street, 
    city: city, 
    state: state, 
    postalCode: postalCode, 
    phoneNumber: phoneNumber)
}

This returns the Customer object for the specified id.

It’s time to use these two new functions. Open Order.swift and replace:


var pizza: Pizza? {
  return nil
}
var customer: Customer? {
  return nil
}

With:


var pizza: Pizza? {
  return DataManager.shared.getPizza(id: pizzaId)
}
var customer: Customer? {
  return DataManager.shared.getCustomer(id: customerId)
}

Build and run.

Couchbase Tutorial - Kitchen build and run

Selecting an order in the Kitchen tab displays a UIAlertController, however, nothing happens when you tap Yes because you haven’t implemented a way to update the order’s state in the database.

Open DataManager.swift and add the following to the Order data extension:


func update(order: Order) {
  // 1
  guard let mutableDoc = database.document(withID: order.id)?.toMutable() else {
    return
  }

  mutableDoc.setInt(order.state.rawValue, forKey: OrderKeys.state.rawValue)
  mutableDoc.setString(order.pizzaId, forKey: OrderKeys.pizzaId.rawValue)
  mutableDoc.setString(order.customerId, forKey: OrderKeys.customerId.rawValue)

  do {
    // 2
    try database.saveDocument(mutableDoc)
  } catch {
    fatalError("Error updating document")
  }
}

A closer look shows some similarities to the previous get and add functions:

  1. Calling database.document(withID:) returns an immutable document, but a call to toMutable() returns a mutable copy so you can update it with the new order values.
  2. Calling database.saveDocument() saves the updated order.

In KitchenViewController.swift, find tableView(_:didSelectRowAt:), and replace:


let moveOrder = UIAlertAction(title: "Yes", style: .default) { (action) in
  let _ = Order(id: order.id,
                pizzaId: order.pizzaId,
                customerId: order.customerId,
                state: nextState)
}

With:


let moveOrder = UIAlertAction(title: "Yes", style: .default) { [weak self] (action) in
  let orderUpdate = Order(
    id: order.id,
    pizzaId: order.pizzaId,
    customerId: order.customerId,
    state: nextState)
  DataManager.shared.update(order: orderUpdate)
  self?.orders = DataManager.shared.getOrders()
}

Build and run.

Now you can tap an Order cell and move it to the next kitchen state.

Couchbase Tutorial - Kitchen next state build and run

Updating the Menu

Open KitchenMenuViewController.swift and in loadData(_:), add:

menu = DataManager.shared.getMenu()

This loads the menu from the database.

Selecting a cell or tapping the + presents the PizzaViewController. If selecting a cell, it loads the selected pizza properties into the appropriate text fields and allows you to update the values. Tapping + loads them with defaults of “Cheese” and “$5.99”. Tap Save to update pizza or add a new pizza to the menu.

However, you need to add some code to save it to the database with the updated or new pizza menu items.

Open DataManager.swift and add the following to the Pizza data extension:


func add(pizza: Pizza) {
  let mutableDoc = MutableDocument()
    .setDouble(pizza.price, forKey: PizzaKeys.price.rawValue)
    .setString(pizza.name, forKey: PizzaKeys.name.rawValue)
    .setString(DocTypes.pizza.rawValue, forKey: "type")

  do {
    try database.saveDocument(mutableDoc)
  } catch {
    fatalError("Error saving document")
  }
}

func update(pizza: Pizza) {
  guard let mutableDoc = database.document(withID: pizza.id)?.toMutable() else { return }

  mutableDoc.setDouble(pizza.price, forKey: PizzaKeys.price.rawValue)
  mutableDoc.setString(pizza.name, forKey: PizzaKeys.name.rawValue)

  do {
    try database.saveDocument(mutableDoc)
  } catch {
    fatalError("Error updating document")
  }
}

func delete(pizza: Pizza) {
  guard let doc = database.document(withID: pizza.id) else { return }

  do {
    try database.deleteDocument(doc)
  } catch {
    fatalError("Error deleting document")
  }
}

The add() and update() functions are similar to those previously added, and there’s nothing unusual in delete(), just get the document and call database.deleteDocument() to delete it.

Open PizzaViewController.swift and replace the following in saveTapped():


if let editPizza = editPizza {
  if editPizza.name != pizzaName || editPizza.price != pizzaPrice {
    let _ = Pizza(id: editPizza.id, name: pizzaName, price: pizzaPrice)
  }
} else {
  let _ = Pizza(id: "", name: pizzaName, price: pizzaPrice)
}

With:


if let editPizza = editPizza {
  if editPizza.name != pizzaName || editPizza.price != pizzaPrice {
    let pizza = Pizza(id: editPizza.id, name: pizzaName, price: pizzaPrice)
    DataManager.shared.update(pizza: pizza)
  }
} else {
  let pizza = Pizza(id: "", name: pizzaName, price: pizzaPrice)
  DataManager.shared.add(pizza: pizza)
}

Open KitchenMenuViewController.swift and in tableView(_:commit:forRowAt:) replace:


if editingStyle == .delete {
  menu?.remove(at: indexPath.row)
}

With:


if editingStyle == .delete,
  let pizza = menu?[indexPath.row] {
  DataManager.shared.delete(pizza: pizza)
  menu = DataManager.shared.getMenu()
}

Build and run.

In Kitchen Menu, tap the Cheese cell, change the price to $12.99 and tap Save.

Couchbase Tutorial - Kitchen menu edited build and run

Excellent! Ray now has full control over his menu; he can add, update and delete items on the menu right from within the app.

Adding Synchronization

In production, you’d use Couchbase Server and Sync Gateway; both installed on a server in some data center. The app would talk to Sync Gateway and handle the data synching between the server and mobile apps. However, you’re going to run Sync Gateway locally in walrus mode, which is an in-memory, development-only mode.

Installing Sync Gateway

Download Sync Gateway community edition and unzip the file. If you downloaded and unzipped the file to the Downloads folder, start it with this command in a Terminal window:


~/Downloads/couchbase-sync-gateway/bin/sync_gateway -dbname="pizza-db"

You should see Starting server on localhost:4984 ... in your Terminal window.

Go to http://localhost:4984 to see that it is up and running:

{couchdb:

Great, Sync Gateway is running! It’s time to replicate the database to the local server.

Synchronization

At the top of DataManager.swift, below the import section, add the following Notification.Name extension:


extension Notification.Name {
  public static let dbUpdated = Notification
    .Name("com.razeware.rayspizzeria.notification.name.dbUpdated")
}

Next, add the following to the property section:


private var replicator: Replicator?

Now, set up the replicator by adding the following to the end of init():


// 1
guard let syncURL = URL(string: "ws://localhost:4984/(database.name)") else { return }

let syncEndpoint = URLEndpoint(url: syncURL)
let replicatorConfig = ReplicatorConfiguration(database: database, target: syncEndpoint)
replicatorConfig.continuous = true
replicator = Replicator(config: replicatorConfig)

// 2
replicator?.addChangeListener { (change) in
  let status = change.status
  if let error = status.error as NSError? {
    print("Error code :: (error.code)")
  } else {
    guard status.progress.completed == status.progress.total else { return }

    NotificationCenter.default.post(name: .dbUpdated, object: nil)
  }
}

// 3
replicator?.start()

This code block does three things:

  1. Set up the replicator configuration and create the replicator.
    • Notice the URL scheme is ws: instead of the usual http:. Couchbase Lite 2.0 uses WebSockets as its communication protocol.
    • replicatorConfig.continuous = true keeps the replicator active indefinitely waiting to replicate changed documents.
  2. Listen for document changes and post a notification. You’ll add observers below to update the UI when these changes occur.
  3. Start the replicator.

In viewDidLoad() of MenuViewController.swift, CustomerViewController.swift, KitchenMenuViewController.swift and KitchenViewController.swift add:


NotificationCenter.default.addObserver(self, 
                                       selector: #selector(loadData(_:)), 
                                       name: .dbUpdated, 
                                       object: nil)

Build and run.

You’ll see GET /pizza-db/_blipsync (as GUEST) in the Terminal window running sync_gateway.

You can see more information from Sync Gateway’s Admin panel (http://localhost:4985/_admin/db/pizza-db).

Admin view of pizza-db

Add a new customer. Refresh Sync Gateway’s Admin panel, and you’ll see a new file. Select the file to see the contents.

Admin view of customer

Simulate Multiple Users

To simulate multiple users, you can run the app on a second simulator. In Xcode, select a different simulator in scheme run destination.

Build and run.

You’ll see two simulators with one running RaysPizzeria. Tap the app icon in the other simulator to run the app.

Two simulators

To see changes in one simulator affect the other, make a few menu changes. In simulator one, select the Menu tab. In simulator two, select the Kitchen tab, tap the Menu bar button, select a pizza and change the price. Once you tap Save, the menu will update in simulator one.

Where to Go From Here?

You can download the completed version of the project using the Download Materials button at the top or bottom of this tutorial.

Couchbase is a great platform for app development, especially if your app is data-driven. You may have noticed that Couchbase Mobile does not require an additional networking layer/framework to download data or JSON parsing. Connecting your app to a Couchbase Server requires Sync Gateway and adding a Replicator in your code.

For more information about Couchbase Lite, check out their documentation. More detailed API documentation is here. You barely touched what’s available in the Query API. If you’re feeling adventours, expand the queries within this app.

We hope you enjoyed this Couchbase tutorial on Couchbase for . If you have any questions or comments, please join the forum discussion below!



Source link https://www.raywenderlich.com/5480-couchbase-tutorial-for-ios-getting-started

LEAVE A REPLY

Please enter your comment!
Please enter your name here