本章是最後一個章節,將新增帳單頁面。我們會介紹 Property Wrapper – @Published 和 @ObservedObject,以及 ObservableObject protocol。利用它們,在物件更新時,可以通知 View 刷新畫面。另外還介紹一個 UI 元件叫 Stepper。
SwiftUI Coffee Shop
帳單頁面
新增一個資料夾 Order,然後在下面新增 OrderListView。這是帳單功能的主頁面。把它加入到 ContentView 的 TabView 裡,並用下面的圖示作為 Tab 上的 Icon。
struct ContentView: View {
var body: some View {
TabView {
...
OrderListView()
.tabItem {
Image("OrderIcon")
Text("Bill")
}
}
}
}OrderListView 的程式碼大致如下。它會從 OrderManager 中取得所有的訂單,然後對每個訂單顯示其商品名稱。最下面會有一行顯示全部訂單的總金額。
執行一下程式。在商品管理頁裡,新增 Aemericano 和金額 2 元。到菜單頁點一次 Aemericano,然後到帳單頁應該可以看到一杯 Americano 和總金額 2 元。
現在你再到菜單頁再加點一次 Americano,然後到帳單頁你會發現,總金額還是 2 元(此時應該是 2 杯 Americano 所以是 4 元)。也就是說,第二次點餐後,帳單頁面並沒有刷新!
struct OrderListView: View {
var manager = OrderManager.shared
var body: some View {
VStack {
List {
ForEach(manager.orders, id: \.id) { order in
Text(order.name)
}
}
HStack {
Text("Total")
.font(.system(size: 22))
Spacer()
Text(String(format: "$ %.2f", total()))
.font(.custom("Georgia", size: 22))
}
.frame(minWidth: 0, maxWidth: .infinity, alignment: .trailing)
.padding()
}
}
private func total() -> Double {
manager.orders.reduce(0, { total, order in
total + order.price * Double(order.quantity)
})
}
}ObservableObject
上面的提到問題主要是,帳單頁並不知道 OrderManager 中的訂單列表有更新,所以沒有刷新頁面。在這邊我們需要一個類似 @State 的功能,來通知 OrderListView 有更新。不過, @State 是用在 View 自己的 Property 被更新時,View 會刷新。而 OrderManager 不是 OrderListView 的 Property,所以無法使用 @State。@State 在這個章節中有較詳細的解說。
SwiftUI 提供另外一個方法來處理這種情況,叫 ObservableObject。
首先修改 OrderManager,讓它實作 protocol ObservableObject。替 Property orders 加上 Property Wrapper – @Published。@Published 是說,當這個 Property 被更新時,會發送 (emit) 一個事件通知給監控它的物件。
class OrderManager: ObservableObject {
@Published var orders: [Order] = []
}在 OrderListView 中,為 Property manager 加上 Property Wrapper – @ObservedObject。@ObservedObject 是說,監聽這個實作 ObservableObject 物件所發出的事件通知。
struct OrderListView: View {
@ObservedObject var manager = OrderManager.shared
}經由 ObservableObject、@Published 和 @ObservedObject 三個搭配下,OrderListView 就可以知道 Property manager 有被更新,那 OrderListView 就會刷新畫面。
執行一下修改後的程式。依照剛剛的步驟再做一次點餐。OrderListView 現在應該會刷新頁面了。
Stepper
在 Order 資料夾下,新增 OrderRowView。我們希望在帳單頁面中,可以對每一筆訂單調整數量,所以這邊我們使用 Stepper 這個 UI 元件。
struct OrderRowView: View {
let order: Order
let onIncrement: (Order) -> Void
let onDecrement: (Order) -> Void
var body: some View {
Stepper(onIncrement: {
self.onIncrement(self.order)
}, onDecrement: {
self.onDecrement(self.order)
}) {
HStack {
Text(order.name)
.font(.custom("Georgia", size: 20))
Spacer()
Text(String(format: "$ %.2f", order.price))
.font(.custom("Georgia", size: 20))
Text(String(format: "%d", order.quantity))
.frame(minWidth: 44)
}
}
.frame(height: 44)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
}
}在 OrderListView 中,我們要加上 OrderRowView,並且處理 onIncrement 和 onDecrement 這兩個方法。OrderManager 裡也要加上 .add(order:) 和 .remove(order:),這邊可參照完整的程式碼。
struct OrderListView: View {
var body: some View {
VStack {
List {
ForEach(manager.orders, id: \.id) { order in
OrderRowView(order: order, onIncrement: { order in
self.manager.add(order: order)
}, onDecrement: { order in
self.manager.remove(order: order)
})
}
}
}
}結語
Coffee Shop App 終於完成了。相信你藉由它學會不少 UI 元件,和 SwiftUI 的程式架構。SwfitUI 引進了數個 Property Wrapper,這對剛剛接觸的人來說,東西多了一點,會覺得比較難學。但是學會了之後,會發現 SwiftUI 藉由這些 Property Wrapper 簡化了整個 UI 上的管理。期待 SwiftUI 可以完全地取代 UIKit。









