Last mile delivery marketplaces make it easy to order delivery food from a mobile device and have it delivered to a user’s door while it’s still hot.
Marketplaces like Deliveroo, Postmates, or Uber Eats use your device’s location to serve you a list of restaurants that are close enough and open so you can get your delivery as soon as possible.
This realtime experience between the customer, restaurant, and driver relies on transactional push notifications to move the order from the kitchen to the table seamlessly. Customers want push notifications to alert them when their order is on its way and when they need to meet the driver at the door.
Setting up push notifications can be confusing and time consuming. However, with Pusher Beams API, the process is a lot easier and faster.
In this article, we will be considering how you can build apps on iOS that have transactional push notifications. For this, we will be building a make-belief food delivery app.
Once you have the requirements, let’s start.
Before we start building our application, we need to do some planning on how we want the application to work.
We will be making three applications:
This will be the API. For simplicity, we will not add any sort of authentication to the API. We will be calling the API from our iOS applications. The API should be able to provide the food inventory, the orders, and also manage the orders. We will also be sending push notifications from the backend application.
This will be the application that will be with the customer. This is where the user will be able to order food from. For simplicity, we will not have any sort of authentication and everything will be straight to the point. A customer should be able to see the inventory and order one or more from the inventory. They should also be able to see the list of their orders and the status of each order.
This will be the application that the company providing the service will use to fulfil orders. The application will display the available orders and the admin will be able to set the status for each order.
The first thing we want to build is the API. We will be adding everything required to support our iOS applications and then add push notifications later on.
To get started, create a project directory for the API. In the directory, create a new file called package.json
and in the file paste the following:
1{ 2 "main": "index.js", 3 "scripts": {}, 4 "dependencies": { 5 "body-parser": "^1.18.2", 6 "express": "^4.16.2" 7 } 8 }
Next run the command below in your terminal:
$ npm install
This will install all the listed dependencies. Next, create an index.js
file in the same directory as the package.json
file and paste in the following code:
1// -------------------------------------------------------- 2 // Pull in the libraries 3 // -------------------------------------------------------- 4 5 const app = require('express')() 6 const bodyParser = require('body-parser') 7 8 // -------------------------------------------------------- 9 // Helpers 10 // -------------------------------------------------------- 11 12 function uuidv4() { 13 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 14 var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); 15 return v.toString(16); 16 }); 17 } 18 19 20 // -------------------------------------------------------- 21 // In-memory database 22 // -------------------------------------------------------- 23 24 var user_id = null 25 26 var orders = [] 27 28 let inventory = [ 29 { 30 id: uuidv4(), 31 name: "Pizza Margherita", 32 description: "Features tomatoes, sliced mozzarella, basil, and extra virgin olive oil.", 33 amount: 39.99, 34 image: 'pizza1' 35 }, 36 { 37 id: uuidv4(), 38 name: "Bacon cheese fry", 39 description: "Features tomatoes, bacon, cheese, basil and oil", 40 amount: 29.99, 41 image: 'pizza2' 42 } 43 ] 44 45 46 // -------------------------------------------------------- 47 // Express Middlewares 48 // -------------------------------------------------------- 49 50 app.use(bodyParser.json()) 51 app.use(bodyParser.urlencoded({extended: false})) 52 53 54 // -------------------------------------------------------- 55 // Routes 56 // -------------------------------------------------------- 57 58 app.get('/orders', (req, res) => res.json(orders)) 59 60 app.post('/orders', (req, res) => { 61 let id = uuidv4() 62 user_id = req.body.user_id 63 let pizza = inventory.find(item => item["id"] === req.body.pizza_id) 64 65 if (!pizza) { 66 return res.json({status: false}) 67 } 68 69 orders.unshift({id, user_id, pizza, status: "Pending"}) 70 res.json({status: true}) 71 }) 72 73 app.put('/orders/:id', (req, res) => { 74 let order = orders.find(order => order["id"] === req.params.id) 75 76 if ( ! order) { 77 return res.json({status: false}) 78 } 79 80 orders[orders.indexOf(order)]["status"] = req.body.status 81 82 return res.json({status: true}) 83 }) 84 85 app.get('/inventory', (req, res) => res.json(inventory)) 86 app.get('/', (req, res) => res.json({status: "success"})) 87 88 89 // -------------------------------------------------------- 90 // Serve application 91 // -------------------------------------------------------- 92 93 app.listen(4000, _ => console.log('App listening on port 4000!'))
The above code is a simple Express application. Everything is self-explanatory and has comments to guide you.
In the first route, /orders
, we display the list of orders available from the in-memory data store. In the second route, the POST /orders
we just add a new order to the list of orders
. In the third route, PUT /orders/:id
we just modify the status of a single order from the list of orders
. In the fourth route, GET /inventory
we list the inventory available from the list of inventory
in the database.
We are done with the API for now and we will revisit it when we need to add the push notification code. If you want to test that the API is working, then run the following command on your terminal:
$ node index.js
This will start a new Node server listening on port 4000.
The next thing we need to do is build the client application in Xcode. To start, launch Xcode and create a new ‘Single Application’ project. We will name our project PizzaareaClient.
Once the project has been created, exit Xcode and create a new file called Podfile
in the root of the Xcode project you just created. In the file paste in the following code:
1platform :ios, '11.0' 2 3 target 'PizzareaClient' do 4 use_frameworks! 5 pod 'PusherSwift', '~> 5.1.1' 6 pod 'Alamofire', '~> 4.6.0' 7 end
In the file above, we specified the dependencies the project needs to run. Remember to change the target
above to the name of your project. Now in your terminal, run the following command to install the dependencies:
$ pod install
After the installation is complete, open the Xcode workspace file that was generated by CocoaPods. This should relaunch Xcode.
When Xcode has been relaunched, open the Main.storyboard
file and in there we will create the storyboard for our client application. Below is a screenshot of how we have designed our storyboard:
The first scene is the navigation view controller which has a table view controller as the root controller. The navigation controller is the initial controller that is loaded when the application is launched.
The second scene is the view controller that lists the inventory that we have available.
Create a new file in Xcode called PizzaTableListViewController.swift
, make it the custom class for the second scene and paste in the following code:
1import UIKit 2 import Alamofire 3 4 class PizzaListTableViewController: UITableViewController { 5 6 var pizzas: [Pizza] = [] 7 8 override func viewDidLoad() { 9 super.viewDidLoad() 10 11 navigationItem.title = "Select Pizza" 12 13 fetchInventory { pizzas in 14 guard pizzas != nil else { return } 15 self.pizzas = pizzas! 16 self.tableView.reloadData() 17 } 18 } 19 20 private func fetchInventory(completion: @escaping ([Pizza]?) -> Void) { 21 Alamofire.request("http://127.0.0.1:4000/inventory", method: .get) 22 .validate() 23 .responseJSON { response in 24 guard response.result.isSuccess else { return completion(nil) } 25 guard let rawInventory = response.result.value as? [[String: Any]?] else { return completion(nil) } 26 27 let inventory = rawInventory.flatMap { pizzaDict -> Pizza? in 28 var data = pizzaDict! 29 data["image"] = UIImage(named: pizzaDict!["image"] as! String) 30 31 return Pizza(data: data) 32 } 33 34 completion(inventory) 35 } 36 } 37 38 @IBAction func ordersButtonPressed(_ sender: Any) { 39 performSegue(withIdentifier: "orders", sender: nil) 40 } 41 42 override func numberOfSections(in tableView: UITableView) -> Int { 43 return 1 44 } 45 46 override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 47 return pizzas.count 48 } 49 50 override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 51 let cell = tableView.dequeueReusableCell(withIdentifier: "Pizza", for: indexPath) as! PizzaTableViewCell 52 53 cell.name.text = pizzas[indexPath.row].name 54 cell.imageView?.image = pizzas[indexPath.row].image 55 cell.amount.text = "$\(pizzas[indexPath.row].amount)" 56 cell.miscellaneousText.text = pizzas[indexPath.row].description 57 58 return cell 59 } 60 61 override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 62 return 100.0 63 } 64 65 override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 66 performSegue(withIdentifier: "pizza", sender: self.pizzas[indexPath.row] as Pizza) 67 } 68 69 override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 70 if segue.identifier == "pizza" { 71 guard let vc = segue.destination as? PizzaViewController else { return } 72 vc.pizza = sender as? Pizza 73 } 74 } 75 }
In the viewDidLoad
method, we call the fetchInventory
method that uses Alamofire
to fetch the inventory from our backend API then we save the response to the orders
property of the controller.
The ordersButtonPressed
is linked to the Orders
button on the scene and this just presents the scene with the list of orders using a named segue orders
.
The tableView*
methods implement methods available to the UITableViewDelegate
protocol and should be familiar to you.
The final method prepare
simply sends the pizza
to the view controller on navigation. This pizza
is only sent over if the view controller being loaded is the PizzaViewController
though.
Before we create the third scene, create a PizzaTableViewCell.swift
class and paste in the following:
1import UIKit 2 3 class PizzaTableViewCell: UITableViewCell { 4 5 @IBOutlet weak var pizzaImageView: UIImageView! 6 @IBOutlet weak var name: UILabel! 7 @IBOutlet weak var miscellaneousText: UILabel! 8 @IBOutlet weak var amount: UILabel! 9 10 override func awakeFromNib() { 11 super.awakeFromNib() 12 } 13 }
⚠️ Make sure the custom class of the cells in the second scene is
PizzaTableViewCell
and that the reusable identifier isPizza
.
The third scene in our storyboard is the Pizza view scene. This is where the selected inventory can be viewed.
Create a PizzaViewController.swift
file, make it the custom class for the scene above and paste in the following code:
1import UIKit 2 import Alamofire 3 4 class PizzaViewController: UIViewController { 5 6 var pizza: Pizza? 7 8 @IBOutlet weak var amount: UILabel! 9 @IBOutlet weak var pizzaDescription: UILabel! 10 @IBOutlet weak var pizzaImageView: UIImageView! 11 12 override func viewDidLoad() { 13 super.viewDidLoad() 14 15 navigationItem.title = pizza!.name 16 pizzaImageView.image = pizza!.image 17 pizzaDescription.text = pizza!.description 18 amount.text = "$\(String(describing: pizza!.amount))" 19 } 20 21 @IBAction func buyButtonPressed(_ sender: Any) { 22 let parameters = [ 23 "pizza_id": pizza!.id, 24 "user_id": AppMisc.USER_ID 25 ] 26 27 Alamofire.request("http://127.0.0.1:4000/orders", method: .post, parameters: parameters) 28 .validate() 29 .responseJSON { response in 30 guard response.result.isSuccess else { return self.alertError() } 31 32 guard let status = response.result.value as? [String: Bool], 33 let successful = status["status"] else { return self.alertError() } 34 35 successful ? self.alertSuccess() : self.alertError() 36 } 37 } 38 39 private func alertError() { 40 return self.alert( 41 title: "Purchase unsuccessful!", 42 message: "Unable to complete purchase please try again later." 43 ) 44 } 45 46 private func alertSuccess() { 47 return self.alert( 48 title: "Purchase Successful", 49 message: "You have ordered successfully, your order will be confirmed soon." 50 ) 51 } 52 53 private func alert(title: String, message: String) { 54 let alertCtrl = UIAlertController(title: title, message: message, preferredStyle: .alert) 55 56 alertCtrl.addAction(UIAlertAction(title: "Okay", style: .cancel) { action in 57 self.navigationController?.popViewController(animated: true) 58 }) 59 60 present(alertCtrl, animated: true, completion: nil) 61 } 62 }
In the code above, we have multiple @IBOutlet
‘s and a single @IBAction
. You need to link the outlets and actions to the controller from the storyboard.
In the viewDidLoad
we set the outlets so they display the correct values using the pizza
sent from the previous view controller. The buyButtonPressed
method uses Alamofire
to place an order by sending a request to the API. The remaining methods handle displaying the error or success response from the API.
The next scene is the orders list scene. In this scene, all the orders are listed so the user can see them and their status:
Create a OrderTableViewController.swift
file, make it the custom class for the scene above and paste in the following code:
1import UIKit 2 import Alamofire 3 4 class OrdersTableViewController: UITableViewController { 5 6 var orders: [Order] = [] 7 8 override func viewDidLoad() { 9 super.viewDidLoad() 10 navigationItem.title = "Orders" 11 12 fetchOrders { orders in 13 self.orders = orders! 14 self.tableView.reloadData() 15 } 16 } 17 18 private func fetchOrders(completion: @escaping([Order]?) -> Void) { 19 Alamofire.request("http://127.0.0.1:4000/orders").validate().responseJSON { response in 20 guard response.result.isSuccess else { return completion(nil) } 21 22 guard let rawOrders = response.result.value as? [[String: Any]?] else { return completion(nil) } 23 24 let orders = rawOrders.flatMap { ordersDict -> Order? in 25 guard let orderId = ordersDict!["id"] as? String, 26 let orderStatus = ordersDict!["status"] as? String, 27 var pizza = ordersDict!["pizza"] as? [String: Any] else { return nil } 28 29 pizza["image"] = UIImage(named: pizza["image"] as! String) 30 31 return Order( 32 id: orderId, 33 pizza: Pizza(data: pizza), 34 status: OrderStatus(rawValue: orderStatus)! 35 ) 36 } 37 38 completion(orders) 39 } 40 } 41 42 @IBAction func closeButtonPressed(_ sender: Any) { 43 dismiss(animated: true, completion: nil) 44 } 45 46 override func numberOfSections(in tableView: UITableView) -> Int { 47 return 1 48 } 49 50 override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 51 return orders.count 52 } 53 54 override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 55 let cell = tableView.dequeueReusableCell(withIdentifier: "order", for: indexPath) 56 let order = orders[indexPath.row] 57 58 cell.textLabel?.text = order.pizza.name 59 cell.imageView?.image = order.pizza.image 60 cell.detailTextLabel?.text = "$\(order.pizza.amount) - \(order.status.rawValue)" 61 62 return cell 63 } 64 65 override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 66 return 100.0 67 } 68 69 override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 70 performSegue(withIdentifier: "order", sender: orders[indexPath.row] as Order) 71 } 72 73 override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 74 if segue.identifier == "order" { 75 guard let vc = segue.destination as? OrderViewController else { return } 76 vc.order = sender as? Order 77 } 78 } 79 }
The code above is similar to the code in the PizzaTableViewController
above. However, instead of fetching the inventory, it fetches the orders
and instead of passing the pizza
in the last method, it passes the order
to the next controller. The controller also comes with a closeButtonPressed
method that just dismisses the controller and returns to the inventory list scene.
The next scene is the order scene. In this scene, we can see the status of the order:
⚠️ The scene above has an invisible view right above the status label. You need to use this view to create an
@IBOutlet
to the controller.
Create a OrderViewController.swift
file, make it the custom class for the scene above and paste in the following code:
1import UIKit 2 3 class OrderViewController: UIViewController { 4 5 var order: Order? 6 7 @IBOutlet weak var status: UILabel! 8 @IBOutlet weak var activityView: ActivityIndicator! 9 10 override func viewDidLoad() { 11 super.viewDidLoad() 12 13 navigationItem.title = order?.pizza.name 14 15 activityView.startLoading() 16 17 switch order!.status { 18 case .pending: 19 status.text = "Processing Order" 20 case .accepted: 21 status.text = "Preparing Order" 22 case .dispatched: 23 status.text = "Order is on its way!" 24 case .delivered: 25 status.text = "Order delivered" 26 activityView.strokeColor = UIColor.green 27 activityView.completeLoading(success: true) 28 } 29 } 30 }
In the code above, we are doing all the work in our viewDidLoad
method. In there we have the ActivityIndicator
class, which we will create next, referenced as an @IBOutlet
.
We are using a third-party library called the [ActivityIndicator](https://github.com/abdulKarim002/activityIndicator)
but since we the package is not available via CocoaPods, we have opted to creating it ourselves and importing it. Create a new file in Xcode called ActivityIndicator
and paste the code from the repo here into it.
Next, create a new Order.swift
file and paste in the following code:
1import Foundation 2 3 struct Order { 4 let id: String 5 let pizza: Pizza 6 var status: OrderStatus 7 } 8 9 enum OrderStatus: String { 10 case pending = "Pending" 11 case accepted = "Accepted" 12 case dispatched = "Dispatched" 13 case delivered = "Delivered" 14 }
Finally, create a Pizza.swift
and paste in the following code:
1import UIKit 2 3 struct Pizza { 4 let id: String 5 let name: String 6 let description: String 7 let amount: Float 8 let image: UIImage 9 10 init(data: [String: Any]) { 11 self.id = data["id"] as! String 12 self.name = data["name"] as! String 13 self.amount = data["amount"] as! Float 14 self.description = data["description"] as! String 15 self.image = data["image"] as! UIImage 16 } 17 }
That is all for the client application. One last thing we need to do though is modify the info.plist
file. We need to add an entry to the plist
file to allow connection to our local server:
Let’s move on to the admin application.
Launch a new instance of Xcode and create a new ‘Single Application’ project. We will name our project PizzaareaAdmin.
Once the project has been created, exit Xcode and create a new file called Podfile
in the root of the Xcode project you just created. In the file paste in the following code:
1platform :ios, '11.0' 2 3 target 'PizzareaAdmin' do 4 use_frameworks! 5 pod 'PusherSwift', '~> 5.1.1' 6 pod 'Alamofire', '~> 4.6.0' 7 end
In the file above, we specified the dependencies the project needs to run. Remember to change the **target**
above to the name of your project. Now in your terminal, run the following command to install the dependencies:
$ pod install
After the installation is complete, open the Xcode workspace file that was generated by CocoaPods. This should relaunch Xcode.
When Xcode has been relaunched, open the Main.storyboard
file and in there we will create the storyboard for our client application. Below is a screenshot of how we have designed our storyboard:
Above we have a navigation view controller that is the initial view controller.
The orders list scene is supposed to show the list of the clients orders and from there we can change the status of each order when we want.
Create a new file in Xcode called OrdersListViewController.swift
, make it the custom class for the second scene and paste in the following code:
1import UIKit 2 import Alamofire 3 4 class OrdersTableViewController: UITableViewController { 5 6 var orders: [Order] = [] 7 8 override func viewDidLoad() { 9 super.viewDidLoad() 10 11 navigationItem.title = "Client Orders" 12 13 fetchOrders { orders in 14 self.orders = orders! 15 self.tableView.reloadData() 16 } 17 } 18 19 private func fetchOrders(completion: @escaping([Order]?) -> Void) { 20 Alamofire.request("http://127.0.0.1:4000/orders").validate().responseJSON { response in 21 guard response.result.isSuccess else { return completion(nil) } 22 23 guard let rawOrders = response.result.value as? [[String: Any]?] else { return completion(nil) } 24 25 let orders = rawOrders.flatMap { ordersDict -> Order? in 26 guard let orderId = ordersDict!["id"] as? String, 27 let orderStatus = ordersDict!["status"] as? String, 28 var pizza = ordersDict!["pizza"] as? [String: Any] else { return nil } 29 30 pizza["image"] = UIImage(named: pizza["image"] as! String) 31 32 return Order( 33 id: orderId, 34 pizza: Pizza(data: pizza), 35 status: OrderStatus(rawValue: orderStatus)! 36 ) 37 } 38 39 completion(orders) 40 } 41 } 42 43 override func numberOfSections(in tableView: UITableView) -> Int { 44 return 1 45 } 46 47 override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 48 return orders.count 49 } 50 51 override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 52 let cell = tableView.dequeueReusableCell(withIdentifier: "order", for: indexPath) 53 let order = orders[indexPath.row] 54 55 cell.textLabel?.text = order.pizza.name 56 cell.imageView?.image = order.pizza.image 57 cell.detailTextLabel?.text = "$\(order.pizza.amount) - \(order.status.rawValue)" 58 59 return cell 60 } 61 62 override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 63 return 100.0 64 } 65 66 override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 67 let order: Order = orders[indexPath.row] 68 69 let alertCtrl = UIAlertController( 70 title: "Change Status", 71 message: "Change the status of the order based on the progress made.", 72 preferredStyle: .actionSheet 73 ) 74 75 alertCtrl.addAction(createActionForStatus(.pending, order: order)) 76 alertCtrl.addAction(createActionForStatus(.accepted, order: order)) 77 alertCtrl.addAction(createActionForStatus(.dispatched, order: order)) 78 alertCtrl.addAction(createActionForStatus(.delivered, order: order)) 79 alertCtrl.addAction(createActionForStatus(nil, order: nil)) 80 81 present(alertCtrl, animated: true, completion: nil) 82 } 83 84 private func createActionForStatus(_ status: OrderStatus?, order: Order?) -> UIAlertAction { 85 let alertTitle = status == nil ? "Cancel" : status?.rawValue 86 let alertStyle: UIAlertActionStyle = status == nil ? .cancel : .default 87 88 let action = UIAlertAction(title: alertTitle, style: alertStyle) { action in 89 if status != nil { 90 self.setStatus(status!, order: order!) 91 } 92 } 93 94 if status != nil { 95 action.isEnabled = status?.rawValue != order?.status.rawValue 96 } 97 98 return action 99 } 100 101 private func setStatus(_ status: OrderStatus, order: Order) { 102 updateOrderStatus(status, order: order) { successful in 103 guard successful else { return } 104 guard let index = self.orders.index(where: {$0.id == order.id}) else { return } 105 106 self.orders[index].status = status 107 self.tableView.reloadData() 108 } 109 } 110 111 private func updateOrderStatus(_ status: OrderStatus, order: Order, completion: @escaping(Bool) -> Void) { 112 let url = "http://127.0.0.1:4000/orders/" + order.id 113 let params = ["status": status.rawValue] 114 115 Alamofire.request(url, method: .put, parameters: params).validate().responseJSON { response in 116 guard response.result.isSuccess else { return completion(false) } 117 guard let data = response.result.value as? [String: Bool] else { return completion(false) } 118 119 completion(data["status"]!) 120 } 121 } 122 }
The code above is similar to the code in the PizzaListTableViewController
in the client application and has been explained before.
There is a createActionForStatus
which is a helper for creating and configuring UIAlertAction
object. There is a setStatus
method that just attempts to set the status for an order and then the updateOrderStatus
method that sends the update request using Alamofire
to the API.
Next, create the Order.swift
and Pizza.swift
classes like we did before in the client application:
1// Order.swift 2 import Foundation 3 4 struct Order { 5 let id: String 6 let pizza: Pizza 7 var status: OrderStatus 8 } 9 10 enum OrderStatus: String { 11 case pending = "Pending" 12 case accepted = "Accepted" 13 case dispatched = "Dispatched" 14 case delivered = "Delivered" 15 } 16 17 18 // Pizza.swift 19 import UIKit 20 21 struct Pizza { 22 let id: String 23 let name: String 24 let description: String 25 let amount: Float 26 let image: UIImage 27 28 init(data: [String: Any]) { 29 self.id = data["id"] as! String 30 self.name = data["name"] as! String 31 self.amount = data["amount"] as! Float 32 self.description = data["description"] as! String 33 self.image = data["image"] as! UIImage 34 } 35 }
That's all for the admin application. One last thing we need to do though is modify the info.plist
file as we did in the client application.
At this point, the application works as expected out of the box. We now need to add push notifications to the application to make it more engaging even when the user is not currently using the application.
⚠️ You need to be enrolled to the Apple Developer program to be able to use the Push Notifications feature. Also Push Notifications do not work on Simulators so you will need an actual iOS device to test.
Pusher Beams API has first-class support for native iOS applications. Your iOS app instances subscribe to interests; then your servers send push notifications to those interests. Every app instance subscribed to that interest will receive the notification, even if the app is not open on the device at the time.
This section describes how you can set up an iOS app to receive transactional push notifications about your food delivery orders through Pusher.
Pusher relies on Apple Push Notification service (APNs) to deliver push notifications to iOS application users on your behalf. When we deliver push notifications, we use your APNs Key. This page guides you through the process of getting an APNs Key and how to provide it to Pusher.
Head over to the Apple Developer dashboard and then create a new Key as seen below:
When you have created the key, download it. Keep it safe as we will need it in the next section.
⚠️ You have to keep the generated key safe as you cannot get it back if you lose it.
The next thing you need to do is create a new Pusher Beams application from the Pusher dashboard.
When you have created the application, you should be presented with a Quickstart wizard that will help you set up the application.
To configure Beams, you will need to get an APNs key from Apple. This is the same key as the one we downloaded in the previous section. Once you’ve got the key, upload it to the Quickstart wizard.
Enter your Apple Team ID. Get the Team ID. Click Continue to proceed to the next step.
In your client application, open the Podfile
and add the following pod to the list of dependencies:
pod 'PushNotifications'
Now run the pod install
command as you did earlier to pull in the notifications package. When installation is complete, create a new class AppMisc.swift
and in there paste the following:
1class AppMisc { 2 static let USER_ID = NSUUID().uuidString.replacingOccurrences(of: "-", with: "_") 3 }
In the little class above, we generate a user ID for the session. In a real application, you would typically have an actual user ID after authentication.
Next open the AppDelegate
class and import the PushNotifications
package:
import PushNotifications
Now, as part of the AppDelegate
class, add the following:
1let pushNotifications = PushNotifications.shared 2 3 func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 4 self.pushNotifications.start(instanceId: "PUSHER_NOTIF_INSTANCE_ID") 5 self.pushNotifications.registerForRemoteNotifications() 6 return true 7 } 8 9 func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { 10 self.pushNotifications.registerDeviceToken(deviceToken) { 11 try? self.pushNotifications.subscribe(interest: "orders_" + AppMisc.USER_ID) 12 } 13 }
💡 Replace
PUSHER_PUSH_NOTIF_INSTANCE_ID
with the key given to you by the Pusher application.
In the code above, we set up push notifications in the application(didFinishLaunchingWithOptions:)
method and then we subscribe in the application(didRegisterForRemoteNotificationsWithDeviceToken:)
method.
Next, we need to enable push notifications for the application. In the project navigator, select your project, and click on the Capabilities tab. Enable Push Notifications by turning the switch ON.
Your admin application also needs to be able to receive push notifications. The process is similar to the set up above. The only difference will be the interest we will be subscribing to in AppDelegate
which will be orders.
Push Notifications will be published using our backend server API which is written in Node.js. For this we will use the Node.js SDK. cd
to the backend project directory and run the following command:
$ npm install @pusher/push-notifications-server --save
Next, open the index.js
file and import the @pusher/push-notifications-server
package:
1const PushNotifications = require('@pusher/push-notifications-server'); 2 3 let pushNotifications = new PushNotifications({ 4 instanceId: 'PUSHER_PUSH_NOTIF_INSTANCE_ID', 5 secretKey: 'PUSHER_PUSH_NOTIF_SECRET_KEY' 6 });
Next, we want to add a helper function that returns a notification message based on the order status. In the index.js
add the following:
1function getStatusNotificationForOrder(order) { 2 let pizza = order['pizza'] 3 switch (order['status']) { 4 case "Pending": 5 return false; 6 case "Accepted": 7 return `⏳ Your "${pizza['name']}" is being processed.` 8 case "Dispatched": 9 return `😋🍕 Your "${order['pizza']['name']}" is on it’s way` 10 case "Delivered": 11 return `🍕 Your "${pizza['name']}" has been delivered. Bon Appetit.` 12 default: 13 return false; 14 } 15 }
Next, in the PUT /orders/:id
route, add the following code before the return statement:
1let alertMessage = getStatusNotificationForOrder(order) 2 3 if (alertMessage !== false) { 4 pushNotifications.publish([`orders_${user_id}`], { 5 apns: { 6 aps: { 7 alert: { 8 title: "Order Information", 9 body: alertMessage, 10 }, 11 sound: 'default' 12 } 13 } 14 }) 15 .then(response => console.log('Just published:', response.publishId)) 16 .catch(error => console.log('Error:', error)); 17 }
In the code above, we send a push notification to the **orders_${user_id}**
interest (user_id
is the ID generated and passed to the backend server from the client) whenever the order status is changed. This will be a notification that will be picked up by our client application since we subscribed for that interest earlier.
Next, in the POST /orders
route, add the following code before the return statement:
1pushNotifications.publish(['orders'], { 2 apns: { 3 aps: { 4 alert: { 5 title: "⏳ New Order Arrived", 6 body: `An order for ${pizza['name']} has been made.`, 7 }, 8 sound: 'default' 9 } 10 } 11 }) 12 .then(response => console.log('Just published:', response.publishId)) 13 .catch(error => console.log('Error:', error));
In this case, we are sending a push notification to the orders interest. This will be sent to the admin application that is subscribed to the orders interest.
That’s all there is to adding push notifications using Pusher. Here are screen recordings of our applications in action:
In this article, we created a basic food delivery system and used that to demonstrate how to use Pusher to send push notifications in multiple applications using the same Pusher application. Hopefully you learnt how you can use Pusher to simplify the process of sending push notifications to your users.