Чтобы следовать этому руководству, необходимо базовое понимание Swift и Node.js.

Торговые площадки доставки последней мили позволяют легко заказать еду с мобильного устройства и доставить ее к двери пользователя, пока она еще горячая.

Торговые площадки, такие как Deliveroo, Postmates или Uber Eats, используют местоположение вашего устройства, чтобы предоставить вам список ресторанов, которые находятся достаточно близко к вам (и открыты), чтобы вы могли получить свою доставку как можно скорее.

Взаимодействие между клиентом, рестораном и водителем в реальном времени основано на транзакционных push-уведомлениях, позволяющих беспрепятственно перемещать заказ с кухни на стол. Клиенты хотят получать push-уведомления, чтобы предупреждать их, когда их заказ находится в пути и когда им нужно встретить водителя у двери.

Настройка push-уведомлений может сбивать с толку и отнимать много времени. Однако с API push-уведомлений Pusher процесс становится намного проще и быстрее.

В этой статье мы рассмотрим, как вы можете создавать приложения на iOS, которые имеют транзакционные push-уведомления. Для этого мы будем создавать вымышленное приложение для доставки еды.

Предпосылки

Когда у вас появятся требования, приступим.

Создание нашего приложения - планирование

Прежде чем мы начнем создавать наше приложение, нам нужно немного спланировать, как мы хотим, чтобы приложение работало.

Мы будем подавать три заявки:

  • Бэкэнд-приложение (Интернет с использованием Node.js).
  • Клиентское приложение (iOS с использованием Swift).
  • Приложение администратора (iOS с использованием Swift).

Бэкэнд-приложение

Это будет API. Для простоты мы не будем добавлять в API какую-либо аутентификацию. Мы будем вызывать API из наших приложений для iOS. API должен иметь возможность предоставлять инвентарь еды, заказы, а также управлять заказами. Мы также будем отправлять push-уведомления из внутреннего приложения.

Клиентское приложение

Это будет приложение, которое будет у заказчика. Здесь пользователь сможет заказать еду. Для простоты у нас не будет никакой аутентификации, и все будет прямо по делу. Покупатель должен иметь возможность видеть инвентарь и заказывать одну или несколько вещей из этого инвентаря. Они также должны иметь возможность видеть список своих заказов и статус каждого заказа.

Приложение администратора

Это будет приложение, которое компания, предоставляющая услугу, будет использовать для выполнения заказов. Приложение отобразит доступные заказы, и администратор сможет установить статус для каждого заказа.

Сборка серверного приложения (API)

Первое, что мы хотим создать, - это API. Мы добавим все необходимое для поддержки наших приложений iOS, а позже добавим push-уведомления.

Для начала создайте каталог проекта для API. В каталоге создайте новый файл с именем package.json. В файл вставьте следующее:

   {
      "main": "index.js",
      "scripts": {},
      "dependencies": {
        "body-parser": "^1.18.2",
        "express": "^4.16.2"
      }
    }

Затем запустите следующую команду в своем терминале:

$ npm install

Это установит все перечисленные зависимости. Затем создайте файл index.js в том же каталоге, что и файл package.json, и вставьте следующий код:

    // --------------------------------------------------------
    // Pull in the libraries
    // --------------------------------------------------------

    const app = require('express')()
    const bodyParser = require('body-parser')

    // --------------------------------------------------------
    // Helpers
    // --------------------------------------------------------

    function uuidv4() {
      return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
        return v.toString(16);
      });
    }


    // --------------------------------------------------------
    // In-memory database
    // --------------------------------------------------------

    var user_id = null

    var orders = []

    let inventory = [
        {
            id: uuidv4(),
            name: "Pizza Margherita",
            description: "Features tomatoes, sliced mozzarella, basil, and extra virgin olive oil.",
            amount: 39.99,
            image: 'pizza1'
        },
        {
            id: uuidv4(),
            name: "Bacon cheese fry",
            description: "Features tomatoes, bacon, cheese, basil and oil",
            amount: 29.99,
            image: 'pizza2'
        }
    ]


    // --------------------------------------------------------
    // Express Middlewares
    // --------------------------------------------------------

    app.use(bodyParser.json())
    app.use(bodyParser.urlencoded({extended: false}))


    // --------------------------------------------------------
    // Routes
    // --------------------------------------------------------

    app.get('/orders', (req, res) => res.json(orders))

    app.post('/orders', (req, res) => {
        let id = uuidv4()
        user_id = req.body.user_id
        let pizza = inventory.find(item => item["id"] === req.body.pizza_id)

        if (!pizza) {
            return res.json({status: false})
        }

        orders.unshift({id, user_id, pizza, status: "Pending"})
        res.json({status: true})
    })

    app.put('/orders/:id', (req, res) => {
        let order = orders.find(order => order["id"] === req.params.id)

        if ( ! order) {
            return res.json({status: false})
        }

        orders[orders.indexOf(order)]["status"] = req.body.status

        return res.json({status: true})
    })

    app.get('/inventory', (req, res) => res.json(inventory))
    app.get('/', (req, res) => res.json({status: "success"}))


    // --------------------------------------------------------
    // Serve application
    // --------------------------------------------------------

    app.listen(4000, _ => console.log('App listening on port 4000!'))

Приведенный выше код представляет собой простое приложение Express. Все говорит само за себя, и есть комментарии, которые помогут вам.

В первом маршруте, /orders, мы отображаем список заказов, доступных из хранилища данных в памяти. Во втором маршруте, POST /orders, мы просто добавляем новый заказ в список orders. В третьем маршруте, PUT /orders/:id, мы просто изменяем статус отдельного заказа из списка orders. В четвертом маршруте, GET /inventory, мы перечисляем инвентарь, доступный из списка inventory в базе данных.

На данный момент мы закончили с API, и мы вернемся к нему, когда нам нужно будет добавить код push-уведомления. Если вы хотите проверить, что API работает, выполните следующую команду на своем терминале:

$ node index.js

Это запустит новый сервер Node, прослушивающий порт 4000.

Сборка клиентского приложения

Следующее, что нам нужно сделать, это создать клиентское приложение в Xcode. Для начала запустите Xcode и создайте новый проект «Одно приложение». Назовем наш проект PizzaareaClient.

После создания проекта выйдите из Xcode и создайте новый файл с именем Podfile в корне только что созданного проекта Xcode. Вставьте в файл следующий код:

platform :ios, '11.0'
    target 'PizzareaClient' do
      use_frameworks!
      pod 'PusherSwift', '~> 5.1.1'
      pod 'Alamofire', '~> 4.6.0'
    end

В приведенном выше файле мы указали зависимости, необходимые для запуска проекта. Не забудьте изменить target на имя вашего проекта. Теперь в вашем терминале выполните следующую команду, чтобы установить зависимости:

$ pod install

После завершения установки откройте файл рабочей области Xcode, созданный Cocoapods. Это должно перезапустить Xcode.

После перезапуска Xcode откройте файл Main.storyboard. В нем мы создадим раскадровку для нашего клиентского приложения. Ниже приведен скриншот того, как мы разработали нашу раскадровку:

Первая сцена - это контроллер представления навигации, который имеет контроллер представления таблицы в качестве корневого контроллера. Контроллер навигации - это начальный контроллер, который загружается при запуске приложения.

Создание сцены со списком пиццы

Вторая сцена - это контроллер представления, который перечисляет имеющийся у нас инвентарь.

Создайте новый файл в Xcode с именем PizzaTableListViewController.swift, сделайте его настраиваемым классом для второй сцены и вставьте следующий код:

import UIKit
    import Alamofire
    class PizzaListTableViewController: UITableViewController {
        var pizzas: [Pizza] = []
        override func viewDidLoad() {
            super.viewDidLoad()
            navigationItem.title = "Select Pizza"
            fetchInventory { pizzas in
                guard pizzas != nil else { return }            
                self.pizzas = pizzas!
                self.tableView.reloadData()
            }
        }
        private func fetchInventory(completion: @escaping ([Pizza]?) -> Void) {
            Alamofire.request("http://127.0.0.1:4000/inventory", method: .get)
                .validate()
                .responseJSON { response in
                    guard response.result.isSuccess else { return completion(nil) }
                    guard let rawInventory = response.result.value as? [[String: Any]?] else { return completion(nil) }
                    let inventory = rawInventory.flatMap { pizzaDict -> Pizza? in
                        var data = pizzaDict!
                        data["image"] = UIImage(named: pizzaDict!["image"] as! String)
                        return Pizza(data: data)
                    }
                    completion(inventory)
                }
        }
        @IBAction func ordersButtonPressed(_ sender: Any) {
            performSegue(withIdentifier: "orders", sender: nil)
        }
        override func numberOfSections(in tableView: UITableView) -> Int {
            return 1
        }
        override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return pizzas.count
        }
        override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "Pizza", for: indexPath) as! PizzaTableViewCell
            cell.name.text = pizzas[indexPath.row].name
            cell.imageView?.image = pizzas[indexPath.row].image
            cell.amount.text = "$\(pizzas[indexPath.row].amount)"
            cell.miscellaneousText.text = pizzas[indexPath.row].description
            return cell
        }
        override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
            return 100.0
        }
        override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            performSegue(withIdentifier: "pizza", sender: self.pizzas[indexPath.row] as Pizza)
        }
        override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
            if segue.identifier == "pizza" {
                guard let vc = segue.destination as? PizzaViewController else { return }
                vc.pizza = sender as? Pizza
            }
        }    
    }

В методе viewDidLoad мы вызываем метод fetchInventory, который использует Alamofire для получения инвентаря из нашего внутреннего API. Затем мы сохраняем ответ в свойстве orders контроллера.

ordersButtonPressed связан с кнопкой Orders на сцене. Это просто представляет сцену со списком заказов с использованием именованного перехода orders.

tableView* методы реализуют методы, доступные для UITableViewDelegate протокола, и должны быть вам знакомы.

Последний метод prepare просто отправляет pizza контроллеру представления при навигации. Но это pizza отправляется только в том случае, если загружаемый контроллер представления - это PizzaViewController.

Прежде чем мы создадим третью сцену, создайте класс PizzaTableViewCell.swift и вставьте следующее:

import UIKit

    class PizzaTableViewCell: UITableViewCell {

        @IBOutlet weak var pizzaImageView: UIImageView!
        @IBOutlet weak var name: UILabel!
        @IBOutlet weak var miscellaneousText: UILabel!
        @IBOutlet weak var amount: UILabel!

        override func awakeFromNib() {
            super.awakeFromNib()
        }
    }

⚠️ Убедитесь, что пользовательский класс ячеек во второй сцене - PizzaTableViewCell, а повторно используемый идентификатор - Pizza.

Создание сцены вида пиццы

Третья сцена в нашей раскадровке - это сцена с видом на пиццу. Здесь можно просмотреть выбранный инвентарь.

Создайте файл PizzaViewController.swift, сделайте его настраиваемым классом для сцены выше и вставьте следующий код:

import UIKit
    import Alamofire
    class PizzaViewController: UIViewController {
        var pizza: Pizza?
        @IBOutlet weak var amount: UILabel!
        @IBOutlet weak var pizzaDescription: UILabel!
        @IBOutlet weak var pizzaImageView: UIImageView!
        override func viewDidLoad() {
            super.viewDidLoad()
            navigationItem.title = pizza!.name
            pizzaImageView.image = pizza!.image
            pizzaDescription.text = pizza!.description
            amount.text = "$\(String(describing: pizza!.amount))"
        }
        @IBAction func buyButtonPressed(_ sender: Any) {
            let parameters = [
                "pizza_id": pizza!.id,
                "user_id": AppMisc.USER_ID
            ]
            Alamofire.request("http://127.0.0.1:4000/orders", method: .post, parameters: parameters)
                .validate()
                .responseJSON { response in
                    guard response.result.isSuccess else { return self.alertError() }
                    guard let status = response.result.value as? [String: Bool],
                          let successful = status["status"] else { return self.alertError() }
                    successful ? self.alertSuccess() : self.alertError()
                }
        }
        private func alertError() {
            return self.alert(
                title: "Purchase unsuccessful!",
                message: "Unable to complete purchase please try again later."
            )
        }
        private func alertSuccess() {
            return self.alert(
                title: "Purchase Successful",
                message: "You have ordered successfully, your order will be confirmed soon."
            )
        }
        private func alert(title: String, message: String) {
            let alertCtrl = UIAlertController(title: title, message: message, preferredStyle: .alert)
            alertCtrl.addAction(UIAlertAction(title: "Okay", style: .cancel) { action in
                self.navigationController?.popViewController(animated: true)
            })
            present(alertCtrl, animated: true, completion: nil)
        }
    }

В приведенном выше коде у нас есть несколько @IBOutlet и один @IBAction. Вам нужно связать выходы и действия с контроллером из раскадровки.

В viewDidLoad мы устанавливаем выходы так, чтобы они отображали правильные значения, используя pizza, отправленный из предыдущего контроллера представления. Метод buyButtonPressed использует Alamofire для размещения заказа путем отправки запроса в API. Остальные методы обрабатывают отображение ошибки или успешного ответа от API.

Создание сцены списка заказов

Следующая сцена - сцена со списком заказов. В этой сцене перечислены все заказы, чтобы пользователь мог видеть их и их статус:

Создайте файл OrderTableViewController.swift, сделайте его настраиваемым классом для сцены выше и вставьте следующий код:

import UIKit
    import Alamofire
    class OrdersTableViewController: UITableViewController {
        var orders: [Order] = []
        override func viewDidLoad() {
            super.viewDidLoad()
            navigationItem.title = "Orders"
            fetchOrders { orders in
                self.orders = orders!
                self.tableView.reloadData()
            }
        }
        private func fetchOrders(completion: @escaping([Order]?) -> Void) {
            Alamofire.request("http://127.0.0.1:4000/orders").validate().responseJSON { response in
                guard response.result.isSuccess else { return completion(nil) }
                guard let rawOrders = response.result.value as? [[String: Any]?] else { return completion(nil) }
                let orders = rawOrders.flatMap { ordersDict -> Order? in
                    guard let orderId = ordersDict!["id"] as? String,
                          let orderStatus = ordersDict!["status"] as? String,
                          var pizza = ordersDict!["pizza"] as? [String: Any] else { return nil }
                    pizza["image"] = UIImage(named: pizza["image"] as! String)
                    return Order(
                        id: orderId,
                        pizza: Pizza(data: pizza),
                        status: OrderStatus(rawValue: orderStatus)!
                    )
                }
                completion(orders)
            }
        }
        @IBAction func closeButtonPressed(_ sender: Any) {
            dismiss(animated: true, completion: nil)
        }
        override func numberOfSections(in tableView: UITableView) -> Int {
            return 1
        }
        override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return orders.count
        }
        override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "order", for: indexPath)
            let order = orders[indexPath.row]
            cell.textLabel?.text = order.pizza.name
            cell.imageView?.image = order.pizza.image
            cell.detailTextLabel?.text = "$\(order.pizza.amount) - \(order.status.rawValue)"
            return cell
        }
        override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
            return 100.0
        }
        override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            performSegue(withIdentifier: "order", sender: orders[indexPath.row] as Order)
        }
        override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
            if segue.identifier == "order" {
                guard let vc = segue.destination as? OrderViewController else { return }
                vc.order = sender as? Order
            }
        }
    }

Приведенный выше код аналогичен коду в PizzaTableViewController выше. Однако вместо получения инвентаря он извлекает orders. Вместо того, чтобы передавать pizza в последний метод, он передает order следующему контроллеру. Контроллер также имеет closeButtonPressed метод, который просто закрывает контроллер и возвращается к сцене со списком инвентаря.

Создание сцены состояния заказа

Следующая сцена - это сцена Ордена. В этой сцене мы можем видеть статус заказа:

⚠️ Сцена выше невидима прямо над меткой состояния. Вам нужно использовать это представление для создания @IBOutlet контроллера.

Создайте файл OrderViewController.swift, сделайте его настраиваемым классом для сцены выше и вставьте следующий код:

import UIKit
    class OrderViewController: UIViewController {
        var order: Order?
        @IBOutlet weak var status: UILabel!
        @IBOutlet weak var activityView: ActivityIndicator!
        override func viewDidLoad() {
            super.viewDidLoad()
            navigationItem.title = order?.pizza.name
            activityView.startLoading()
            switch order!.status {
            case .pending:
                status.text = "Processing Order"
            case .accepted:
                status.text = "Preparing Order"
            case .dispatched:
                status.text = "Order is on its way!"
            case .delivered:
                status.text = "Order delivered"
                activityView.strokeColor = UIColor.green
                activityView.completeLoading(success: true)
            }
        }
    }

В приведенном выше коде мы выполняем всю работу в нашем viewDidLoad методе. Здесь у нас есть класс ActivityIndicator, который мы создадим следующим, на который будет ссылаться как @IBOutlet.

Создание других частей приложения

Мы используем стороннюю библиотеку под названием [ActivityIndicator](https://github.com/abdulKarim002/activityIndicator), но, поскольку пакет недоступен через Cocoapods, мы решили создать его самостоятельно и импортировать.

Создайте новый файл в Xcode с именем ActivityIndicator и вставьте в него код из репо здесь.

Затем создайте новый файл Order.swift и вставьте следующий код:

import Foundation
    struct Order {
        let id: String
        let pizza: Pizza
        var status: OrderStatus
    }
    enum OrderStatus: String {
        case pending = "Pending"
        case accepted = "Accepted"
        case dispatched = "Dispatched"
        case delivered = "Delivered"
    }

Наконец, создайте Pizza.swift и вставьте следующий код:

import UIKit
    struct Pizza {
        let id: String
        let name: String
        let description: String
        let amount: Float
        let image: UIImage
        init(data: [String: Any]) {
            self.id = data["id"] as! String
            self.name = data["name"] as! String
            self.amount = data["amount"] as! Float
            self.description = data["description"] as! String
            self.image = data["image"] as! UIImage
        }
    }

Это все для клиентского приложения. И последнее, что нам нужно сделать, это изменить файл info.plist. Нам нужно добавить запись в файл plist, чтобы разрешить подключение к нашему локальному серверу:

Перейдем к админке.

Сборка административного приложения

Запустите новый экземпляр Xcode и создайте новый проект «Одно приложение». Назовем наш проект PizzaareaAdmin.

После создания проекта выйдите из Xcode и создайте новый файл с именем Podfile в корне только что созданного проекта Xcode. Вставьте в файл следующий код:

platform :ios, '11.0'
    target 'PizzareaAdmin' do
      use_frameworks!
      pod 'PusherSwift', '~> 5.1.1'
      pod 'Alamofire', '~> 4.6.0'
    end

В приведенном выше файле мы указали зависимости, необходимые для запуска проекта. Не забудьте изменить **target** на название вашего проекта.

Теперь в вашем терминале выполните следующую команду, чтобы установить зависимости:

$ pod install

После завершения установки откройте файл рабочей области Xcode, созданный Cocoapods. Это должно перезапустить Xcode.

После перезапуска Xcode откройте файл Main.storyboard. Там мы создадим раскадровку для нашего клиентского приложения. Ниже приведен скриншот того, как мы разработали нашу раскадровку:

Выше у нас есть контроллер представления навигации, который является начальным контроллером представления.

Создание сцены списка заказов

Сцена со списком заказов предназначена для отображения списка заказов клиентов. Оттуда мы можем изменить статус каждого заказа, когда захотим.

Создайте новый файл в Xcode с именем OrdersListViewController.swift, сделайте его настраиваемым классом для второй сцены и вставьте следующий код:

import UIKit
    import Alamofire
    class OrdersTableViewController: UITableViewController {
        var orders: [Order] = []
        override func viewDidLoad() {
            super.viewDidLoad()
            navigationItem.title = "Client Orders"
            fetchOrders { orders in
                self.orders = orders!
                self.tableView.reloadData()
            }
        }
        private func fetchOrders(completion: @escaping([Order]?) -> Void) {
            Alamofire.request("http://127.0.0.1:4000/orders").validate().responseJSON { response in
                guard response.result.isSuccess else { return completion(nil) }
                guard let rawOrders = response.result.value as? [[String: Any]?] else { return completion(nil) }
                let orders = rawOrders.flatMap { ordersDict -> Order? in
                    guard let orderId = ordersDict!["id"] as? String,
                          let orderStatus = ordersDict!["status"] as? String,
                          var pizza = ordersDict!["pizza"] as? [String: Any] else { return nil }
                    pizza["image"] = UIImage(named: pizza["image"] as! String)
                    return Order(
                        id: orderId,
                        pizza: Pizza(data: pizza),
                        status: OrderStatus(rawValue: orderStatus)!
                    )
                }
                completion(orders)
            }
        }
        override func numberOfSections(in tableView: UITableView) -> Int {
            return 1
        }
        override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return orders.count
        }
        override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "order", for: indexPath)
            let order = orders[indexPath.row]
            cell.textLabel?.text = order.pizza.name
            cell.imageView?.image = order.pizza.image
            cell.detailTextLabel?.text = "$\(order.pizza.amount) - \(order.status.rawValue)"
            return cell
        }
        override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
            return 100.0
        }
        override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            let order: Order = orders[indexPath.row]
            let alertCtrl = UIAlertController(
                title: "Change Status",
                message: "Change the status of the order based on the progress made.",
                preferredStyle: .actionSheet
            )
            alertCtrl.addAction(createActionForStatus(.pending, order: order))
            alertCtrl.addAction(createActionForStatus(.accepted, order: order))
            alertCtrl.addAction(createActionForStatus(.dispatched, order: order))
            alertCtrl.addAction(createActionForStatus(.delivered, order: order))
            alertCtrl.addAction(createActionForStatus(nil, order: nil))
            present(alertCtrl, animated: true, completion: nil)
        }
        private func createActionForStatus(_ status: OrderStatus?, order: Order?) -> UIAlertAction {
            let alertTitle = status == nil ? "Cancel" : status?.rawValue
            let alertStyle: UIAlertActionStyle = status == nil ? .cancel : .default
            let action = UIAlertAction(title: alertTitle, style: alertStyle) { action in
                if status != nil {
                    self.setStatus(status!, order: order!)
                }
            }
            if status != nil {
                action.isEnabled = status?.rawValue != order?.status.rawValue
            }
            return action
        }
        private func setStatus(_ status: OrderStatus, order: Order) {
            updateOrderStatus(status, order: order) { successful in
                guard successful else { return }
                guard let index = self.orders.index(where: {$0.id == order.id}) else { return }
                self.orders[index].status = status
                self.tableView.reloadData()
            }
        }
        private func updateOrderStatus(_ status: OrderStatus, order: Order, completion: @escaping(Bool) -> Void) {
            let url = "http://127.0.0.1:4000/orders/" + order.id
            let params = ["status": status.rawValue]
            Alamofire.request(url, method: .put, parameters: params).validate().responseJSON { response in
                guard response.result.isSuccess else { return completion(false) }
                guard let data = response.result.value as? [String: Bool] else { return completion(false) }
                completion(data["status"]!)
            }
        }
    }

Приведенный выше код похож на код в PizzaListTableViewController в клиентском приложении, поэтому проверьте его, если вам нужны дополнительные объяснения.

Существует createActionForStatus, который является помощником для создания и настройки объекта UIAlertAction. Существует setStatus метод, который просто пытается установить статус заказа. А еще есть метод updateOrderStatus, который отправляет запрос на обновление, используя Alamofire, в API.

Затем создайте классы Order.swift и Pizza.swift, как мы делали это раньше в клиентском приложении:

// Order.swift
    import Foundation
    struct Order {
        let id: String
        let pizza: Pizza
        var status: OrderStatus
    }
    enum OrderStatus: String {
        case pending = "Pending"
        case accepted = "Accepted"
        case dispatched = "Dispatched"
        case delivered = "Delivered"
    }

    // Pizza.swift
    import UIKit
    struct Pizza {
        let id: String
        let name: String
        let description: String
        let amount: Float
        let image: UIImage
        init(data: [String: Any]) {
            self.id = data["id"] as! String
            self.name = data["name"] as! String
            self.amount = data["amount"] as! Float
            self.description = data["description"] as! String
            self.image = data["image"] as! UIImage
        }
    }

Это все для административного приложения. И последнее, что нам нужно сделать, это изменить файл info.plist, как мы это делали в клиентском приложении.

Добавление push-уведомлений в наше приложение для iOS по доставке еды

На данный момент приложение работает, как и ожидалось, из коробки. Теперь нам нужно добавить push-уведомления в приложение, чтобы сделать его более привлекательным, даже когда пользователь в данный момент не использует приложение.

⚠️ Вы должны быть зарегистрированы в программе Apple Developer, чтобы иметь возможность использовать функцию push-уведомлений. Кроме того, push-уведомления не работают в симуляторах, поэтому для тестирования вам потребуется реальное устройство iOS.

API push-уведомлений Pusher обеспечивает первоклассную поддержку нативных приложений iOS. Ваши экземпляры приложений iOS подписываются на Я ** интересы **, а затем ваши серверы отправляют этим интересам push-уведомления. Каждый экземпляр приложения, подписанный на этот интерес, получит уведомление, даже если приложение не открыто на устройстве в это время.

В этом разделе описывается, как настроить приложение iOS для получения транзакционных push-уведомлений о ваших заказах на доставку еды через Pusher.

Настроить APN

Pusher полагается на службу Apple Push Notification (APN) для доставки push-уведомлений пользователям приложений iOS от вашего имени. Когда мы доставляем push-уведомления, мы используем ваш ключ APN. На этой странице вы узнаете, как получить ключ APN и как предоставить его Pusher.

Перейдите на панель управления Apple Developer, нажав здесь, а затем создайте новый ключ, как показано ниже:

Когда вы создали ключ, загрузите его. Держите его в безопасности, так как он нам понадобится в следующем разделе.

⚠️ Вы должны хранить сгенерированный ключ в безопасности, так как вы не сможете вернуть его, если потеряете.

Создание вашего приложения Pusher

Следующее, что вам нужно сделать, это создать новое приложение Pusher Push Notification из панели управления Pusher.

Когда вы создали приложение, вам должен быть представлен мастер быстрого запуска, который поможет вам настроить приложение.

Чтобы настроить push-уведомления, вам необходимо получить ключ APN от Apple. Это тот же ключ, что и тот, который мы скачали в предыдущем разделе. Получив ключ, загрузите его в мастер быстрого запуска.

Введите свой Apple Team ID. Получить Team ID можно здесь. Нажмите Продолжить, чтобы перейти к следующему шагу.

Обновление клиентского приложения для поддержки push-уведомлений

В своем клиентском приложении откройте Podfile и добавьте следующий модуль в список зависимостей:

pod 'PushNotifications'

Теперь выполните команду pod install, как вы делали раньше, чтобы загрузить пакет уведомлений. Когда установка будет завершена, создайте новый класс AppMisc.swift и вставьте туда следующее:

class AppMisc {
      static let USER_ID = NSUUID().uuidString.replacingOccurrences(of: "-", with: "_")
    }

В небольшом классе выше мы генерируем идентификатор пользователя для сеанса. В реальном приложении у вас обычно будет фактический идентификатор пользователя после аутентификации.

Затем откройте класс AppDelegate и импортируйте пакет PushNotifications:

import PushNotifications

Теперь, как часть класса AppDelegate, добавьте следующее:

let pushNotifications = PushNotifications.shared
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
      self.pushNotifications.start(instanceId: "PUSHER_NOTIF_INSTANCE_ID")
      self.pushNotifications.registerForRemoteNotifications()
      return true
    }
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
      self.pushNotifications.registerDeviceToken(deviceToken) {
        try? self.pushNotifications.subscribe(interest: "orders_" + AppMisc.USER_ID)
      }
    }

💡 Замените PUSHER_PUSH_NOTIF_INSTANCE_ID ключом, предоставленным вам приложением Pusher.

В приведенном выше коде мы настраиваем push-уведомления в методе application(didFinishLaunchingWithOptions:), а затем подписываемся в методе application(didRegisterForRemoteNotificationsWithDeviceToken:).

Далее нам нужно включить push-уведомления для приложения. В навигаторе проекта выберите свой проект и перейдите на вкладку Возможности. Включить push-уведомления, включив переключатель.

Обновление вашего административного приложения для поддержки push-уведомлений

Ваше административное приложение также должно иметь возможность получать push-уведомления. Процесс аналогичен описанному выше. Единственная разница будет заключаться в процентах, на которые мы будем подписываться в AppDelegate, которые будут заказами.

Обновление вашего API для отправки push-уведомлений

Push-уведомления будут публиковаться с использованием API нашего внутреннего сервера, написанного на Node.js. Для этого воспользуемся Node.js SDK. cd в каталог внутреннего проекта и выполните следующую команду:

$ npm install pusher-push-notifications-node --save

Затем откройте файл index.js и импортируйте пакет pusher-push-notifications-node:

const PushNotifications = require('pusher-push-notifications-node');
    let pushNotifications = new PushNotifications({
        instanceId: 'PUSHER_PUSH_NOTIF_INSTANCE_ID',
        secretKey: 'PUSHER_PUSH_NOTIF_SECRET_KEY'
    });

Затем мы хотим добавить вспомогательную функцию, которая возвращает уведомление в зависимости от статуса заказа. В index.js добавить следующее:

function getStatusNotificationForOrder(order) {
        let pizza = order['pizza']
        switch (order['status']) {
            case "Pending":
                return false;
            case "Accepted":
                return `⏳ Your "${pizza['name']}" is being processed.`
            case "Dispatched":
                return `😋🍕 Your "${order['pizza']['name']}" is on it’s way`
            case "Delivered":
                return `🍕 Your "${pizza['name']}" has been delivered. Bon Appetit.`
            default:
                return false;
        }
    }

Затем в маршруте PUT /orders/:id добавьте следующий код перед оператором return:

let alertMessage = getStatusNotificationForOrder(order)
    if (alertMessage !== false) {
       pushNotifications.publish([`orders_${user_id}`], {
            apns: { 
                aps: {
                    alert: {
                        title: "Order Information",
                        body: alertMessage,
                    }, 
                    sound: 'default'
                } 
            }
        })
        .then(response => console.log('Just published:', response.publishId))
        .catch(error => console.log('Error:', error));
    }

В приведенном выше коде мы отправляем push-уведомление **orders_${user_id}** интересу (user_id - это идентификатор, сгенерированный и переданный на внутренний сервер от клиента) всякий раз, когда изменяется статус заказа. Это будет уведомление, которое будет подхвачено нашим клиентским приложением, поскольку мы подписались на этот интерес ранее.

Затем в маршруте POST /orders добавьте следующий код перед оператором return:

pushNotifications.publish(['orders'], {
        apns: {
            aps: {
                alert: {
                    title: "⏳ New Order Arrived",
                    body: `An order for ${pizza['name']} has been made.`,
                },
                sound: 'default'
            }
        }
    })
    .then(response => console.log('Just published:', response.publishId))
    .catch(error => console.log('Error:', error));

В этом случае мы отправляем push-уведомление заинтересованным заказам. Он будет отправлен в приложение администратора, которое подписано на интерес заказов.

Вот и все, что нужно для добавления push-уведомлений с помощью Pusher. Вот записи экрана наших приложений в действии:

Заключение

В этой статье мы создали базовую систему доставки еды и использовали ее, чтобы продемонстрировать, как использовать Pusher для отправки push-уведомлений в нескольких приложениях с использованием одного и того же приложения Pusher. Надеюсь, вы узнали, как можно использовать Pusher, чтобы упростить процесс отправки push-уведомлений вашим пользователям.

Этот пост впервые был опубликован в Pusher.