#29 飲料App來杯五桐號吧!

練習串接後台 API,新增讀取後台資料、上傳資料、刪除資料

來來回回修改了好多次,終於可以來寫這篇了!
這次畫面有三個,菜單頁面、商品明細的訂購頁、以及訂單成立頁

⬇️成品預覽

✏️App功能

  • 串接Airtable API
  • JSON Codable/Get/Post/Delete
  • Result Type
  • prepare傳資料
  • UIAlertController / UIPickerView / UIToolBar
  • NotificationCenter

✏️程式說明

1.將菜單一一建立在AirTable的表單上,下圖是點選了grouped by (category)將茶飲分類

AirTable的詳細做法可參考彼得潘大師的文章 ⬇️

連到 Airtable 的 API 網頁,並登入帳號找到建立好的程式
➡️ https://airtable.com/api.

*右邊黑色匡內中的curl解析JSON時會用到

在授權頁面找到personal access tokens,進去後就產生一組API key囉!

2. 使用postman 解析資料
( Get / Post / Delete )

將授權頁面的curl網址輸入postman,並在Headers輸入授權的API Key
依據下方的資料,將app需要的內容建立在struct內

解析的方式可參考這篇⬇️

在Get的部分,可不需要提供Body的raw data就能生成,但在Post時就需要囉!要注意request和response的內容不同,在建立response的codable時,要觀察回傳時的內容有哪些,若寫的不一樣,debug欄位可能會一直出現 key not found 或nil 等等

3. 這次將下載菜單、飲料圖片、及重新抓取訂單的 JSONDecoder 寫在 class裡,並利用shared及completion: @escaping 將funcion傳送到目標controller使用,也搭配Result Type來判斷成功和失敗,如下為下載菜單的程式碼

func fetchData(urlStr: String, completion: @escaping (Result<[Record], Error>) -> Void) {
let url = URL(string: urlStr)
var request = URLRequest(url: url!)
request.httpMethod = "Get"
request.setValue("Bearer keyy7QrfYj3mhT9pM", forHTTPHeaderField: "Authorization")
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
do {
let decoder = JSONDecoder()
let drinkMenu = try decoder.decode(DrinkData.self, from: data)

completion(.success(drinkMenu.records))
print("✅ download menu")

} catch {
completion(.failure(error))
}
}else if let error = error {
completion(.failure(error))
}
}.resume()
}

Result Type 則可以參考這篇 ⬇️

上傳訂單及刪除訂單則寫在執行這些動作的controller裡,上傳訂單時,先編碼再解碼,這邊有參考postman上code的寫法,需特別注意API JSON 和AirTable所需的內容有沒有相同,否則可能會一直出現typeMismatch、keyNotFound(CodingKeys(stringValue: “records”, intValue: nil)…等問題

 func uploadData(){
let urlStr = "https://api.airtable.com/v0/appPjWNJvMilEx1Cz/Order"
let url = URL(string: urlStr)
var request = URLRequest(url: url!)
request.httpMethod = "POST"
request.setValue("Bearer keyy7QrfYj3mhT9pM", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")

let id = createdID()
let confirmOrder = OrderData.Record.Fields(buyer: orderName, drinkName: menuDatas.fields.name, size: size, sugar: sugar, temperature: temp, toppings: topping, pricePerCup: totalPrice, numberOfCups: numberOfCup, createdID: id)
let record = OrderData.Record(id: nil, fields: confirmOrder)
let order = OrderData(records: [record])

let encoder = JSONEncoder()
let encodeData = (try? encoder.encode(order))!
let jasonString = String(data: encodeData, encoding: .utf8)
let postData = jasonString!.data(using: .utf8)
request.httpBody = postData

URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
do {
let decoder = JSONDecoder()
let order = try decoder.decode(OrderData.self, from: data)
let content = String(data: data, encoding: .utf8)
print("🧋\(order)")
print("✅ \(content ?? "")")
DispatchQueue.main.async { // 在使用者按下送出訂單後加入array中
MenuController.shared.order.orders.append(record)
print("📝訂單 \(record)")
}
} catch {
print("😡\(error.localizedDescription)")
}
}
}.resume()

}

4.UIPickerView delegate/dataSource 及
UITextFieldDelegate 及UIToolBar

利用UIPickerView delegate/dataSource來設定PickerView要顯示的數量(numberOfRowsInComponent)、內容(titleForRow)及選擇後是否要執行動作(didSeletedRow),請參考下列文章

UITextFieldDelegate則是用來管理文字欄位物件中文字的編輯和驗證textFieldDidBeginEditing(_:)開始編輯文字欄位,並將pickerView設置在TextField裡,點選後彈出

而pickerView上方一般都會有取消/完成的按鈕,則利用UIToolBar來設定,可注意到UIBarButtonItem的init( title: String?, style: UIBarButtonItem.Style, target: Any?, action: Selector? ),其中target是接收action的訊息,而action則是將操作內容傳送到target,由於是#selector故在建立的function前須加上 @objc

    //set barBotton
let doneBtn = UIBarButtonItem(title: "確認", style: .plain, target: self , action: #selector(done))
let cancelBen = UIBarButtonItem(title: "取消", style: .plain, target: self, action: #selector(cancel))
let spaceBtn = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
toolBar.setItems([cancelBen, spaceBtn, doneBtn], animated: false) //順序影響位置
toolBar.isUserInteractionEnabled = true

for textfield in self.pickerViewTextField {
textfield.inputView = pickerView
textfield.inputAccessoryView = toolBar
}

}
// barbutton action
@objc func done() {
if let orderName = orderNameTextField.text {
self.orderName = orderName
}

switch pickerView.tag {
case 0:

tempTextField.text = temp
if temp.isEmpty == true {
temp = Temperature.allCases[0].rawValue
tempTextField.text = temp
}

case 1:
sugarTextField.text = sugar
if sugar.isEmpty == true {
sugar = Sugar.allCases[0].rawValue
sugarTextField.text = sugar
}

case 2:

sizeTextField.text = size
if size.isEmpty == true {
size = Size.allCases[0].rawValue
sizeTextField.text = size
}

default:
toppingTextField.text = topping
if topping.isEmpty == true {
topping = Topping.allCases[0].rawValue
toppingTextField.text = topping
}
}
tableView.endEditing(true)

}

@objc func cancel(){
tableView.endEditing(true)
}

5. 在sceneDelegate中新增接收通知,並在取得通知後去檢查儲存上傳資料的Array有多少資料,顯示在TabBar上的badgeValue(右上角顯示紅底白字的提醒數字)

NotificationCenter.default.addObserver(self, selector: #selector(updateOrderBadgeValue), name: MenuController.orderUpdateNotification, object: nil)
listTabBar = (window?.rootViewController as? UITabBarController)?.viewControllers?[1].tabBarItem

}

@objc func updateOrderBadgeValue () {
if MenuController.shared.order.orders.isEmpty {
listTabBar?.badgeValue = nil
} else {
listTabBar?.badgeValue = String(MenuController.shared.order.orders.count)
}

在訂單頁面送出通知

static let orderUpdateNotification = Notification.Name("MenuController.orderUpdate")
var order = Order() {
didSet { // 發生改變時立即呼叫
NotificationCenter.default.post(name: MenuController.orderUpdateNotification, object: nil)
}
}

在訂單明細頁面建立觀察者

NotificationCenter.default.addObserver(tableView!, selector: #selector(UITableView.reloadData), name: MenuController.orderUpdateNotification, object: nil)

✏️ App操作及後台的即時顯

左:飲料分類按鈕 ; 右:送出訂單

--

--