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...
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 thecustomer-ios
directory. A variable namedDeepstreamHubURL
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 dialogOrderViewController.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
MenuViewController
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
– anenum
representing the current state of the order e.g. 'in-progress'.viewDidLoad()
– implements theUIViewController
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)
}
}