精通 SwiftUI - iOS 17 版

第 7 章
了解狀態與綁定

狀態管理(state management)是每個開發者在應用程式開發中必須處理的事情。想像一下,你正在開發一個音樂播放器 App,當使用者點擊「播放」按鈕時,該按鈕會變為「停止」按鈕。在你的實作中,必須有一些方式來追蹤應用程式狀態,以讓你知道何時變更按鈕的外觀。

圖 7.1. 「停止」與「播放」按鈕
圖 7.1. 「停止」與「播放」按鈕

在 SwiftUI 中,內建了一些狀態管理的功能,特別是它導入了一個名為「@State」的屬性包裝器( Property Wrapper )。當你使用 @State 來標註一個屬性時,SwiftUI 會自動將其儲存在你的應用程式中的某處。此外,使用該屬性的視圖會自動監聽屬性值的變更,當狀態改變時,SwiftUI 將重新計算這些視圖,並更新應用程式的外觀。

聽起來不錯,不是嗎?還是你對於狀態管理覺得困惑?

總之,透過本章的範例程式碼,你將對狀態與綁定有更多的了解。而且,我為你準備了一些作業,請花一點時間來練習一下,這將幫助你掌握 SwiftUI 的重要觀念。

啟用SwiftUI 建立新專案

我們從剛才提到的簡單範例來開始,以了解如何透過追蹤應用程式的狀態,來切換「播放」與「停止」按鈕。首先,開啟Xcode 並使用「App」模板,來建立一個新專案。設定專案名稱為「SwiftUIState」,不過你可以自由使用其他的名稱,而你只需確保已選取「SwiftUI」選項,如圖 7 .2 所示。

圖 7.2. 建立一個新專案
圖 7.2. 建立一個新專案

當你儲存專案後,Xcode 應該要載入 ContentView.swift 檔,並且在設計畫布中顯示一個預覽。現在我們建立「播放」按鈕,如下所示:

Button {
    // 在「播放」與「停止」按鈕之間切換
} label: {
    Image(systemName: "play.circle.fill")
        .font(.system(size: 150))
        .foregroundColor(.green)
}

我們使用系統圖片,並將按鈕塗成綠色,如圖 7.3 所示。

圖 7.3 預覽「播放」按鈕
圖 7.3 預覽「播放」按鈕

控制按鈕的狀態

按鈕的動作現在是空的,我們要做的是當使用者點擊按鈕時,將按鈕的外觀從「播放」改為「停止」。顯示「停止」按鈕時,按鈕的顏色也應變成紅色。

那麼,我們要如何實作呢?顯然的,我們需要一個變數來追蹤按鈕的狀態。我們將其命名為 isPlaying,它是一個布林變數,指示 App 是否處於「播放」狀態。如果將變數設定為「true」,則 App 應顯示一個「停止」按鈕;反之,App 顯示一個「播放」按鈕。程式碼如下所示:

struct ContentView: View {

    private var isPlaying = false

    var body: some View {
        Button {
            // 在「播放」與「停止」按鈕之間切換
        } label: {
            Image(systemName: isPlaying ? "stop.circle.fill" : "play.circle.fill")
            .font(.system(size: 150))
            .foregroundColor(isPlaying ? .red : .green)
        }

    }
}

我們參照 isPlaying 變數的值來變更圖片的名稱與顏色。如果更新在你的專案中的程式碼,則應該會在預覽畫布中看到一個「播放」按鈕。不過,若是你將 isPlaying 的預設值設定為 true,則會見到一個「停止」按鈕。

現在的問題是,App 如何監聽狀態(即 isPlaying )的變化,並自動更新按鈕呢?使用 SwiftUI,你需要做的是在 isPlaying 屬性前面加上 @State

@State private var isPlaying = false

當我們宣告屬性為一個狀態變數時,SwiftUI 就會管理isPlaying 的儲存區,並監聽其值的變化。當 isPlaying 的值更改時,SwiftUI 會參照 isPlaying狀態,來自動重新計算視圖。這裡的視圖指的是 Button

只能從視圖的 body(或者從被它呼叫的函數)內部存取一個狀態屬性。由於這個緣故,你應該宣告你的狀態屬性為 private,以防止你的視圖的用戶端存取它。

- Apple 的官方文件 (https://developer.apple.com/documentation/swiftui/state)

我們還沒有實作按鈕的動作。因此,修改程式碼如下:

Button {
    // 在「播放」與「停止」按鈕之間切換
    self.isPlaying.toggle()
} label: {
    Image(systemName: isPlaying ? "stop.circle.fill" : "play.circle.fill")
    .font(.system(size: 150))
    .foregroundColor(isPlaying ? .red : .green)
}

action 閉包(closure )中,我們呼叫 toggle() 方法來將布林值從 false 切換為 true,或者從 true 切換為 false。在預覽畫布中,試著在「播放」與「停止」按鈕之間切換,如圖 7.4 所示。

圖 7.4 在「播放」與「停止」按鈕之間切換
圖 7.4 在「播放」與「停止」按鈕之間切換

你是否注意到,當你在按鈕之間切換時,SwiftUI 會渲染一個淡入淡出動畫?這個動畫是內建且自動為你產生的。我們將在後續的章節中討論更多有關動畫的內容。不過,如你所見,SwiftUI 讓所有的開發者對UI 動畫可立即上手。

作業 #1

你的作業是建立一個計數器按鈕,以顯示點擊次數。當使用者點擊按鈕時,該計數器會自動增加數字,並顯示點擊總次數,如圖 7.5 所示。

圖 7.5. 計數器按鈕
圖 7.5. 計數器按鈕

使用綁定

你是否能夠建立計數器按鈕呢?這裡我們不將布林變數宣告為狀態,而是使用一個整數狀態變數來追蹤計數。當點擊按鈕時,這個計數器會增加 1。圖 7.6 的程式碼片段可供你參考。

圖 7.6. 計數器按鈕
圖 7.6. 計數器按鈕

好的,現在我們進一步修改程式碼,以顯示三個計數器按鈕,如圖 7.7 所示。這三個按鈕都共享相同的計數器,不論哪一個按鈕被點擊,該計數器將會增加 1,所有的按鈕會同時一起顯示更新後的計數。

圖 7.7. 三個計數按鈕
圖 7.7. 三個計數按鈕

如你所見,所有的按鈕共享相同的外觀。就如我在前面章節內容所說明的,與其複製程式碼,較好的作法是取出一個共用視圖作為可重複使用的子視圖。因此,我們可以取出 Button 來建立一個獨立視圖,如下所示:

struct CounterButton: View {
    @Binding var counter: Int

    var color: Color

    var body: some View {
        Button {
            counter += 1
        } label: {
            Circle()
                .frame(width: 200, height: 200)
                .foregroundStyle(color)
                .overlay {
                    Text("\(counter)")
                        .font(.system(size: 100, weight: .bold, design: .rounded))
                        .foregroundStyle(.white)
                }
        }
    }
}

CounterButton 視圖接收 「counter」 與 「color」 等兩個參數,你可以使用紅色來建立按鈕,如下所示:

CounterButton(counter: $counter, color: .red)

你應該會注意到 counter 變數以 @Binding 來做標註。當你建立一個 CounterButton 實例時,counter 會加上一個 $ 符號作為前綴。

這是什麼意思呢?

我們取出按鈕至獨立的視圖後,CounterButton 變成 ContentView 的子視圖。現在,計數器遞增是在CounterButton 視圖中完成的,而不是在 ContentView 中。CounterButton 必須在 ContentView 中有一個管理狀態變數的方式。

這個 @Binding 關鍵字指示呼叫者必須提供狀態變數的綁定。這樣做就如同建立了 ContentView 中的 counter 以及 CounterButton 中的 counter 之間的雙向連接。更新 CounterButton 視圖中的 counter,會將其值傳送回 ContentView 中的 counter 狀態。

圖 7.8 了解綁定
圖 7.8 了解綁定

那麼 $ 符號是什麼呢? 在 SwiftUI 中, 你使用 $ 前綴運算子從狀態變數取得綁定。

如果你了解綁定的原理,則可以繼續建立其他兩個按鈕,並使用 VStack 來垂直對齊, 如下所示:

struct ContentView: View {

    @State private var counter = 1

    var body: some View {
        VStack {
            CounterButton(counter: $counter, color: .blue)
            CounterButton(counter: $counter, color: .green)
            CounterButton(counter: $counter, color: .red)
        }
    }
}

變更完成後,你可以執行 App 並做測試。點擊任何一個按鈕,將使計數增加 1,如圖 7.9 所示。

圖 7.9. 測試三個計數器按鈕
圖 7.9. 測試三個計數器按鈕

作業 #2

目前,所有的按鈕共享相同的計數,而本作業需要修改程式碼,以使每個按鈕都有其計數器。例如:當使用者點擊藍色按鈕,則該 App 中只有藍色按鈕的計數器會加 1。除此之外,你需要提供一個主計數器,以計算所有按鈕。圖 7.10 為本作業的示範佈局。

圖 7.10. 每個按鈕都有其計數器
圖 7.10. 每個按鈕都有其計數器

本章小結

在 SwiftUI 中,狀態的支援可簡化應用程式開發中的狀態管理。了解什麼是 @State@Binding 是非常重要的,因為它們對於在 SwiftUI 中做「狀態管理」與「UI 更新」而言, 發揮了很大的作用。本章介紹了 SwiftUI 中狀態管理的基礎概念,之後你將學習到更多有關如何在視圖動畫應用 @State,以及如何管理多個視圖之間的共享狀態。

在本章所準備的範例檔中,有完整的專案與作業解答可以下載: