#033 訂飲料App — Part3畫面

功能介紹

  • 查看飲料清單
  • 飲料介紹
  • 選擇甜度、冰塊及配料
  • 加入訂單後可編輯杯數或刪除
  • 編輯訂購人名稱
  • 送出訂單上傳至AirTable
  • 新增loading view

前兩篇傳送門

呼叫picker / 刪除清單&回傳資料

✨調整訂購杯數呼叫 UIPicker

首先將 Picker view & Toolbar 拉至 controller Exit 下方,在 Toolbar 中放 Bar Button Item,之後將 controller 設為 picker view 的 data source & delegate

設定好後,順一下我們的需求,希望點選綠色 view 的時候出現 Picker,Picker 選擇完後 label 更新
因此必須知道現在點選的是哪個 cell ,以及用什麼方法去呼叫 Picker View

根據參考的文章可以用隱藏的 textField 呼叫代替鍵盤的 picker,因此在 cell 內綠色的 view 上放了隱藏的 textField,在生成 cell 的程式碼將鍵盤換成 picker,記得 Picker view & Toolbar 要拉進 controller 連結 outlet,Textfield 要拉進 TableViewCell 連結 outlet

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = orderTableView.dequeueReusableCell(withIdentifier: "\(OrderTableViewCell.self)", for: indexPath) as! OrderTableViewCell
let row = indexPath.row

//鍵盤換成Picker
cell.editNumTextfield.inputView = numberPicker
//Picker上方加入Toolbar
cell.editNumTextfield.inputAccessoryView = numberToolBar

cell.orderNameLabel.text = orders[row].name
cell.orderPriceLabel.text = "$\(orders[row].totalPrice)"
cell.addLabel.text = "\(orders[row].ice)、\(orders[row].sugar) \(orders[row].additional)"
cell.orderNumLabel.text = "\(orders[row].quantity)杯"

if orders[row].additional == Add.AddPearl.rawValue {
cell.addPriceLabel.text = "+ $\(orders[row].quantity * 10)"
}else {
cell.addPriceLabel.text = ""
}
return cell
}

建立 picker 裡的內容

let orderNumbers = Array(1...10)
var selectPickerNum: Int?

extension OrderViewController: UIPickerViewDelegate, UIPickerViewDataSource {

func numberOfComponents(in pickerView: UIPickerView) -> Int {
1
}

//picker一欄有幾個選項
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
orderNumbers.count
}
//picker每個選項顯示的字串
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
"\(orderNumbers[row])"
}
//選取的數字存入變數,後面可以拿來運算價格
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
selectPickerNum = orderNumbers[row]
}
}

參考的文章內是利用 tap gesture 來觸發呼叫 picker 這件事,但因為這是程式生成的 cell,不能在裡面加 tap gesture,卡超級久後,想到用透明的 UIButton 放在綠色 view 上當觸發的物件不就好了!茅塞頓開,後面直接順起來,UIButton IBAction 可以寫在 controller 裏面,不受重複生成的 cell 影響~

weak var currentCellSelected: OrderTableViewCell?
var currentCellPath: IndexPath?

@IBAction func showPicker(_ sender: UIButton) {
//找出點擊的cell位置
let point = sender.convert(CGPoint.zero, to: orderTableView)
if let indexPath = orderTableView.indexPathForRow(at: point) {
bringUpPickerViewWithRow(indexPath: indexPath)
currentCellPath = indexPath
}
}

func bringUpPickerViewWithRow(indexPath: IndexPath) {
currentCellSelected = orderTableView.cellForRow(at: indexPath) as? OrderTableViewCell
currentCellSelected?.editNumTextfield.becomeFirstResponder()
}

這樣 picker 就被我們呼叫起來拉,可喜可賀
接著利用得到點擊 cell 的位置 currentCellPath 與 currentCellSelected 存取及修改 label,詳細細節可以下載GitHub

@IBAction func savePicker(_ sender: Any) {
guard let selectPickerNum = self.selectPickerNum,
let row = currentCellPath?.row else { return }
let newTotalPrice = orders[row].totalPrice / orders[row].quantity * selectPickerNum
let editOrder = DrinkDetail(name: orders[row].name, ice: orders[row].ice, sugar: orders[row].sugar, quantity: selectPickerNum, totalPrice: newTotalPrice, orderTime: orders[row].orderTime, additional: orders[row].additional, orderer: nameTextfield.text!)
let newAddPrice = selectPickerNum * 10
orders[row] = editOrder
currentCellSelected?.orderNumLabel.text = "\(selectPickerNum)杯"
currentCellSelected?.orderPriceLabel.text = "$\(newTotalPrice)"
currentCellSelected?.addPriceLabel.text = "+ $\(newAddPrice)"
let sum = sumMoney(orders: orders)
totalMoneyLabel.text = "$ \(sum)"
delegate?.orderViewControllerDelegate(self, didSelect: orders)
view.endEditing(true)
}

參考文章

✨填寫 textfield 時不被鍵盤擋住

這頁的畫面中,是 tableview 裝進 controller ,當時參考下面文章介紹運用 scroll view 的做法,但因 tableview 與 scroll view 兩個都是可滑動的物件,因此不適用,畫面會長 contentInset 但底部的 View 不會如期往上移動。不要灰心,從文章中我們獲得如何通知鍵盤出現與消失和如何取得鍵盤高度的方法

func registerForKeyboardNotifications() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWasShown(_:)), name: UIResponder.keyboardDidShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillBeHidden(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
}
@objc func keyboardWasShown(_ notification: NSNotification) {
guard let info = notification.userInfo,
let keyboardFrameValue = info[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return }
let keyboardFrame = keyboardFrameValue.cgRectValue
let keyboardSize = keyboardFrame.size
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 0.3, delay: 0) {
self.bottomView.transform = CGAffineTransform(translationX: 0, y: -keyboardSize.height)
}
}

@objc func keyboardWillBeHidden(_ notification: NSNotification) {
bottomView.transform = CGAffineTransform(translationX: 0, y: 0)
}

用 keyboardFrameEndUserInfoKey 獲得鍵盤高度,利用 NotificationCenter 加入鍵盤出現跟消失的通知,底部的 view 用 transform 移動獲得的鍵盤高度位置,因為有用 AutoLayout 的關係,不能用 frame 去移動位置,會大跑版

參考文章

等待期間顯示 loading 動畫 (Lottie)

首先安裝 Lottie 套件,接著建立一個 xib 檔案,將裡面 view 的 class 指定為 LottieAnimationView,接著把下載好的動畫 json 檔丟進 Xcode,將這個view 要顯示的動畫 json 檔名稱,填進 AnimationName 裏

新增一個 UIViewController ,建立 extension 方便之後每個 ViewController 都可以呼叫

import UIKit
import Lottie

class LoadingViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
//background
let bgLayer = UIView()
bgLayer.frame = CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height)
bgLayer.layer.backgroundColor = CGColor(gray: 0, alpha: 0.5)
view.addSubview(bgLayer)
loadXib()
}

func loadXib(){
//call xib
if let loadingView = Bundle(for: LottieAnimationView.self).loadNibNamed("loading", owner: nil, options: nil)?.first as? LottieAnimationView {
let width = 140
let centerX = (Int(view.bounds.width) - width)/2
let centerY = (Int(view.bounds.height) - width)/2
//define size of animation
loadingView.frame = CGRect(x: centerX, y: centerY, width: width, height: width)
view.addSubview(loadingView)
//animation style setting
loadingView.contentMode = .scaleAspectFit
loadingView.loopMode = .loop
loadingView.animationSpeed = 1
loadingView.play()
}
}
}

extension UIViewController {
func presentLoading() {
let controller = LoadingViewController()
//controller直接蓋上前一個viewcontroller方式出現
controller.modalPresentationStyle = .overCurrentContext
//controller用漸變方式呈現
controller.modalTransitionStyle = .crossDissolve
present(controller, animated: true, completion: nil)
}

func dismissLoading(){
dismiss(animated: true)
}
}

參考文章

動畫來源

https://lottiefiles.com/98194-loading

color asset

在 asset 裡面新增 color ,可以直接設定同一個顏色在 lightmode 與 darkmode 的顯示色,如果都要長一樣就把右邊的 dark 刪掉即可

成果江江江~~~

沒想到三篇文章花了好多時間撰寫,打字的同時一邊回顧當時自己遇到的卡點與思路,看似三行兩語的,其實當下都試了很多小時,常常找到的資料與自己的狀況不大一樣,但不曉得能否共用之下都嘗試了一遍,雖然多花了一些時間但收穫滿滿!

GitHub

--

--