iOS| #25 | 約翰紅茶訂餐App 2.0–Part.3 送出訂單與修改訂單

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

--

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

繼上篇講解到購物車的功能,本篇文章主要會說明從購物車頁面送出訂單與在訂單頁面修改訂單的UI畫面與功能實現。

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

技術應用

  • Programming UIKit
  • AutoLayout
  • TableView
  • Airtable REST API — CRUD

UI畫面

  • Cart Page
  • Order Page

功能解說&程式解說

  • Alert 搭配 DatePicker與TextField
    在購物車頁面點擊Send會跳出Alert讓使用者選擇取餐時間

宣告屬性alertController

在viewDidLoad設定呼叫method setAlertController,初始化alertController的action與textField

生出UIDatePicker

let datePicker = UIDatePicker()

設定樣式

datePicker.preferredDatePickerStyle = .wheels

使用addTarget新增事件處理的程式,在valueChanged時會自動呼叫method datePickerValueChanged

datePicker.addTarget(self, action: #selector(datePickerValueChanged), for: .valueChanged)

以下說明datePickerValueChanged程式:

取得alert的相關屬性textField與action “ OK ”(按照規格會有兩個action,但addAction這邊是由自己控制,且確定 “ OK ”一定會在第一個,所以使用first取得)

if let textField = alertController.textFields?[0] ,   let action = alertController.actions.first{    取得屬性後要做的事}

同步datePicker的時間至textField.text上

textField.text = NetWorkController.shared.getOrderHeaderDateTime(date: sender.date)

此時間文字格式統一由NetWorkController的method getOrderHeaderDateTime處理

//格式化為 OrderHeader-date 的字串func getOrderHeaderDateTime(date: Date) -> String {  let formatter = DateFormatter()  formatter.dateFormat = "(yyyy-MM-dd,HH:mm)"  return formatter.string(from: date)}

時間只能選擇未來一小時的,若選擇小於一小時的時間則無法點擊action “ OK ”

//只能點未來一小時的if sender.date > Date().addingTimeInterval(60 * 60){  action.isEnabled = true}else{  action.isEnabled = false}

效果如下:

接下來回到setAlertController程式:

在alertController中加入textField,在closure中寫程式設定textField的屬性

//alertController加TextFieldalertController.addTextField { textField in  textField.inputView = datePicker  textField.addTarget(self, action: #selector(self.textfieldEditingChanged), for: .editingChanged)}

將inputView設為剛剛生成的datePicker讓使用者可以自由選擇時間

textField.inputView = datePicker

使用addTarget新增事件處理的程式,在editingChanged時會自動呼叫method textfieldEditingChanged

textField.addTarget(self, action: #selector(self.textfieldEditingChanged), for: .editingChanged)

以下說明textfieldEditingChanged程式:

我們只希望使用者使用datePicker修改時間,為了防止使用者在textField自己輸入文字,於是在這邊固定使用alertController的inputView(datePicker)屬性date作為textField的值

接下來回到setAlertController程式:

生成UIAlertAction,當點擊action“OK”會送出訂單。(saveDataToAirTable()會在後面送出訂單部分說明)

let action_Ok = UIAlertAction(title: "OK", style: .default) { action in//keep取貨時間g_headerInfo.date = NetWorkController.shared.getOrderHeaderDateTime(date: datePicker.date)//儲存資料到AirTableself.saveDataToAirTable()}

生成UIAlertAction,當點擊action“Cancel”會離開alert,返回購物車頁面。

let action_Cancel = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)

新增action至alert

alertController.addAction(action_Ok)alertController.addAction(action_Cancel)

固定color

alertController.view.tintColor = .systemBlue
  • 送出訂單
    在購物車頁面點選Alert的action button“OK”時會呼叫method saveDataToAirTable()送出訂單

以下會說明saveDataToAirTable()內程式

先初始化orderHeader

orderHeader = OrderHeader(records: [OrderHeader.Records]())

組訂單編號,在這邊先產生日期。(規格為:年月日+六碼流水號。)

var orderNumber = NetWorkController.shared.getOrderHeaderDate(date: Date())

用fetchOrderHeader下where條件從airtable — OrderHeader取得今天已產生的編號

NetWorkController.shared.fetchOrderHeader(urlString: NetWorkController.getOrderHeaderURL, where: (field: "orderNumber", value: orderNumber)) { result inswitch result{  case.failure(let error):    組錯誤訊息,並showAlert    return  case.success(var result):    排序已抓到的資料(by訂單號碼倒序),並且組出流水號  }}

組錯誤訊息,並showAlert

var errorMessage = ""switch error{  case .invalidData:    errorMessage = "invalidData"  case .invalidJsonFormat:    errorMessage = "invalidJsonFormat"  case .invalidResponse:    errorMessage = "invalidResponse"  case .invalidurl:    errorMessage = "invalidurl"  case .requestFailed:    errorMessage = "requestFailed"}DispatchQueue.main.async {  NetWorkController.shared.showAlert(title: "警告", message: "訂單建立失敗" + errorMessage) { alert in    self.present(alert, animated: true)  }}

排序已抓到的資料(by訂單號碼倒序),並且組出流水號

//倒序result.records.sort { records1, records2 in  return records2.fields.orderNumber < records1.fields.orderNumber}//判斷result是否為空陣列if !result.records.isEmpty{  let num = result.records.first!.fields.orderNumber  let startIndex = num.index(num.startIndex, offsetBy: 8)//endIndex是最後一個字的下一個  let string = num[startIndex..<num.endIndex]  var number = Int(string)!//號碼+1  number += 1//轉文字合併字串orderNumber += NetWorkController.shared.getFrontZero(count: 6, value: number)}else{  orderNumber += "000001"}

比較值得一提的為函式sort與字串處理,以下會依序說明。

函式sort解說:
records1:前面的參數
records2:後面的參數
講白話一點,就是後面的數字要小於前面的數字,也就是降冪排列。

result.records.sort { records1, records2 inreturn records2.fields.orderNumber < records1.fields.orderNumber}

字串處理解說:

取得剛才排序後的第一筆orderNumber

let num = result.records.first!.fields.orderNumber

取得orderNumber的流水碼startIndex(在這邊使用函式index並且從num.startIndex數到第八位)。例:20220315000001,依照此邏輯的話startIndex會落在六碼流水號的第一位“0”。

let startIndex = num.index(num.startIndex, offsetBy: 8)

根據index取得完整流水碼。
在這邊使用range從startIndex至num.endIndex要使用小於是因為實際上endIndex會比實際的最後一位再多一位

let string = num[startIndex..<num.endIndex]

轉換成整數

var number = Int(string)!

流水碼 + 1

number += 1

組成完整orderNumber

orderNumber += NetWorkController.shared.getFrontZero(count: 6, value: number)

在這邊利用NetWorkController的method getFrontZero(count: , value: )將整數補上前置零。
count:位數
value:要補上前置零的值

//補前置零func getFrontZero(count: Int, value: Int) -> String{  let formatter = NumberFormatter()  formatter.minimumIntegerDigits = count  let stringNum = NSNumber(value: value)  return formatter.string(from: stringNum)!}

確認表頭資料是否都有值

if let date = g_headerInfo.date,let phone = g_headerInfo.phone,let userName = g_headerInfo.userName,let consigneeName = g_headerInfo.consigneeName{   確定有值以後生成表頭資料,並且上傳資料 }

確定有值以後生成表頭資料

let field = OrderHeader.Records.Fields(orderNumber: orderNumber, userName: userName, date: date, phone: phone, consigneeName: consigneeName, status: Status.send.statusText, totalPrice: self.total)let records = OrderHeader.Records(fields: field)self.orderHeader.records.append(records)

準備上傳資料,先叫出loading畫面

DispatchQueue.main.async {  self.navigationController?.pushViewController(self.loadingVC, animated: false)}

使用NetWorkController的method postOrderHeader、postOrderItem上傳資料,成功或失敗都會出相關alert訊息。詳細程式解說請參考iOS| #25 | 約翰紅茶訂餐App 2.0–Part.1 串接Airtable API

  • 查詢歷史訂單
    在輸入完使用者名稱後點擊icon可以查看歷史訂單

這邊將從歷史訂單的頁面(OrderViewController)開始講起

因為一進來頁面就準備要抓資料了,首先在viewDidLoad生出Loading的ViewController,並且push過去。

抓取訂單資料

根據result type分為success與failure兩種情況,成功就會更新tableView資料,失敗出alert訊息。

switch result {case .success(let orderHeader):  抓到資料,更新tableViewcase .failure:  抓資料失敗,出Alert訊息}

Call API相關程式解說請參考iOS| #25 | 約翰紅茶訂餐App 2.0–Part.1 串接Airtable API

  • 修改訂單&取消訂單

修改訂單與取消訂單都是放在trailing(右手邊的地方),所以要實作func tableView(_ tableView: , trailingSwipeActionsConfigurationForRowAt indexPath: ) -> UISwipeActionsConfiguration?

檢查已經完成、取消的訂單不能更改

//完成訂單不能更改if record.fields.status == Status.complete.statusText { return nil }//取消訂單不能更改if record.fields.status == Status.cancel.statusText { return nil }

生出取消訂單的button,並且點擊以後做相對應的動作。(Call API相關程式不再贅述)

let cancel = UIContextualAction(style: .normal, title: "取消") { action, view, bool in  更新Airtable}

生出更改訂單的button,並且點擊以後做相對應的動作。(Call API相關程式不再贅述)

let edit = UIContextualAction(style: .normal, title: "更改") { action, view, bool in  使用UIAlertController搭配TextField讓使用者可以修改資料let okAction = UIAlertAction(title: "OK", style: .default) { action 
in
檢查alertController中的textField是否有值 更新Airtable}
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)

在這邊值得一提的是再度使用了UIAlertController搭配TextField的技術

let alert = UIAlertController(title: "修改訂單資訊", message: "僅以下資訊提供調整", preferredStyle: .alert)alert.addTextField { textField in  textField.placeholder = "取貨人名稱"}alert.addTextField { textField in  textField.placeholder = "聯絡電話"  textField.keyboardType = .phonePad}

點擊Alert的 OK action,檢查alertController中的textField是否有值

//兩個都是空的if let consigneeName = alert.textFields?[0].text,consigneeName.isEmpty,let phone = alert.textFields?[1].text,phone.isEmpty{  NetWorkController.shared.showAlert(title: "警告", message: "無修改內容") { alert in    self.present(alert, animated: true, completion: nil)  }  return}

若有值則更新Airtable。(Call API相關程式不再贅述)

//填值if let consigneeName = alert.textFields?[0].text,!consigneeName.isEmpty{  record.fields.consigneeName = consigneeName}if let phone = alert.textFields?[1].text,!phone.isEmpty{  record.fields.phone = phone}

Call API相關程式解說請參考iOS| #25 | 約翰紅茶訂餐App 2.0–Part.1 串接Airtable API

  • 刪除訂單

應用到的技術與前面雷同,相關的程式解說就不再贅述。

Call API相關程式解說請參考iOS| #25 | 約翰紅茶訂餐App 2.0–Part.1 串接Airtable API

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

--

--