iOS| #25 | 約翰紅茶訂餐App 2.0–Part.1 串接Airtable API

Tommy
彼得潘的 Swift iOS / Flutter App 開發教室
13 min readMar 8, 2022

--

不久前已經實作過訂飲料App初版,那麼這次要利用目前所學的技能將其升級為2.0,並且加上數個功能使其成為一個更完整的產品。

本篇文章主要會說明Airtable設計Model的架構與App設計Model的架構。

由於此系列文篇幅會較長,會拆分成多篇文章紀錄實作的過程與功能講解

技術應用

  • Airtable REST API — CRUD
  • Result Type
  • 關聯式資料庫

Model設計 — Airtable

  • Menu
    維護所有品項的資料,後續將使用此設定檔table展開所有飲料品項提供User做選擇
  • OrderHeader
    透過App維護訂單的相關的表頭資訊將會記錄在此table

這邊會利用到關連式資料庫的基本觀念,將表頭存在一個table中,藉由欄位OrderNumber關聯至表身Table — OrderItem,這樣假如有多筆item的話就只需要記一筆表頭的資料就好,不需要在每一筆item都記住一樣的表頭資料。

備註:後續實作發現Airtable的機制並不是這麼簡單,這裡的欄位OrderNumber並不是肉眼看到的”20210215000001”,call api時會返回關聯的table欄位id的string
  • OrderItem
    透過App維護訂單的相關的表身資訊將會記錄在此table

OrderItem — OrderNumber關聯到OrderHeader — OrderNumber
OrderItem — itemID關聯到Menu — itemID

ER Model:

Model設計 — Swift

  • NetWorkController
    有關網路的功能會使用它來實作
  • Ice
    飲料冰塊
  • Sugar
    飲料甜度
  • Size
    飲料尺寸
  • Status
    訂單狀態

因為以下資訊橫跨多個頁面都會用到,再加上正常情況使用下,並不會有龐大的資料量。
在這邊設計讓以下兩個項目作為global data活在程式裡:

  • Cart
    購物車項目
  • HeaderInfo
    表頭資訊

功能解說&程式解說

  • Result Type

在說明Call API的程式以前要先了解Result Type的用法,因為實作CRUD的程式都是使用Result Type來得出結果的。

首先我們來看一小段程式:

FoodType為食物類別:在這邊有兩種食物,分別為MCChicken (麥脆雞)、KFCChicken (肯德基)

enum FoodType {case MCChicken  //麥脆雞case KFCChicken //肯德基}

ErrorChicken為錯誤訊息:在這邊只需要判斷是不是肯得基就好
屬性errorMessage是型態為String的錯誤訊息

//errorenum ErrorChicken: Error {  case isNotKFCChicken  var errorMessage: String {  switch self {       case .isNotKFCChicken:       return "這不是肯德基"        } 
}
}

實作function判斷是否為肯德基,並且使用Result Type。
若為肯德基,則使用.success回傳string;不是肯德基就使用.failure回傳error

//判斷是不是肯德基func checkChicken(food: FoodType , handler: @escaping (Result<String,ErrorChicken>) -> Void){   switch food{   case .KFCChicken:   handler(.success("肯德基好吃"))   case .MCChicken:   handler(.failure(ErrorChicken.isNotKFCChicken))   }}

各自宣告型態為麥脆雞、肯德基的常數

//生出麥脆雞let MCchicken = FoodType.MCChicken//生成肯德基let KFCChicken = FoodType.KFCChicken

將前面宣告好的常數當作參數傳入function checkChicken中。
並且根據result判斷.success/.failure 分別要做什麼事

checkChicken(food: MCchicken) { result in   switch result{   case .success(let string):   print("沒錯!\(string)")   case .failure(let error):   print("嗚嗚嗚~\(error.errorMessage)")   }} // 結果:嗚嗚嗚~這不是肯德基checkChicken(food: KFCChicken) { result in   switch result{   case .success(let string):   print("沒錯!\(string)")   case .failure(let error):   print("嗚嗚嗚~\(error.errorMessage)")   }} // 結果:沒錯!肯德基好吃

可以看得出來藉由Result Type可以讓source code更有可讀性與維護性。

那麼接下來就說明使用 CRUD API的部分,以下範例都由Airtable - OrderHeader來做說明。

  • 讀取資料(GET)

先複製一份URL string

var urlStringWithCondition = urlString

幫url String加上where條件。(某些特別符號或中文URL可能會認不得,所以記得要使用addingPercentEncoding轉換成URL認得的格式。)

注意:在這邊使用了Airtable的function “SEARCH”,可以搜尋特定欄位的特定資料。詳情請參考官方文件

if let field = condition.field,let value = condition.value{ urlStringWithCondition = urlStringWithCondition +      "&filterByFormula=" + "SEARCH(\"\(value)\",{\ (field)})".addingPercentEncoding(withAllowedCharacters:  .urlQueryAllowed)!}

將string轉換成URL,若失敗的話則丟出Error

guard let url = URL(string: urlStringWithCondition) else {completion(.failure(.invalidurl))return }

產生data task上網抓資料

URLSession.shared.dataTask(with: url) { data, response, error in   抓資料程式}.resume()

檢查是否有錯誤,若有錯誤的話則丟出Error

if let error = error {completion(.failure(.requestFailed))return}

檢查status是否為OK,若失敗的話則丟出Error

guard let httpResponse = response as? HTTPURLResponse,httpResponse.statusCode == 200else {completion(.failure(.invalidResponse))return}

檢查是否有抓到資料,若失敗的話則丟出Error

guard let data = dataelse {completion(.failure(.invalidData))return}

解析JSON格式的資料,若失敗的話則丟出Error

let decoder = JSONDecoder()guard let orderHeader = try? decoder.decode(OrderHeader.self, from: data)else {completion(.failure(.invalidJsonFormat))return  }

前面都成功的話就會走到這一步,最後將解析完的資料透過.success傳出去

completion(.success(orderHeader))
  • 新增資料(POST)

可以看得出來與讀取資料的程式十分雷同,接下來將會說明差異的地方,其餘相同部分不再贅述。

建立request

var request = URLRequest(url: url)

設好httpMethod

request.httpMethod = "POST"

給api所需的http Header值(建立好table後在官方文件裡看找到curl,就可以利用它來知道要填哪些資訊)

request.setValue("Bearer \(NetWorkController.apikey)", forHTTPHeaderField: "Authorization")request.setValue("application/json", forHTTPHeaderField: "Content-Type")

將組好的資料轉為JSON格式,並把值pass給http body

let encoder = JSONEncoder()let data = try? encoder.encode(orderHeader)request.httpBody = data

其餘部分可以參考上述讀取資料的說明。

  • 修改資料(PATCH)

首先要了解PATCH、PUT都是跟Update Table有關的method,根據官方文件說明:

濃縮內容大致上為使用PATCH只會更改指定的fields;使用PUT會將指定的fields以外的欄位都清空。

只透過文字說明可能還是會有一些理解上的困難,我們來看看下述例子吧!

首先我們以訂單編號“20220226000002”為例子。

使用PATCH更新資料,將欄位totalPrice改為100。

使用PUT更新資料,將欄位totalPrice改為125。

以上例子來看感覺沒有什麼差異,這是因爲我們同時有指定其他的fields,以下我們試看看在body只指定totalPrice與id:

使用PATCH更新資料,將欄位totalPrice改為100。

使用PUT更新資料,將欄位totalPrice改為125。

可以明顯地看到使用PUT的話,totalPrice一樣成功被修改了,但由於其他欄位沒有被指定的關係,會全部被清空。
備註:createdTime是因為欄位屬性本身就會自動產生值,所以沒有被清空。

為了避免資料被清空,在這邊我們使用PATCH來修改訂單。

可以看得出來與新增資料的程式更是如出一徹,差別只在於httpMethod改為PATCH。

request.httpMethod = "PATCH"

其餘部分可以參考上述新增資料的說明。

  • 刪除資料(DELETE)

可以看得出來與修改資料的程式非常相似,接下來將會說明差異的地方,其餘相同部分不再贅述

組query string(在這邊只考慮刪除單筆的情況),可以參考官方文件給的範例

var urlStringWithCondition = urlString + "?records[]=\(id)"

httpMethod改為DELETE

request.httpMethod = "DELETE"

若內容有誤煩請指教,感謝收看。

--

--