why deepstreamHub? compare us getting started feature roadmap faq
use cases pricing
developers
company
enterprise blog contact

In this post we'll walk through building a realtime coffee ordering application for iOS using deepstreamHub and Swift.

The application we'll be looking at:

  • Displays and updates a synchronized list of menu items between an iOS app and a browser-based dashboard.

  • Allows a user to select and purchase a coffee from the menu.

  • Tracks the progress of an order in realtime e.g. Order received, In Progress...

Demo animation

This example application uses a lot of the core deepstream concepts, so we'd recommend being familiar with the basics of Records and Lists. Prior Swift experience will be helpful as well.

Setting up

For the sake of brevity, I won't be covering project setup or some of the view logic. If you're new to iOS development here are some resources that might help:

  • Getting Started With Swift
  • iOS Getting Started (Swift)

Create a free account and get your API key

Create an account at deepstreamHub, create an application and make a note of its url.

To run the iOS app:

  • Ensure you have XCode(>= 8.0) and Cocoapods installed.

  • From the terminal, clone the application repository:

    git clone git@github.com:deepstreamIO/dsh-demo-coffee.git
    cd dsh-demo-coffee/customer-ios
    
  • Install the dependencies (this could take a while):

    pod install
    
  • Open the project in XCode:

    open customer-ios.xcworkspace
    
  • When XCode opens, from the project navigator on the left of the window, open the file named AppDelegate.swift inside the customer-ios directory. A variable named DeepstreamHubURL holds the url of your deepstreamHub application. Remember the url of the application which you created earlier? You should enter it here.

  • Click on the triangle ('Build and Run') at the top of the screen to run the application in the iOS simulator. If the simulator doesn't fit on your screen, try changing your zoom or simulating a smaller device.

To run the dashboard:

  • Open up dsh-demo-coffee/barista/application.js in a text editor and change the deepstream url to point to the application you created earlier.

  • Run a web server from the dsh-demo-coffee/barista directory.

cd ../barista 
python -m SimpleHTTPServer
  • Open the dashboard (at 0.0.0.0:8000 by default) in a web browser.

Using the application:

If all is well, you should now see a menu of available coffees in the iOS simulator. Try requesting one by clicking on it, and it should take you to a progress screen. You should now be able to see the request in the dashboard and update it by clicking on the progress bar.

A closer look at the app

Back in XCode, let's take another look at the files in the project navagator. The important files are:

  • AppDelegate.swift – This is the entry point to the application, and is where we setup the deepstream client and connect to the server

  • View Controllers – A directory containing view code and logic for:

    • MenuViewController.swift – The menu dialog
    • OrderViewController.swift – The order progress dialog

To start with we'll look at AppDelegate.swift. The AppDelegate class implements the application method from UIApplicationDelegate, which is called when the application is initialized. This makes it a good place to instantiate the deepstream client:

let DeepstreamHubURL = "wss://..."
class AppDelegate: UIResponder, UIApplicationDelegate {
    // ...
    func application(/* ... */) -> Bool {
        IOSDeepstreamFactory.getInstance().getClient(DeepstreamHubURL, callback: { (client) in
            client!.setRuntimeErrorHandler(RuntimeErrorHandler())
            client!.addConnectionChange(AppConnectionStateListener())
            let loginResult = client!.login()
            if (loginResult!.getErrorEvent() == nil) {
                print("Successfully logged in")
            } else {
                print("Error: Failed to log in")
            }
        })
        return true
    }
}

Open Auth is enabled on deepstreamHub applications by default, so we don't need to provide any parameters to client.login. Click here for more information on authentication.

Error handling

It's always best to provide the client an error handler that implements the DeepstreamRuntimeErrorHandler protocol.

We can implement a basic error handler that logs to the standard output as follows:

final class RuntimeErrorHandler : NSObject, DeepstreamRuntimeErrorHandler {
    func onException(_ topic: Topic!, event: Event!, errorMessage: String!) {
        if (errorMessage != nil && topic != nil && event != nil) {
            print("Error: \(errorMessage!) for topic: \(topic!), event: \(event!)")
        }
    }
}

Connection state

We also listen for connection changes and log them for debugging purposes:

final class AppConnectionStateListener : NSObject, ConnectionStateListener {
    func connectionStateChanged(_ connectionState: ConnectionState!) {
        print("Connection state changed \(connectionState!)")
    }
}

View

The MenuViewController class simply displays the menu items. When a menu item is selected, a new OrderViewController is created and a segue transitions to that view.

let Menu = [
    "espresso",
    "machiatto",
    "cappuccino",
    "latte",
    "americano"
]

class MenuViewController: UIViewController {
    //...
}

We'll skip over the details here since the class doesn't manage any real state, but feel free to explore it on your own.

OrderViewController

The OrderViewController class describes the order progress view and uses deepstream records to manage state. It has the following fields/methods:

  • menuItem – the type of coffee that was selected on the menu.
  • client – a reference to the deepstream client.
  • ordersList – a deepstream list of all orders in progress. When a new order is created, a record representing the state of the order is added to this list.
  • orderStage – an enum representing the current state of the order e.g. 'in-progress'.
  • viewDidLoad() – implements the UIViewController protocol. Called once the view has been loaded. This is where we'll setup the new order state.
class OrderViewController: UIViewController {

    // UI Labels
    //...

    var menuItem : String?

    private var client : DeepstreamClient?
    private var ordersList : List?
    var orderRecord : Record?

    enum orderStage : String {
        case received = "received"
        case inProgress = "in-progress"
        case ready = "ready"
        case delivered = "delivered"
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        /* order creation code goes here */
    }
}

Handling Orders

Creating the order record

Inside the viewDidLoad() method, we can use IOSDeepstreamFactory to get a global reference to the deepstream client connection we setup earlier.

IOSDeepstreamFactory.getInstance().getClient(callback: { (client) in
    self.client = client
    // ....
})

We can now get a reference to the order list using RecordHandler.getList().

// Get list
let list = self.client!.record.getList("orders")

self.ordersList = list

Now we create a record named coffee/$uid for storing the order state, where uid is a unique random id generated using client.getUid(). Then we add the name of this record to the order list.

// Generate unique id for order
let uuid = self.client!.getUid()

// Get record handler
let orderRecord = self.client!.record.getRecord("coffee/\(uuid)")
self.orderRecord = orderRecord

//...

let order = [
    "type" : item,
    "stage" : orderStage.received.rawValue
]

self.orderRecord!.set(order.jsonElement)

self.ordersList!.addEntry(orderRecord!.name())

Realtime updates

Whenever we change an order's state in the dashboard, the corresponding order record's "stage" is updated. We can listen to changes to this value and trigger updates in the view using Record.subscribe.

// Subscribe to changes
self.orderRecord!.subscribe("stage",
        recordPathChangedCallback: OrderRecordPathChangedCallback(callback: { (data) in

        DispatchQueue.main.async {
            if let stage = OrderViewController.orderStage(rawValue: data.getAsString()!) {
                switch (stage) {
                    // Update the order progress view
                    // ...
                }
            }
        }
    })
)

Here, OrderRecordPathChangedCallback is a boilerplate class that implements the RecordPathChangedCallback protocol and simply calls the function it was initialized with:

final class OrderRecordPathChangedCallback : NSObject, RecordPathChangedCallback {

    private var callback : ((JsonElement) -> Void)!

    init(callback: @escaping (JsonElement) -> Void) {
        self.callback = callback
        super.init()
    }

    func onRecordPathChanged(_ recordName: String!, path: String!, data: JsonElement!) {
        print("Record '\(recordName!)' changed, data is now: \(data)")
        self.callback(data)
    }
}