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)
}
}

實驗:

  1. 一開始顯示黑桃 A。
  2. 點選 random rank,rank 變成 6。
  3. 點選 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 裡。

實驗:

  1. 一開始 poker & dice 都是 1。
  2. 點選 random dice,dice 變成 5,poker 維持 1。
  3. 點選 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)
}
}

實驗:

  1. 點選 Show Dice,顯示 DiceView,此時 dice number 為 1。
  2. 點選 Random Dice,此時 dice number 為 5。
  3. 點選 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()
})

}
}

實驗:

  1. 點選 Show Dice,顯示 DiceView,此時 dice number 為 1。
  2. 點選 Random Dice,此時 dice number 為 3。
  3. 將 Dice View 往下拖曳關閉。

4. 點選 Show Dice,再次顯示 DiceView,此時 dice number 為 1,而非之前的 3。

--

--

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

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