【iOS】#8 訂飲料APP|Part.2 新增飲料訂單 — POST、客製 RadioButton & Checkbox、NotificationCenter

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

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

本篇為飲料訂購頁面

🌟 本篇重點功能

  1. 新增飲料訂單,使用 RESTful API — POST
  2. 客製單選 RadioButton & 多選 Checkbox
  3. NotificationCenter 傳送通知,更新 badge 數字提醒
成果展示

功能介紹

① 新增飲料訂單,使用 RESTful API — POST

與上一篇(GET)相同的步驟就不贅述,可前往Part.1文章參考,以下簡易說明

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

建立 Airtable 訂購清單

(欄位為訂單需要顯示的資料)

查看 API 文件 — POST

在 POST Request 的說明頁面,取得 Request data 和 Response 的 Json 資料

定義 CreateOrderDrink

import Foundation

// MARK: - CreateOrderDrink
struct CreateOrderDrink: Encodable {
let records: [CreateOrderRecord]
}

// MARK: - CreateOrderRecord
struct CreateOrderRecord: Encodable {
let fields: CreateOrderFields
}

// MARK: - CreateOrderFields
struct CreateOrderFields: Codable {
let drinkName: String
let size: String
let ice: String
let sugar: String
let addOns: [String]?
let price: Int
let orderName: String
let numberOfCups: Int
let imageUrl: URL
}


// MARK: - CreateOrderDrinkResponse
struct CreateOrderDrinkResponse: Decodable {
let records: [CreateOrderDrinkResponseRecord]
}

// MARK: - CreateOrderDrinkResponseRecord
struct CreateOrderDrinkResponseRecord: Decodable {
let id: String
let createdTime: String
let fields: CreateOrderFields
}

2. 建立 postOrder function

MenuViewController — postOrder

這邊學習 Apple 大大的寫法,建立 function 搭配 Result Type & Completion Closure

class MenuViewController: UIViewController {

// MARK: - POST Order
func postOrder(orderData: CreateOrderDrink, 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 = "POST"
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")

do {
// 發送資料時(POST),建立Encoder進行編碼
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 — addToCart

class DrinkDetailViewController: UIViewController {

@objc func addToCart() {

// 設置訂單內容
let createOrderFields = CreateOrderFields(
drinkName: drink.fields.name,
size: selectedSize?.shortName ?? "",
ice: selectedIce?.shortName ?? "",
sugar: selectedSugar?.shortName ?? "",
addOns: totalAddOns, price: drinkPrice * numberOfCups,
orderName: userName ?? "", numberOfCups: numberOfCups,
imageUrl: (drink.fields.image.first?.url)!)
let createOrderRecord = CreateOrderRecord(fields: createOrderFields)
let createOrderDrink = CreateOrderDrink(records: [createOrderRecord])

// POST
MenuViewController.shared.postOrder(orderData: createOrderDrink) { result in
switch result {
case .success(let createOrderResponse):
print(createOrderResponse)
case .failure(let error):
print(error)
}
}

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

}
新增飲料訂單(POST)

②客製單選 RadioButton & 多選 Checkbox

1. 單選按鈕 RadioButton

定義型別為 UIButton 的 RadioButton

AutoLayout 畫面

import UIKit

class RadioButton: UIButton {

let titleLable = UILabel()
let checkImageView = UIImageView()

// 選項簡稱
var shortName: String = ""

override init(frame: CGRect) {
super.init(frame: frame)

// 設置選項文字
addSubview(titleLable)
titleLable.snp.makeConstraints { make in
make.left.equalToSuperview()
make.centerY.equalToSuperview()
}
titleLable.textColor = .darkPrimary
titleLable.font = UIFont.systemFont(ofSize: 18)

// 設置選項圖像
addSubview(checkImageView)
checkImageView.snp.makeConstraints { make in
make.right.equalToSuperview()
make.centerY.equalToSuperview()
make.size.equalTo(titleLable.snp.height)
}
checkImageView.tintColor = .darkPrimary
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

}

定義選項狀態 Enum: RadioButtonStatus

  1. RadioButtonStatus:表示單選按鈕的狀態,定義兩個可能的值:.checked(已選取)和.unchecked(未選取)
  2. 定義一個計算屬性 image,根據 enum 的不同狀態返回對應的圖像
enum RadioButtonStatus {
case checked
case unchecked

var image: UIImage {
switch self {

// 未選取時返回空心圓的圖像
case .unchecked:
return UIImage(systemName: "circle")!

// 已選取時返回一個實心圓打勾的圖像
case .checked:
return UIImage(systemName: "checkmark.circle.fill")!
}
}
}

定義選項類別 Enum: TypeOfOption

TypeOfOption:表示選項的類型,定義三個可能的值:.size(尺寸)、.ice(冰塊)和.sugar(甜度)

enum TypeOfOption {
case size
case ice
case sugar
}

設置按鈕類型、狀態、功能

class RadioButton: UIButton {

// 按鈕類型
var type: TypeOfOption?

// 按鈕狀態:預設值為未選取
var status: RadioButtonStatus = .unchecked {

// 屬性觀察器(didset):當status的值被設置時,更新按鈕的外觀
didSet {
updateUI()
}
}

override init(frame: CGRect) {

// 設置按鈕圖像為其狀態返回的圖像
checkImageView.image = status.image

// 綁定功能onClick
addTarget(self, action: #selector(onClick), for: .touchUpInside)
}

func updateUI() {

// 更新按鈕圖像與其狀態一致
self.checkImageView.image = status.image
}

@objc func onClick(sender: UIButton) {

// 點擊時將其狀態變更為已選取
self.status = .checked
}

}

實現 RadioButton delegate 模式

// 定義protocol
protocol RadioButtonDelegate: AnyObject {
func RadioButtonTapped(_ sender: RadioButton)
}

class RadioButton: UIButton {

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

@objc func onClick(sender: UIButton) {

// delegate處理按鈕點擊事件
self.delegate?.RadioButtonTapped(sender as! RadioButton)
}

}

DrinkDetailViewController — RadioButtonTapped

class DrinkDetailViewController: UIViewController {

// 用於保存當前選中的按鈕
var selectedSize: RadioButton?
var selectedIce: RadioButton?
var selectedSugar: RadioButton?

}

extension DrinkDetailViewController: RadioButtonDelegate {

func RadioButtonTapped(_ sender: RadioButton) {

// 根據按鈕的類型執行相應的操作
switch sender.type {
case .size:
didSelected(type: &selectedSize)
case .ice:
didSelected(type: &selectedIce)
case .sugar:
didSelected(type: &selectedSugar)
default:
break
}

func didSelected(type selectedButton: inout RadioButton?) {

// 取消先前選中的按鈕
selectedButton?.status = .unchecked

// 選中當前按鈕
sender.status = .checked
selectedButton = sender
}
}

}
單選按鈕 RadioButton

2. 多選按鈕 Checkbox

做法和上述 RadioButton(單選按鈕)相似,以下說明 Checkbox 變化的部分

選項狀態 Enum: CheckboxStatus

返回圖像為方形

enum CheckboxStatus {
case checked
case unchecked

var image: UIImage {
switch self {
case .unchecked:
return UIImage(systemName: "square")!
case .checked:
return UIImage(systemName: "checkmark.square.fill")!
}
}
}

Checkbox

點擊時根據目前狀態調整變更的狀態

class Checkbox: UIButton {

@objc func didChecked(sender: UIButton) {
switch self.status {

// 點擊時為未選取->變更為已選取
case .unchecked:
self.status = .checked

// 點擊時為已選取->變更為未選取
case .checked:
self.status = .unchecked
}
}
}

DrinkDetailViewController — checkboxTapped

class DrinkDetailViewController: UIViewController {

// 用Array保存已選取的選項簡稱
var totalAddOns = [String]()
}

extension DrinkDetailViewController: CheckboxDelegate {

func checkboxTapped(_ sender: Checkbox) {

// 根據按鈕的狀態執行相應的操作
switch sender.status {

// 選取時將選項簡稱加入totalAddOns
case .checked:
totalAddOns.append(sender.shortName)

// 未選取時從totalAddOns移除選項簡稱
case .unchecked:
let objectToRemove = sender.shortName
if let index = totalAddOns.firstIndex(of: objectToRemove) {
totalAddOns.remove(at: index)
}
}
}

}
多選按鈕 Checkbox

③NotificationCenter 傳送通知,更新 badge 數字提醒

1. 定義通知名稱

Notification.Name

擴充 Notification.Name,定義一個 orderUpdateNotification 靜態屬性,該屬性的值是一個 Notification.Name 實例,通知名稱為 "OrderUpdateNotification",用於發送和接收通知。

extension Notification.Name {

static let orderUpdateNotification = Notification.Name("OrderUpdateNotification")

}

(將通知名稱的定義放在 Notification.Name 的擴展中,有助於更好地組織和管理代碼)

2. 發送通知

DrinkDetailViewController

在飲料訂購頁面送出 POST 請求時,發送訂單更新通知

class DrinkDetailViewController: UIViewController {

@objc func addToCart() {
// 設置訂單內容...

// POST
MenuViewController.shared.postOrder(orderData: createOrderDrink) { result in
switch result {
case .success(let createOrderResponse):
print(createOrderResponse)

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

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

}

3. 註冊觀察者監聽通知,更新 badge 數字提醒

OrderViewController

將訂購清單頁面註冊為觀察者(observer),監聽名為 .orderUpdateNotification 的通知,當接收到通知時,將調用 updateOrder 方法

class OrderViewController: UIViewController {

init() {
super.init(nibName: nil, bundle: nil)

// 設置tabBar上的badge顏色
tabBarItem.badgeColor = .secondary

// 註冊觀察者為self
NotificationCenter.default.addObserver(self, selector: #selector(updateOrder), name: .orderUpdateNotification, object: nil)
}

deinit {
// 在視圖控制器被銷毀時移除通知觀察者
NotificationCenter.default.removeObserver(self, name: .orderUpdateNotification, object: nil)
}

// 接收到通知時,調用updateOrder方法
@objc func updateOrder() {

// 執行fetchOrderList(GET)抓取新的訂購清單資料
MenuViewController.shared.fetchOrderList { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let orderListResponse):
self.orders = orderListResponse.records

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

func updateUI() {

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

}
更新 badge 數字提醒

--

--