建立第一個 SwiftUI App:Day 11 – @Published, @ObservedObject

Photo by Brooke Lark on Unsplash
Photo by Brooke Lark on Unsplash
本章是最後一個章節,將新增帳單頁面。我們會介紹 Property Wrapper – @Published 和 @ObservedObject,以及 ObservableObject protocol。利用它們,在物件更新時,可以通知 View 刷新畫面。另外還介紹一個 UI 元件叫 Stepper。

本章是最後一個章節,將新增帳單頁面。我們會介紹 Property Wrapper – @Published 和 @ObservedObject,以及 ObservableObject protocol。利用它們,在物件更新時,可以通知 View 刷新畫面。另外還介紹一個 UI 元件叫 Stepper。

Coffee Shop 的完整程式碼可以在 下載。

帳單頁面

新增一個資料夾 Order,然後在下面新增 OrderListView。這是帳單功能的主頁面。把它加入到 ContentViewTabView 裡,並用下面的圖示作為 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,並且處理 onIncrementonDecrement 這兩個方法。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。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

You May Also Like
Photo by Alex Alvarez on Unsplash
Read More

Dispatch Queue 教學

GCD 提供有效率的並行處理,讓我們不需要直接管理多執行緒。它的 Dispatch Queues 可以循序地(serially)或是並行地(concurrently)執行任務。我們只需要將要並行的程式當作任務提交到 dispatch queues 就可以了。
Read More
Photo by Florinel Gorgan on Unsplash
Read More

如何製作一個 XCFramework

XCFramework 讓你可以將 iPhone、iPhone 模擬器等多的不同平台的二進位碼打包到一個可發佈的 .xcframework 檔。你只需要為你的 Framework 產生出一個 .xcframework 檔,就可以支援多種平台。
Read More
Photo by Fabian Gieske on Unsplash
Read More

SwiftUI @State & @Binding 教學

SwiftUI 推出了兩個 Property Wrapper – @State and @Binding。利用它們可以達到變數的 Two-way Binding 功能。也就是當變數的值改變時,它會重新被顯示。本章藉由製作一個 Custom View 來展示如何使用 @State 和 @Binding。
Read More
Photo by Svitlana on Unsplash
Read More

iOS:禁止螢幕截圖

基於一些理由,我們可能會想要禁止使用者對我們的 app 做螢幕截圖。然而,iOS 並沒有提供這樣的功能。所幸,我們可以利用 UITextField 來達到此效果。
Read More