Build a last mile food delivery Swift app and send transactional push notifications between the restaurant, driver, and customer as the order progresses.
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’s Push Notifications 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:
– The backend application (Web using Node.js).
– The client application (iOS using Swift).
– The admin application (iOS using Swift).
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 fulfill 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 create 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 to 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 in 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’s Push Notifications API has first-class support for native iOS applications. Your iOS app instances subscribe to I****nterests; 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 by clicking here 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 Push Notification 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.
In order to configure Push Notifications, 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. You can get the Team ID from here. Click on the 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 the 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-node --save
Next, open the index.js
file and import the pusher-push-notifications-node
package:
1const PushNotifications = require('pusher-push-notifications-node'); 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 learned how you can use Pusher to simplify the process of sending Push Notifications to your users.
If you have any question or have some feedback leave a comment below. The source code to the repository is available on GitHub.