swiftPractice[18]_咖啡豆購物 APP

利用 Page Control,Button & Gesture 更換內容+UIStepper練習

Tania
彼得潘的 Swift iOS / Flutter App 開發教室
26 min readNov 29, 2023

--

這個購物 App 是兩個作業的綜合體,也是卡關許久。
一開始不小心設計成兩個 controller 的模式,我試圖把 TableViewController 嵌入 ViewController 裡,原本只是想說用表格方式呈現商品比較省力,結果畫面全部拉好才發現不小心跳級打怪,需要用到資料傳遞,原地掙扎了好一陣子查解法,最後還是跑去問 peter ,結果 peter 叫我回新手村(?😂

後來我還是使用了能力範圍內的做法,用 ScrollView 的方式呈現長得像表格的 View,土法煉鋼在 View 裡加入很多格式相同的 subview 在塞回 ScrollView 裡。詳細做法參考下面連結👇

沒能順利產出的第一版(ContainerView+TableViewController呈現內容)
成功的第二版(ScrollView+View呈現內容)

經過第一版的練習,一回生二回熟,第二版寫程式跟拉線過程飛快,不過也不是非常順利完成,中間犯了很多粗心小失誤,導致模擬出不來也跑不動,讓我苦惱了一個下午😂將一併記錄在後面!

成果

拆解!

A. 咖啡廳分店資訊區

目錄:
- 拉IBOutlet & IBAction
- 建立各種資訊 Array
- 設定初始畫面
- 更新分店資訊的function
- 功能一、使用PageControl更換圖片與文字資訊
- 功能二、使用Button & Swipe Gesture Recognizer更換圖片與文字資訊
分店資訊區Demo

拉 IBOutlet & IBAction

//分店資訊區
@IBOutlet weak var cafeImageView: UIImageView!
@IBOutlet weak var cafeNameLabel: UILabel!
@IBOutlet weak var cafePhoneLabel: UILabel!
@IBOutlet weak var cafeAdressLabel: UILabel!
@IBOutlet weak var pageControl: UIPageControl!

建立各種資訊 Array

var index = 0
var cafeImageArray = ["cafe1","cafe2","cafe3","cafe4","cafe5"]
var cafeNameArray = ["民權創始店","二號公園店","三號松江店","古亭示範店","內湖港墘店"]
var cafePhoneArray = ["(02)2545-9945","(02)2545-3737","(02)2571-6845","(02)2365-6627","(02)2797-1919"]
var cafeAdressArray = ["松山區民權東路三段140巷2-1號","松山區民生東路三段113巷25弄2號","中山區松江路330巷30號","中正區金門街2-6號","內湖區港墘路127巷12號"]

設定初始畫面

✦ 將初始畫面設為第一張圖以及資訊

//設定初始畫面
override func viewDidLoad() {
super.viewDidLoad()
cafeImageView.image = UIImage(named: cafeImageArray[0])
cafeNameLabel.text = cafeNameArray[0]
cafePhoneLabel.text = cafePhoneArray[0]
cafeAdressLabel.text = cafeAdressArray[0]
pageControl.currentPage = 0

cafeImageView.layer.cornerRadius = 5
}

更新分店資訊的 function

//更新分店資訊function
func updateInfo(){
cafeImageView.image = UIImage(named: cafeImageArray[index])
cafeNameLabel.text = cafeNameArray[index]
cafePhoneLabel.text = cafePhoneArray[index]
cafeAdressLabel.text = cafeAdressArray[index]
pageControl.currentPage = index
}

功能一、使用 PageControl 更換圖片與文字資訊

✦ 小圓點第一頁以 0 代表,剛好跟 Array 一樣,因此可以直接用 sender.currentPage 得到當前頁面存入 index 中,在呼叫前面做好的 updateInfo( ) 功能來將資訊更新到畫面上

✦ 特別需要注意的是小圓點只會往右或是往左,沒辦法依照選到的位置跳著移動

//小圓點換頁功能
@IBAction func pressPageControl(_ sender: UIPageControl) {
index = sender.currentPage
updateInfo()
}

功能二、使用 Button & Swipe Gesture Recognizer 更換圖片與文字資訊

✦ 會寫在一起是因為左滑手勢跟下一頁按鈕的程式是一樣的;反之亦然,所以也可以將他們連到同一個IBAction

✦ 下一頁(左滑):代表 index 數字要 +1 ,但是又不能超過總數,不然就會爆掉,所以取總數的餘數就能確保數字不會超過

✦ 上一頁(右滑):代表 index 要 -1 ,為了確保數字不會變負數先加上總數,再取總數的餘數,就能確保數字在範圍內而且向後走

    //左滑到下一頁
@IBAction func swipeLeft(_ sender: UISwipeGestureRecognizer) {
index = ( index + 1 ) % cafeNameArray.count
updateInfo()
}
//右滑到上一頁
@IBAction func swipeRight(_ sender: UISwipeGestureRecognizer) {
index = ( index + cafeNameArray.count - 1 ) % cafeNameArray.count
updateInfo()
}
//下一頁按鈕
@IBAction func nextButtonPressed(_ sender: UIButton) {
index = ( index + 1 ) % cafeNameArray.count
updateInfo()
}
//上一頁按鈕
@IBAction func preButtonPressed(_ sender: UIButton) {
index = ( index + cafeNameArray.count - 1 ) % cafeNameArray.count
updateInfo()

}

筆記:在拉 Gesture Recognizer 時,每個方向要分開拉,可以編輯名字避免混亂(左),方向選單也要記得選(右),才不會兩個都是預設的同方向

B. 商品選購&金額計算區

目錄
- 拉IBOutlet & IBAction
- 設定初始畫面
- 功能一、調整商品數量
- 功能二、清除所有選購內容
商品選購&計算區Demo

拉 IBOutlet & IBAction

✦ 價格 Label 是為了轉換成數字計算金額
✦ 數量 Label 用來更新當前選購數量
✦ UIStepper 用來讀取數量計算總價、以及轉換成字串更新數量標籤

//商品區
//價格Label
@IBOutlet weak var houseBlendPriceLabel: UILabel!
@IBOutlet weak var wEthiopiaPriceLabel: UILabel!
@IBOutlet weak var nEthiopiaPriceLabel: UILabel!
@IBOutlet weak var geishaPriceLabel: UILabel!
@IBOutlet weak var kenyaPriceLabel: UILabel!
@IBOutlet weak var sumatraPriceLabel: UILabel!
//數量Label
@IBOutlet weak var houseBlendQty: UILabel!
@IBOutlet weak var ethiopiaWashedQty: UILabel!
@IBOutlet weak var ethiopiaNaturalQty: UILabel!
@IBOutlet weak var geishaQty: UILabel!
@IBOutlet weak var kenyaQty: UILabel!
@IBOutlet weak var sumatraQty: UILabel!
//UIStepper
@IBOutlet weak var houseBlendStepper: UIStepper!
@IBOutlet weak var ethiopiaWashedStepper: UIStepper!
@IBOutlet weak var ethiopiaNaturalStepper: UIStepper!
@IBOutlet weak var geishaStepper: UIStepper!
@IBOutlet weak var kenyaStepper: UIStepper!
@IBOutlet weak var sumatraStepper: UIStepper!
//計算區   
@IBOutlet weak var totalLabel: UILabel!
@IBOutlet weak var totalQtyLabel: UILabel!

設定初始畫面

✦ 設定金額,方便後面計算時使用

//設定初始畫面
override func viewDidLoad() {
super.viewDidLoad()

houseBlendPriceLabel.text = "280"
wEthiopiaPriceLabel.text = "450"
nEthiopiaPriceLabel.text = "450"
geishaPriceLabel.text = "550"
kenyaPriceLabel.text = "500"
sumatraPriceLabel.text = "500"
}

功能一、調整商品數量

✦ 先在前面建立儲存各商品數量的變數

  var houseBlend:Int = 0
var ethiopiaWashed:Int = 0
var ethiopiaNatural:Int = 0
var geisha:Int = 0
var kenya:Int = 0
var sumatra:Int = 0

✦ 做一個計算總金額的Function,先將價格 Label 由 String 轉換成 Int,各商品數量會在 Stepper 的功能獲得更新,總金額=所有品項 「價格X數量」 的加總

✦ 使用 NumberFormatter( ) 裡的 .maximumFractionDigits 設定為 0,代表數字後小數點有 0 位數
✦ .numberStyle 設為 .currency 可以將字串用金錢方式呈現
✦ 再將數字轉成字串存入 totalPrice 的 Label 裡面

✦ 另外加總商品數量存入 totalQty 的 Label

//商品區
//計算總金額功能
func calculate(){
//將價格Label轉換成Int
let houseBlendPrice = Int(houseBlendPriceLabel.text!)!
let ethioiaWashedPrice = Int(wEthiopiaPriceLabel.text!)!
let ethiopiaNaturalPrice = Int(nEthiopiaPriceLabel.text!)!
let geishaPrice = Int(geishaPriceLabel.text!)!
let kenyaPrice = Int(kenyaPriceLabel.text!)!
let sumatraPrice = Int(sumatraPriceLabel.text!)!
//總金額=價格*數量
let totalPrice = houseBlendPrice * houseBlend + ethioiaWashedPrice * ethiopiaWashed + ethiopiaNaturalPrice * ethiopiaNatural + geishaPrice * geisha + kenyaPrice * kenya + sumatraPrice * sumatra

//使用NumberFormatter()將數字轉成金額格式的字串
let formatter = NumberFormatter()
//設定沒有小數點後的數字
formatter.maximumFractionDigits = 0
formatter.numberStyle = .currency
let priceString = formatter.string(from: NSNumber(value: totalPrice))
totalLabel.text = priceString
//計算總購物數量
let totalQty = houseBlend + ethiopiaWashed + ethiopiaNatural + geisha + kenya + sumatra
totalQtyLabel.text = String(totalQty)
}

✦ 這邊的 stepper 功能只是為了偵測使用者動作,在每次動作時即時更新資訊,所以可以全部連在同一個IBAction。至於各品項確切數量,則會由各自的 IBOutlet 取得

//stepper功能
@IBAction func stepperPressed(_ sender: UIStepper) {
//利用stepper outlet讀取數量
houseBlend = Int(houseBlendStepper.value)
ethiopiaWashed = Int(ethiopiaWashedStepper.value)
ethiopiaNatural = Int(ethiopiaNaturalStepper.value)
geisha = Int(geishaStepper.value)
kenya = Int(kenyaStepper.value)
sumatra = Int(sumatraStepper.value)

//上面讀取到的數量轉為字串,更新在對應Label
houseBlendQty.text = String(houseBlend)
ethiopiaWashedQty.text = String(ethiopiaWashed)
ethiopiaNaturalQty.text = String(ethiopiaNatural)
geishaQty.text = String(geisha)
kenyaQty.text = String(kenya)
sumatraQty.text = String(sumatra)

//呼叫計算功能
calculate()
}

功能二、清除所有選購內容

✦ 將所有參數歸零

@IBAction func clearPurchase(_ sender: UIButton) {
//Stepper歸零
houseBlendStepper.value = 0
ethiopiaWashedStepper.value = 0
ethiopiaNaturalStepper.value = 0
geishaStepper.value = 0
kenyaStepper.value = 0
sumatraStepper.value = 0
//數量Label歸零
houseBlendQty.text = "0"
ethiopiaWashedQty.text = "0"
ethiopiaNaturalQty.text = "0"
geishaQty.text = "0"
kenyaQty.text = "0"
sumatraQty.text = "0"
//計算區Label歸零
totalLabel.text = "0"
totalQtyLabel.text = "0"
}

完整程式碼

import UIKit

class ViewController: UIViewController {
//分店資訊區
@IBOutlet weak var cafeImageView: UIImageView!
@IBOutlet weak var cafeNameLabel: UILabel!
@IBOutlet weak var cafePhoneLabel: UILabel!
@IBOutlet weak var cafeAdressLabel: UILabel!
@IBOutlet weak var pageControl: UIPageControl!

var index = 0
var cafeImageArray = ["cafe1","cafe2","cafe3","cafe4","cafe5"]
var cafeNameArray = ["民權創始店","二號公園店","三號松江店","古亭示範店","內湖港墘店"]
var cafePhoneArray = ["(02)2545-9945","(02)2545-3737","(02)2571-6845","(02)2365-6627","(02)2797-1919"]
var cafeAdressArray = ["松山區民權東路三段140巷2-1號","松山區民生東路三段113巷25弄2號","中山區松江路330巷30號","中正區金門街2-6號","內湖區港墘路127巷12號"]

//更新分店資訊function
func updateInfo(){
cafeImageView.image = UIImage(named: cafeImageArray[index])
cafeNameLabel.text = cafeNameArray[index]
cafePhoneLabel.text = cafePhoneArray[index]
cafeAdressLabel.text = cafeAdressArray[index]
pageControl.currentPage = index
}

//商品區
//價格
@IBOutlet weak var houseBlendPriceLabel: UILabel!
@IBOutlet weak var wEthiopiaPriceLabel: UILabel!
@IBOutlet weak var nEthiopiaPriceLabel: UILabel!
@IBOutlet weak var geishaPriceLabel: UILabel!
@IBOutlet weak var kenyaPriceLabel: UILabel!
@IBOutlet weak var sumatraPriceLabel: UILabel!
//數量
@IBOutlet weak var houseBlendQty: UILabel!
@IBOutlet weak var ethiopiaWashedQty: UILabel!
@IBOutlet weak var ethiopiaNaturalQty: UILabel!
@IBOutlet weak var geishaQty: UILabel!
@IBOutlet weak var kenyaQty: UILabel!
@IBOutlet weak var sumatraQty: UILabel!
//Stepper
@IBOutlet weak var houseBlendStepper: UIStepper!
@IBOutlet weak var ethiopiaWashedStepper: UIStepper!
@IBOutlet weak var ethiopiaNaturalStepper: UIStepper!
@IBOutlet weak var geishaStepper: UIStepper!
@IBOutlet weak var kenyaStepper: UIStepper!
@IBOutlet weak var sumatraStepper: UIStepper!

var houseBlend:Int = 0
var ethiopiaWashed:Int = 0
var ethiopiaNatural:Int = 0
var geisha:Int = 0
var kenya:Int = 0
var sumatra:Int = 0

//計算區
@IBOutlet weak var totalLabel: UILabel!
@IBOutlet weak var totalQtyLabel: UILabel!


//設定初始畫面
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
cafeImageView.image = UIImage(named: cafeImageArray[0])
cafeNameLabel.text = cafeNameArray[0]
cafePhoneLabel.text = cafePhoneArray[0]
cafeAdressLabel.text = cafeAdressArray[0]
pageControl.currentPage = 0

cafeImageView.layer.cornerRadius = 5
houseBlendPriceLabel.text = "280"
wEthiopiaPriceLabel.text = "450"
nEthiopiaPriceLabel.text = "450"
geishaPriceLabel.text = "550"
kenyaPriceLabel.text = "500"
sumatraPriceLabel.text = "500"
}

//分店資訊區功能
//小圓點換頁功能
@IBAction func pressPageControl(_ sender: UIPageControl) {
index = sender.currentPage
updateInfo()
}
//左滑到下一頁
@IBAction func swipeLeft(_ sender: UISwipeGestureRecognizer) {
index = ( index + 1 ) % cafeNameArray.count
updateInfo()
}
//右滑到上一頁
@IBAction func swipeRight(_ sender: UISwipeGestureRecognizer) {
index = ( index + cafeNameArray.count - 1 ) % cafeNameArray.count
updateInfo()
}
//下一頁按鈕
@IBAction func nextButtonPressed(_ sender: UIButton) {
index = ( index + 1 ) % cafeNameArray.count
updateInfo()
}
//上一頁按鈕
@IBAction func preButtonPressed(_ sender: UIButton) {
index = ( index + cafeNameArray.count - 1 ) % cafeNameArray.count
updateInfo()
}

//商品區功能
//計算總金額功能
func calculate(){
let houseBlendPrice = Int(houseBlendPriceLabel.text!)!
let ethioiaWashedPrice = Int(wEthiopiaPriceLabel.text!)!
let ethiopiaNaturalPrice = Int(nEthiopiaPriceLabel.text!)!
let geishaPrice = Int(geishaPriceLabel.text!)!
let kenyaPrice = Int(kenyaPriceLabel.text!)!
let sumatraPrice = Int(sumatraPriceLabel.text!)!

let totalPrice = houseBlendPrice * houseBlend + ethioiaWashedPrice * ethiopiaWashed + ethiopiaNaturalPrice * ethiopiaNatural + geishaPrice * geisha + kenyaPrice * kenya + sumatraPrice * sumatra

let formatter = NumberFormatter()
formatter.maximumFractionDigits = 0
formatter.numberStyle = .currency
let priceString = formatter.string(from: NSNumber(value: totalPrice))
totalLabel.text = priceString

let totalQty = houseBlend + ethiopiaWashed + ethiopiaNatural + geisha + kenya + sumatra
totalQtyLabel.text = String(totalQty)
}
//stepper功能
@IBAction func stepperPressed(_ sender: UIStepper) {
houseBlend = Int(houseBlendStepper.value)
ethiopiaWashed = Int(ethiopiaWashedStepper.value)
ethiopiaNatural = Int(ethiopiaNaturalStepper.value)
geisha = Int(geishaStepper.value)
kenya = Int(kenyaStepper.value)
sumatra = Int(sumatraStepper.value)

houseBlendQty.text = String(houseBlend)
ethiopiaWashedQty.text = String(ethiopiaWashed)
ethiopiaNaturalQty.text = String(ethiopiaNatural)
geishaQty.text = String(geisha)
kenyaQty.text = String(kenya)
sumatraQty.text = String(sumatra)

calculate()
}
//計算區功能
//清除所有選購內容
@IBAction func clearPurchase(_ sender: UIButton) {
houseBlendStepper.value = 0
ethiopiaWashedStepper.value = 0
ethiopiaNaturalStepper.value = 0
geishaStepper.value = 0
kenyaStepper.value = 0
sumatraStepper.value = 0

houseBlendQty.text = "0"
ethiopiaWashedQty.text = "0"
ethiopiaNaturalQty.text = "0"
geishaQty.text = "0"
kenyaQty.text = "0"
sumatraQty.text = "0"

totalLabel.text = "0"
totalQtyLabel.text = "0"
}

}

後記+粗心失誤筆記:

失誤一:第二版拉完所有線、幾乎完成之前我的部分 outlet 連線突然消失,紅色警告沒有顯示,Xcode 進度環卡住跑不動,模擬器當然也叫不出來

結論:後來發現連線消失的上方多了一個大括弧,可能外加遇到少見的 Xcode 卡住,沒有警告輔助的狀態下我找好久,最後求助 peter 三十秒就發現問題了(哈哈哈哈

失誤二:“The compiler is unable to type-check this expression…”

結論:這邊我檢查好久,核對學長姐的方式跟我明明沒有差異,卻沒有注意到我在讀取 stepper 數量時傳錯一個參數,導致程式在 IBAction 跟 function 之間鬼打牆算不出答案(兇手如下圖)

傳了兩個一樣的kenyaStepper參數存到不同商品裡

以上兩個小失誤誤了一個下午,給各位以及未來的我參考~終於又完成一個練習了!耶!

作業出處:

GitHub

--

--