【iOS】#8 訂飲料APP|Part.2 新增飲料訂單 — POST、客製 RadioButton & Checkbox、NotificationCenter
前情提要 — 可不可飲料訂購APP為系列文章,初步規劃分為四篇文章介紹以下四大頁面及功能:
・註冊登入頁面:使用者註冊登入及訪客登入|Firebase 身份驗證
・Menu菜單頁面:串接 Airtable API 呈現飲料菜單|GET
・飲料訂購頁面:新增飲料訂單|POST
・訂購清單頁面:編輯、刪除訂單|PATCH、DELETE
本篇為飲料訂購頁面
🌟 本篇重點功能
- 新增飲料訂單,使用 RESTful API — POST
- 客製單選 RadioButton & 多選 Checkbox
- 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)
}
}
②客製單選 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
RadioButtonStatus
:表示單選按鈕的狀態,定義兩個可能的值:.checked
(已選取)和.unchecked
(未選取)- 定義一個計算屬性
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
}
}
}
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)
}
}
}
}
③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
}
}
}