用狀態設計 SwiftUI 畫面 — 認識 State property

SwiftUI 用狀態決定如何顯示和更新畫面,它的很多 UI 元件都跟狀態有關,幫助我們開發 App 時更容易實現資料和畫面的同步。接下來我們就以下雨時顯示孫燕姿的壞天氣,放晴時顯示盧廣仲的我愛你歌詞為例,說明狀態如何深深影響著 SwiftUI 的畫面。

設計 App 畫面

struct ContentView: View {
var isRain = true

var body: some View {
VStack {
if isRain {
Image(systemName: "cloud.rain.fill")
.resizable()
.frame(width: 100, height: 100)
Text("我們淋著大雨不知何時才能放晴")
} else {
Image(systemName: "sun.max.fill")
.resizable()
.frame(width: 100, height: 100)
Text("太陽公公出來了,他對我呀笑呀笑")
}
Button("今天天氣如何 ?") {

}
}
}
}

說明。

  • 宣告型別 Bool 的變數 isRain 判斷是否下雨,利用它決定要顯示的圖片和文字。
  • 點擊 button 將決定今天是否下雨。

將 if else 改成 ? : (ternary conditional operator)

實作點擊 button 決定是否下雨前,我們先將程式改成 ? : 的寫法。SwiftUI 的程式經常使用 ?: 來讓程式變得更精簡。

struct ContentView: View {
var isRain = true

var body: some View {
VStack {
Image(systemName: isRain ? "cloud.rain.fill" : "sun.max.fill")
.resizable()
.frame(width: 100, height: 100)
Text(isRain ? "我們淋著大雨不知何時才能放晴" : "太陽公公出來了,他對我呀笑呀笑")
Button("今天天氣如何 ?") {

}
}
}
}

透過 random 決定是否下雨

在 button 點選執行的 closure 裡透過 random 決定是否下雨。

Button("今天天氣如何 ?") {
isRain = .random()
}

此時將馬上產生紅色錯誤,Cannot assign to property: self is immutable。

為什麼會錯呢 ? 因為 SwiftUI 的 view 通常以 struct 定義,就像剛剛的 struct ContentView。而 struct 是 value type,因此我們無法在 computed property 的 getter & function 裡修改 stored property。就像以下的 Baby 以 struct 定義,function getOlder 修改 property age 將產生類似的錯誤。(註: 不過我們可在加了 mutating 的 function 和 computed property 的 setter 修改 stored property。)

以 @State 宣告可改變的 property

在 property isRain 前加上 @State ,即可修正剛剛的錯誤。(註: 一般我們宣告 @State property 時會再加上 private,讓程式更安全,表示我們只會在定義它的型別裡存取它。)

@State private var isRain = true

因此在 SwiftUI view 型別的定義裡,若想讓它的 property 能夠改變,不只要以 var 宣告成變數,還要記得加上 @State

加了 @State 後,除了變數可以改變,還會有神奇的副作用,每當我們點選 button,觸發 button 的 closure 程式修改 isRain 時,畫面也會更新顯示目前的天氣內容。

當我們在 ContentView 的 property 前加上 @State 時,SwiftUI 將認為此 property 代表某種影響畫面內容的狀態。當狀態改變時,畫面將馬上更新,重新生成 body 裡受 state property 影響的元件。

@State private var isRain = true 表示是否下雨的狀態,當我們在 button 點選時透過 isRain = Bool.random() 修改它時, computed property body 的程式將重新執行產生新的畫面,顯示晴天或雨天的圖片文字。

以 @State 宣告的 property 有個重要的特性,只要它的內容改變,畫面也會立即更新。它帶來了以下兩個好處。

  • 不用另外寫 property 內容改變時更新畫面的程式。
  • 不用擔心畫面顯示的內容跟 property 的內容不同步,比方修改了 property,但卻忘了更新畫面。

也許有人好奇,以 struct 定義的 ContentView 是 value type,為何加了 @State 就能修改它的 property ? 這是因為加了 @State 後,SwiftUI 將在背後另外產生空間儲存 property 的內容,它不再儲存在 ContentView 裡,因此我們可以修改它的內容。

isRain 儲存在獨立的空間,不屬於 ContentView

ps: @State 實現的機制跟 Swift 的 property wrapper 有關,有興趣的朋友也可參考以下連結的說明。

完整程式

struct ContentView: View {
@State private var isRain = true

var body: some View {
VStack {
Image(systemName: isRain ? "cloud.rain.fill" : "sun.max.fill")
.resizable()
.frame(width: 100, height: 100)
Text(isRain ? "我們淋著大雨不知何時才能放晴" : "太陽公公出來了,他對我呀笑呀笑")

Button("今天天氣如何 ?") {
isRain = .random()
}
}
}
}

現在可以開始測試 App 了。當我們點選今天天氣如何,isRain 將隨機變成 true 或 false,當它為 true 時畫面將顯示雨天,false 時畫面將顯示晴天。

以狀態設計 SwiftUI App

了解 SwiftUI state 的作用後,我們未來設計 SwiftUI 畫面時,可以試著多用狀態的角度設計畫面,思考畫面會有哪些狀態以及每個狀態呈現的模樣,將內容影響畫面的變數常數加上 @State,讓資料和畫面自動保持同步。

練習: 結合圖片、文字、button 的 SwiftUI State 練習題

視情況聰明更新的 SwiftUI body

也許有人會擔心,當畫面比較複雜時,property body 裡將生成很多 UI 元件,如果常常更新重新執行 body 裡的程式,豈不是會大大影響 App 的效能 ?

其實 SwiftUI 是很聰明的,它會判斷此次更新將影響哪些元件,只會重新產生那些受影響的元件,相關說明可參考以下連結。

SwiftUI @State 的進階解析

影響 SwiftUI 畫面更新的 Equatable & function ==

SwiftUI 參考資料來源的 Binding

SwiftUI 綁定資料的 Binding 元件

參考連結

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com