SwiftUI @State 的進階解析
在之前的文章裡,我們認識了 SwiftUI @State 的基本用法,接下來讓我們認識更多關於 @State 的故事。
State 搭配 Int,Double,String,Bool,Array 等基本的 value type
- 搭配 Int
struct ContentView: View {
@State private var number = 1
var body: some View {
Button(action: {
number = Int.random(in: 1...6)
}, label: {
Image(systemName: "die.face.\(number).fill")
.resizable()
.scaledToFit()
})
}
}
- 搭配 Array
struct ContentView: View {
@State private var numbers = [Int]()
var body: some View {
VStack {
Button(action: {
numbers.insert(Int.random(in: 1...100), at: 0)
}, label: {
Image(systemName: "plus.circle.fill")
.resizable()
.frame(width: 100, height: 100)
})
List(numbers, id: \.self) { number in
Text("\(number)")
}
}
}
}
State 搭配複雜的型別,比方自訂的 struct 型別
比方撲克牌 Card 的定義如下
struct Card {
var suit: String
var rank: String
var isRed: Bool {
suit == "♥︎" || suit == "♦︎"
}
}
State 搭配 struct 型別 Card。
struct ContentView: View {
@State private var card = Card(suit: "♠︎", rank: "A")
var body: some View {
VStack {
Text(card.rank)
.foregroundColor(card.isRed ? .red : .black)
Text(card.suit)
.foregroundColor(card.isRed ? .red : .black)
Button(action: {
card.rank = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"].randomElement()!
}, label: {
Text("random rank")
})
Button(action: {
card.suit = ["♠︎", "♣︎", "♥︎", "♦︎"].randomElement()!
}, label: {
Text("random suit")
})
}
.font(.largeTitle)
}
}
點選 Random Rank 隨機牌的數字。
點選 Random suit 隨機牌的花色。
State 搭配 reference type, 比方自訂的 class 型別
通常 State 會搭配 value type,因為搭配 reference type 會產生很特別的結果,比方我們將剛剛例子的 Card 改以 class 定義。
class Card {
var suit: String
var rank: String
var isRed: Bool {
suit == "♥︎" || suit == "♦︎"
}
internal init(suit: String, rank: String) {
self.suit = suit
self.rank = rank
}
}
此時我們的夢想成真了。我們的牌永遠是黑桃 A,不管我們按了幾次 random rank 或 random suit。
當我們點選 random rank 或 random suit 時,card 的內容其實真的有改變,只是畫面沒有更新,所以我們沒看到。
為什麼畫面不會更新呢? 因為當 State 搭配的資料是 reference type 時,它儲存的是資料的記憶體位置。因此要資料的記憶體位置改變,它才會覺得資料有改變,此時才會更新畫面。
因此若我們修改剛剛的程式,在點選 button 時重新生成一個 Card 存入變數 card,即可讓 card 儲存的記憶體位置改變,觸發畫面更新。
struct ContentView: View {
@State private var card = Card(suit: "♠︎", rank: "A")
var body: some View {
VStack {
Text(card.rank)
.foregroundColor(card.isRed ? .red : .black)
Text(card.suit)
.foregroundColor(card.isRed ? .red : .black)
Button(action: {
card = Card(suit: card.suit, rank: ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"].randomElement()!)
}, label: {
Text("random rank")
})
Button(action: {
card = Card(suit: ["♠︎", "♣︎", "♥︎", "♦︎"].randomElement()!, rank: card.rank)
}, label: {
Text("random suit")
})
}
.font(.largeTitle)
}
}
實驗:
- 一開始顯示黑桃 A。
- 點選 random rank,rank 變成 6。
- 點選 random suit,suit 變成愛心。
利用剛剛的方法,當 State 搭配的 reference type 資料改變時,畫面也可以正常更新。不過此方法其實不太好,因為它需要重新生成物件,即使我們只是想改變物件的某個 property。
因此若資料是 reference type,比較建議的做法是搭配 @StateObject, @ObservedObject,@Published & ObservableObject,有興趣的朋友可進一步研究相關說明。
當 view 原本就在畫面上,view 重新產生時將維持 State 變數之前儲存的資料
struct PokerView: View {
@State private var pokerNumber = 1
var body: some View {
VStack {
Text("poker number \(pokerNumber)")
Button(action: {
pokerNumber = Int.random(in: 1...13)
}, label: {
Text("Random")
})
DiceView()
}
.font(.largeTitle) }
}struct DiceView: View {
@State private var diceNumber = 1
init() {
print("Create DiceView")
}
var body: some View {
VStack {
Text("dice number \(diceNumber)")
Button(action: {
diceNumber = Int.random(in: 1...6)
}, label: {
Text("Random")
})
}
.font(.largeTitle) }
}
當 pokerNumber 改變時,PokerView 的畫面會更新,它的 body 程式將執行,因此重新生成 DiceView,印出 Create DiceView。值得注意的,此時 DiceView 的 diceNumber 將維持之前的數值,而非初始值 1,因為 State 宣告的 diceNumber 存在其它的地方,並不是存在 DiceView 裡。
實驗:
- 一開始 poker & dice 都是 1。
- 點選 random dice,dice 變成 5,poker 維持 1。
- 點選 random poker,poker 變成 7,dice 維持 5。
當 view 重新顯示到畫面上,state 儲存的資料將重新產生,不會維持之前的內容
剛剛的例子的 DiceView 重新產生時,state 儲存的資料可以維持之前的內容,因為 DiceView 原本就顯示在畫面上。不過若是 DiceView 是重新出現到畫面上,SwiftUI 將認為此時我們想要的是全新的資料,而非之前的資料,所以以下例子 DiceView 的 diceNumber 都會被初始為 1,不會維持之前的內容。
- 例子 1: 利用 if 控制 DiceView 的顯示
struct PokerView: View {
@State private var pokerNumber = 1
@State private var showDiceView = false var body: some View {
VStack {
Text("poker number \(pokerNumber)")
Button(action: {
pokerNumber = Int.random(in: 1...13)
}, label: {
Text("Random Poker")
})
Button(action: {
showDiceView.toggle()
}, label: {
Text("\(showDiceView ? "Hide" : "Show") Dice")
})
if showDiceView {
DiceView()
.onTapGesture {
showDiceView = false
}
}
}
.font(.largeTitle)
}
}
實驗:
- 點選 Show Dice,顯示 DiceView,此時 dice number 為 1。
- 點選 Random Dice,此時 dice number 為 5。
- 點選 Hide Dice,
4. 點選 Show Dice,再次顯示 DiceView,此時 dice number 為 1,而非之前的 5。
- 例子 2: 利用 sheet 控制 DiceView 的顯示
struct PokerView: View {
@State private var pokerNumber = 1
@State private var showDiceView = false var body: some View {
VStack {
Text("poker number \(pokerNumber)")
Button(action: {
pokerNumber = Int.random(in: 1...13)
}, label: {
Text("Random Poker")
})
Button(action: {
showDiceView.toggle()
}, label: {
Text("\(showDiceView ? "Hide" : "Show") Dice")
})
}
.font(.largeTitle)
.sheet(isPresented: $showDiceView, content: {
DiceView()
})
}
}
實驗:
- 點選 Show Dice,顯示 DiceView,此時 dice number 為 1。
- 點選 Random Dice,此時 dice number 為 3。
- 將 Dice View 往下拖曳關閉。
4. 點選 Show Dice,再次顯示 DiceView,此時 dice number 為 1,而非之前的 3。