【iOS】#9 訂飲料APP|Part.3 修改、刪除飲料訂單 — PATCH、DELETE

前情提要 — 可不可飲料訂購APP為系列文章,初步規劃分為四篇文章介紹以下四大頁面及功能:

・註冊登入頁面:使用者註冊登入及訪客登入|Firebase 身份驗證
・Menu菜單頁面:串接 Airtable API 呈現飲料菜單|GET
・飲料訂購頁面:新增飲料訂單|POST
・訂購清單頁面:編輯、刪除訂單|PATCH、DELETE

本篇為訂購清單頁面

🌟 本篇重點功能

  1. 在飲料訂購頁面呈現訂單內容
  2. 修改飲料訂單,使用 RESTful API — PATCH
  3. 刪除飲料訂單,使用 RESTful API — DELETE
成果展示

功能介紹

① 在飲料訂購頁面呈現訂單內容

1. 抓取資料,呈現訂購清單

參考Part.1文章 — GET方法,抓取 Airtable 訂單資料

OrderViewController — fetchOrderList

class OrderViewController: UIViewController {

var orders = [CreateOrderDrinkResponseRecord]()

MenuViewController.shared.fetchOrderList { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let orderListResponse):

// 將抓到的資料存入變數orders
self.orders = orderListResponse.records

// 在主執行緒更新畫面
DispatchQueue.main.async {
self.updateUI()
}
case .failure(let error):
print(error)
}
}

func updateUI() {

// reloadData
orderTableView.reloadData()

// 更新飲料杯數及總金額
calculateQuantityAndPrice()
checkoutNumberOfCups.text = "共計 \(numberOfCups)杯"
checkoutPrice.text = "$\(totalPrice)"

// 更新badge數字
if orders.count > 0 {
tabBarItem.badgeValue = "\(numberOfCups)"
} else {
tabBarItem.badgeValue = nil
}
}


}

2. 點擊訂單 cell 時,將訂單資料傳至飲料訂購頁面

OrderViewController — tableView — didSelectRowAt

extension OrderViewController: UITableViewDelegate, UITableViewDataSource {

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

let order = orders[indexPath.row]

let drinkDetailViewController = DrinkDetailViewController()

// 帶入訂單資料
drinkDetailViewController.accessOrderData(data: order.fields, id: order.id)

// 呈現該筆訂單的訂購頁面
present(drinkDetailViewController, animated: true)
}

}

DrinkDetailViewController — accessOrderData

存取訂單資料

class DrinkDetailViewController: UIViewController {

var drink: DrinkRecord!
private var orderData: CreateOrderFields?
private var orderId: String?

// 存取訂單資料
func accessOrderData(data: CreateOrderFields, id: String) {

// 在Menu頁存取的菜單飲料中,比對與訂單相同的飲料名(作為呈現的飲料數據)
self.drink = MenuViewController.shared.drinks.first(where: { $0.fields.name == data.drinkName })

// 存取訂單數據
self.orderData = data

// 存取訂單id
self.orderId = id
}

}

3. 在飲料訂購頁面呈現訂單內容

DrinkDetailViewController — updateOptionsOfOrder

呈現訂單內容(選項勾選、價格、杯數等等)

class DrinkDetailViewController: UIViewController {

// 呈現訂單內容
func updateOptionsOfOrder() {

// 確保orderData有資料才執行(表示從訂購清單頁面過來)
guard let orderData else { return }

// 呈現訂單內容(選項勾選、價格、杯數等等)
handleRadioButtonSelect(checkoutName: orderData.size, in: sizeView, didSelectedOption: &selectedSize)
handleRadioButtonSelect(checkoutName: orderData.ice, in: iceView, didSelectedOption: &selectedIce)
handleRadioButtonSelect(checkoutName: orderData.sugar, in: sugarView, didSelectedOption: &selectedSugar)
handleCheckbox(totalAddOns: orderData.addOns ?? [])
updateCheckoutOptions()
drinkPrice = orderData.price / orderData.numberOfCups
checkoutPrice.text = "$\(orderData.price)"
numberOfCups = orderData.numberOfCups
numberOfCupsLabel.text = "\(numberOfCups)"

// 變更按鈕功能及文字(加入購物車->更新購物車)
addToCartButton.removeTarget(self, action: #selector(addToCart), for: .touchUpInside)
addToCartButton.addTarget(self, action: #selector(updateOrder), for: .touchUpInside)
addToCartButton.setTitle("更新購物車", for: .normal)
}

}
在飲料訂購頁面呈現訂單內容

② 修改飲料訂單,使用 RESTful API — PATCH

1. 根據修改訂單的 Json 資料定義 Struct

查看 API 文件 — PATCH

定義 UpdateOrderDrink

參考 PATCH Request 的 Json 格式,根據 APP 所需 (修改訂單需要id、可以被編輯修改的選項等),定義自己的 UpdateOrderDrink

import Foundation

// MARK: - PatchOrderDrink
struct UpdateOrderDrink: Encodable {
let records: [UpdateOrderRecord]
}

// MARK: - Record
struct UpdateOrderRecord: Encodable {
let id: String
let fields: UpdateOrderFields
}

// MARK: - Fields
struct UpdateOrderFields: Encodable {
let size: String
let ice: String
let sugar: String
let addOns: [String]
let price: Int
let numberOfCups: Int
}

2. 建立 updateOrder function

MenuViewController — updateOrder

與新增訂單(postOrder)的程式基本相同,差別只在於 httpMethod 改為 PATCH

class MenuViewController: UIViewController {

// MARK: - PATCH Order
func updateOrder(orderData: UpdateOrderDrink, completion: @escaping (Result<String, Error>) -> Void) {
let orderURL = baseURL.appendingPathComponent("OrderDrink")
guard let components = URLComponents(url: orderURL, resolvingAgainstBaseURL: true) else { return }
guard let orderURL = components.url else { return }

var request = URLRequest(url: orderURL)
request.httpMethod = "PATCH"
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")

do {
let encoder = JSONEncoder()
request.httpBody = try encoder.encode(orderData)
URLSession.shared.dataTask(with: request) { data, response, resError in
if let data = data,
let content = String(data: data, encoding: .utf8) {
completion(.success(content))
} else if let resError = resError {
completion(.failure(resError))
}
}.resume()
} catch {
completion(.failure(error))
}
}

}

3. 點擊更新購物車,修改 Airtable 上該筆訂單資料

DrinkDetailViewController — updateOrder

與加入購物車(addToCart)的程式相似,差別在於設置的訂單內容不同,以及打 API 的方法改為 PATCH

class DrinkDetailViewController: UIViewController { 

@objc func updateOrder() {

// 設置訂單更新內容
let updateOrderFields = UpdateOrderFields(
size: selectedSize?.shortName ?? "",
ice: selectedIce?.shortName ?? "",
sugar: selectedSugar?.shortName ?? "",
addOns: totalAddOns, price: drinkPrice * numberOfCups,
numberOfCups: numberOfCups)
let updateOrderRecord = UpdateOrderRecord(id: orderId!, fields: updateOrderFields)
let updateOrderDrink = UpdateOrderDrink(records: [updateOrderRecord])

// PATCH
MenuViewController.shared.updateOrder(orderData: updateOrderDrink) { result in
switch result {
case .success(let updateOrderResponse):
print(updateOrderResponse)

// 若成功修改訂單,發送訂單更新通知
NotificationCenter.default.post(name: .orderUpdateNotification, object: nil)

case .failure(let error):
print(error)
}
}

// 關閉當前視圖
self.dismiss(animated: true)
}

}

( NotificationCenter 傳送通知可參考Part.2文章 )

透過「更新購物車」修改訂單(PATCH)

4. 直接在訂單 cell 增減飲料杯數,修改訂單資料

OrderCell — 實現 delegate 模式

import UIKit

// 定義protocol
protocol OrderCellDelegate: AnyObject {
func updateQuantityAndPrice(sender: UIButton, numberOfCups: Int, orderPrice: Int)
}

class OrderCell: UITableViewCell {

let minusButton = UIButton()
let plusButton = UIButton()

// 建立delegate屬性
weak var delegate: OrderCellDelegate?

// 點擊減號
@objc func minusCup() {
if numberOfCups > 1 {
numberOfCups -= 1
}
let orderPrice = orderPrice * numberOfCups

// delegate處理更新訂單功能
delegate?.updateQuantityAndPrice(sender: self.minusButton, numberOfCups: numberOfCups, orderPrice: orderPrice)
}

// 點擊加號
@objc func plusCup() {
numberOfCups += 1
let orderPrice = orderPrice * numberOfCups

// delegate處理更新訂單功能
delegate?.updateQuantityAndPrice(sender: self.plusButton, numberOfCups: numberOfCups, orderPrice: orderPrice)
}

}

OrderViewController— tableView — cellForRowAt

設置 OrderCell 的 delegate 為 OrderViewController

設置加減按鈕的 tag 屬性為該筆訂單的 index 值

extension OrderViewController: UITableViewDelegate, UITableViewDataSource {

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = orderTableView.dequeueReusableCell(withIdentifier: "orderCell", for: indexPath) as! OrderCell

// cell找delegate幫忙
cell.delegate = self

// 設置加減按鈕的tag為indexPath.row
cell.minusButton.tag = indexPath.row
cell.plusButton.tag = indexPath.row

return cell
}

}

OrderViewController — updateQuantityAndPrice

與更新購物車(updateOrder)的程式相似,可參考上方第三點說明

利用加減按鈕的 tag 屬性判斷要修改哪一筆訂單資料

extension OrderViewController: OrderCellDelegate {

func updateQuantityAndPrice(sender: UIButton, numberOfCups: Int, orderPrice: Int) {

// 獲取按鈕的tag屬性,即對應的indexPath.row
let rowIndex = sender.tag

// 設置訂單更新內容
let updateOrderFields = UpdateOrderFields(
size: orders[rowIndex].fields.size,
ice: orders[rowIndex].fields.ice,
sugar: orders[rowIndex].fields.sugar,
addOns: orders[rowIndex].fields.addOns ?? [], price: orderPrice,
numberOfCups: numberOfCups)

let updateOrderRecord = UpdateOrderRecord(id: orders[rowIndex].id, fields: updateOrderFields)
let updateOrderDrink = UpdateOrderDrink(records: [updateOrderRecord])

// PATCH
MenuViewController.shared.updateOrder(orderData: updateOrderDrink) { result in
switch result {
case .success(let updateOrderResponse):
print(updateOrderResponse)

// 若成功修改訂單,發送訂單更新通知
NotificationCenter.default.post(name: .orderUpdateNotification, object: nil)

case .failure(let error):
print(error)
}
}
}

}
透過「OrderCell 的加減按鈕」修改訂單(PATCH)

③ 刪除飲料訂單,使用 RESTful API — DELETE

1. 查看 API 文件 — DELETE

・在 curl 命令中,-G 選項表示將參數以 GET 請求的形式傳遞,即將參數附加到 URL 查詢字符串中,而非放在 Request Body

・— data-urlencode 用於 URL 編碼表單數據,將鍵值對(key-value)添加到 DELETE 請求中

(以下圖為例:鍵(key)為 “records[]”,值(value)為 “recam5DemP2qZIHNQ”)

2. 建立 deleteOrder function

MenuViewController — deleteOrder

在 orderURL 後添加要刪除的訂單 id,httpMethod 改為 DELETE

class MenuViewController: UIViewController {

// MARK: - DELETE Order
func deleteOrder(orderID: String, completion: @escaping(Result<String,Error>) -> Void) {
var orderURL = baseURL.appendingPathComponent("OrderDrink")

// 帶入要刪除的訂單id
orderURL = orderURL.appendingPathComponent(orderID)

guard let components = URLComponents(url: orderURL, resolvingAgainstBaseURL: true) else { return }
guard let orderURL = components.url else { return }

var request = URLRequest(url: orderURL)
request.httpMethod = "DELETE"
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")

URLSession.shared.dataTask(with: request) { data, response, resError in
if let response = response as? HTTPURLResponse,
response.statusCode == 200,
resError == nil,
let data = data,
let content = String(data: data, encoding: .utf8) {
completion(.success(content))
} else if let resError = resError {
completion(.failure(resError))
}
}.resume()
}

}

3. 刪除 Airtable 上的訂單資料

OrderViewController — tableView — editingStyle

extension OrderViewController: UITableViewDelegate, UITableViewDataSource {

func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {

if editingStyle == .delete {
let order = self.orders[indexPath.row]

// DELETE
MenuViewController.shared.deleteOrder(orderID: order.id) { result in
switch result {
case .success(let message):
print(message)

// 在主執行緒更新畫面
DispatchQueue.main.async {
self.orders.remove(at: indexPath.row)
self.orderTableView.deleteRows(at: [indexPath], with: .left)
self.updateUI()
}
case .failure(let error):
print(error)
}
}
}
}

}
刪除飲料訂單(DELETE)

--

--