How to send push notifications to a food delivery Swift app

food-delivery-notifications-swift-header.png

Build a last mile food delivery Swift app and send transactional push notifications between the restaurant, driver, and customer as the order progresses.

Introduction

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.

Prerequisites

Once you have the requirements, let’s start.

Building Our Application – Planning

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).

The Backend Application

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.

The Client 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.

The Admin Application

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.

Building the Backend Application (API)

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.

Building the Client Application

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.

Creating the Pizza List Scene

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 is Pizza.

Creating the Pizza View Scene

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.

Creating the Orders List Scene

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.

Creating the Order Status 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.

Creating Other Parts of the Application

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.

Building 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.

Creating the Orders List Scene

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.

Adding Push Notifications to Our Food Delivery iOS App

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.

Configure APNs

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.

Creating your Pusher Application

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.

Updating Your Client Application to Support Push Notifications

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.

Updating Your Admin Application to Support Push Notifications

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.

Updating Your API to Send Push Notifications

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:

Conclusion

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.