本章是最後一個章節,將新增帳單頁面。我們會介紹 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。