狀態管理(state management)是每個開發者在應用程式開發中必須處理的事情。想像一下,你正在開發一個音樂播放器 App,當使用者點擊「播放」按鈕時,該按鈕會變為「停止」按鈕。在你的實作中,必須有一些方式來追蹤應用程式狀態,以讓你知道何時變更按鈕的外觀。
在 SwiftUI 中,內建了一些狀態管理的功能,特別是它導入了一個名為「@State」的屬性包裝器( Property Wrapper )。當你使用 @State
來標註一個屬性時,SwiftUI 會自動將其儲存在你的應用程式中的某處。此外,使用該屬性的視圖會自動監聽屬性值的變更,當狀態改變時,SwiftUI 將重新計算這些視圖,並更新應用程式的外觀。
聽起來不錯,不是嗎?還是你對於狀態管理覺得困惑?
總之,透過本章的範例程式碼,你將對狀態與綁定有更多的了解。而且,我為你準備了一些作業,請花一點時間來練習一下,這將幫助你掌握 SwiftUI 的重要觀念。
我們從剛才提到的簡單範例來開始,以了解如何透過追蹤應用程式的狀態,來切換「播放」與「停止」按鈕。首先,開啟Xcode 並使用「App」模板,來建立一個新專案。設定專案名稱為「SwiftUIState」,不過你可以自由使用其他的名稱,而你只需確保已選取「SwiftUI」選項,如圖 7 .2 所示。
當你儲存專案後,Xcode 應該要載入 ContentView.swift
檔,並且在設計畫布中顯示一個預覽。現在我們建立「播放」按鈕,如下所示:
Button {
// 在「播放」與「停止」按鈕之間切換
} label: {
Image(systemName: "play.circle.fill")
.font(.system(size: 150))
.foregroundColor(.green)
}
我們使用系統圖片,並將按鈕塗成綠色,如圖 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 所示。
你是否注意到,當你在按鈕之間切換時,SwiftUI 會渲染一個淡入淡出動畫?這個動畫是內建且自動為你產生的。我們將在後續的章節中討論更多有關動畫的內容。不過,如你所見,SwiftUI 讓所有的開發者對UI 動畫可立即上手。
你的作業是建立一個計數器按鈕,以顯示點擊次數。當使用者點擊按鈕時,該計數器會自動增加數字,並顯示點擊總次數,如圖 7.5 所示。
你是否能夠建立計數器按鈕呢?這裡我們不將布林變數宣告為狀態,而是使用一個整數狀態變數來追蹤計數。當點擊按鈕時,這個計數器會增加 1。圖 7.6 的程式碼片段可供你參考。
好的,現在我們進一步修改程式碼,以顯示三個計數器按鈕,如圖 7.7 所示。這三個按鈕都共享相同的計數器,不論哪一個按鈕被點擊,該計數器將會增加 1,所有的按鈕會同時一起顯示更新後的計數。
如你所見,所有的按鈕共享相同的外觀。就如我在前面章節內容所說明的,與其複製程式碼,較好的作法是取出一個共用視圖作為可重複使用的子視圖。因此,我們可以取出 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
狀態。
那麼 $
符號是什麼呢? 在 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 所示。
目前,所有的按鈕共享相同的計數,而本作業需要修改程式碼,以使每個按鈕都有其計數器。例如:當使用者點擊藍色按鈕,則該 App 中只有藍色按鈕的計數器會加 1。除此之外,你需要提供一個主計數器,以計算所有按鈕。圖 7.10 為本作業的示範佈局。
在 SwiftUI 中,狀態的支援可簡化應用程式開發中的狀態管理。了解什麼是 @State
與 @Binding
是非常重要的,因為它們對於在 SwiftUI 中做「狀態管理」與「UI 更新」而言, 發揮了很大的作用。本章介紹了 SwiftUI 中狀態管理的基礎概念,之後你將學習到更多有關如何在視圖動畫應用 @State
,以及如何管理多個視圖之間的共享狀態。
在本章所準備的範例檔中,有完整的專案與作業解答可以下載: