(SwiftUI) @State property @Binding @StateObject @ObservedObject @EnvironmentObject 學習筆記

Photo by Christopher Gower on Unsplash

@State property

先來設計一個簡單的畫面

import SwiftUI

struct ContentView: View {

var body: some View {

VStack {
Image(systemName: "heart")
.resizable()
.scaledToFit()

}
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

簡單改變一下顯示的圖案,只要將 Image(systemName: “heart”) 中的 heart 改成 heart.fill 即可改變顯示圖示

也許我們可以新增一個變數 isFill 來控制要顯示的 heart 為哪種圖示

import SwiftUI

struct ContentView: View {

// 控制顯示的 heart 圖示
var isFill = true

var body: some View {

VStack {

if isFill == true {
Image(systemName: "heart.fill")
.resizable()
.scaledToFit()
}else {
Image(systemName: "heart")
.resizable()
.scaledToFit()
}
}
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
左:(isFill = true)右:(isFill = false)

也許我們可以讓程式變得更加簡單,可以將 if else 改成 ? : 寫法

先來改變一下程式碼

import SwiftUI

struct ContentView: View {

// 控制顯示的 heart 圖示
var isFill = true

var body: some View {

VStack {
Image(systemName: isFill ? "heart.fill" : "heart")
.resizable()
.scaledToFit()

}
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

當 isFill 為 true 時,我們的 code 其實會變成以下的程式碼

Image(systemName: "heart.fill")

當 isFill 為 false 時,我們的 code 其實會變成以下的程式碼

Image(systemName: "heart")

isFill ? “heart.fill” : “heart”

其實蠻好理解的,如果今天判斷為 true 就回傳 : 的左邊的值,若為 false 就回傳 : 右邊的值

新增一個按鈕,來改變 heart 的狀態

import SwiftUI

struct ContentView: View {

// 控制顯示的 heart 圖示
var isFill = true

var body: some View {

VStack {
Image(systemName: isFill ? "heart.fill" : "heart")
.resizable()
.scaledToFit()

Button {
// 點選 button 時,觸發的事件
isFill.toggle()
} label: {
Text("press")
}
}
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

先來看一下,點選 button 所觸發的事件

            Button {
// 點選 button 時,觸發的事件
isFill.toggle()
} label: {
Text("press")
}

isFill.toggle() 這段程式碼是什麼意思呢,可以看下方程式碼來了解,簡單來說就是讓 true -> false false -> true

var bools = [true, false]

bools[0].toggle()
// bools == [false, false]

這樣也許我們就能在點選 button 時,切換 heartheart.fill 兩個圖示

但往往事情並不是那麼美好,我們會看到一個錯誤

在 SwiftUI 通常以 struct 來定義,在 struct 中要改變 property 必須在 var isFill 前面加入 @State

    @State var isFill = true

當然我們可以讓程式變得更加安全,在 state 加上 private

@State private var isFill = true

若想了解更多 private

完整程式碼

import SwiftUI

struct ContentView: View {

// 控制顯示的 heart 圖示
@State private var isFill = true

var body: some View {

VStack {
Image(systemName: isFill ? "heart.fill" : "heart")
.resizable()
.scaledToFit()

Button {
// 點選 button 時,觸發的事件
isFill.toggle()
} label: {
Text("press")
}
}
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

執行看看程式

@Binding

接下來我們來客製化一個播放按鈕,這個按鈕功能很簡單,只要點選它自己,button 就會切換成 play.circle (播放圖示)或 pause.circle(暫停圖示)

import SwiftUI

struct PlayerButton: View {

@State var play = true

var body: some View {
VStack {

Button {
play.toggle()
}label: {
Image(systemName: play ? "play.circle" : "pause.circle")
}
.font(.title)
}
}
}

struct PlayerButton_Previews: PreviewProvider {
static var previews: some View {
PlayerButton(play: true)
}
}

這在操控 button 顯示的圖片的 var play = true 前面不會加上 private,因為我希望這個 button 能在很多地方都能使用,並且是能夠改變它的狀態

接下來到一個新的 View 中,來呼叫看看我們剛剛所設計的 button 吧,在新的畫面,我希望能夠做到,點選 button 來切換下方兩張圖片

先來設計一下初始畫面吧

import SwiftUI

struct BindingView: View {

@State var play = false

var body: some View {

VStack {
Image(play ? "霞柱" : "暫停")
.resizable()
.scaledToFit()
// 將設計的 button 加入畫面
PlayerButton(play: false)

}

}
}

struct BindingView_Previews: PreviewProvider {
static var previews: some View {
BindingView()
}
}

程式執行後

可以發現我們點選 button 後,它是可以正常做切換 button 外觀的,但是我們今天的目標也想切換 button 上方的圖片到 霞柱,這時候我們來改變一下 PlayerButton 中的程式碼

將 @State 改成 @Binding 也就是說我們現在 play 的 Bool 數值,現在不是我們給的,而是參考別人的數值,不用給的話,當然我們在產生 play 時,只要宣告它的型別就好,可以看下方程式碼來理解

// @State 寫法
@State var play = true
// @Binding 寫法,參考別人數值,不需要給初始值,但是要告訴它型別
@Binding var play: Bool

這時候會跳出一個錯誤

現在要傳給 play 的並不是 Bool ,而是 Binding<Bool>,這時後我們在測試畫面,我們可以給定一個 Binding<Bool> ,產生方式如下

struct PlayerButton_Previews: PreviewProvider {
static var previews: some View {
PlayerButton(play: .constant(true))
}
}

.constant( Value) 就是回傳 Binding<Value>

回到我們主畫面 BindingView,這時候需要在 PlayerButton(play: 回傳值)回傳 Binding<Bool>,在這個主畫面中,我們所參考的對象就是 play

只需要在 play 前面加上 $ 就會變成 Binding<Bool>

PlayerButton(play: $play)

完整程式碼

import SwiftUI

struct BindingView: View {

@State var play = false

var body: some View {

VStack {
Image(play ? "霞柱" : "暫停")
.resizable()
.scaledToFit()
// 將設計的 button 加入畫面
PlayerButton(play: $play)

}

}
}

struct BindingView_Previews: PreviewProvider {
static var previews: some View {
BindingView()
}
}

用圖片來幫助理解

在 BindingView 中,我們會有一個 play = false,而在 PlayerButton 中,我們也會有一個 play ,但是這個 play 是 Binding ,也就是說它不需要初始值,而它的數值是參考別人數值所產生的

當我們在 BindingView 中新增 PlayerButton(play: $play) ,也就是將 PlayerButton 中的 play 參考 BindingView 中的 play

在 PlayerButton 中,還記得有一個功能嗎?也就是點選 button 時,它會改變 play 的 Bool 數值,true -> false false -> true

struct PlayerButton: View {

@Binding var play: Bool

var body: some View {
VStack {

Button {
play.toggle()
}label: {
Image(systemName: play ? "play.circle" : "pause.circle")
}
.font(.title)
}
}
}

也就是說今天我們在 BindingView 畫面中,按下 PlayerButton 時,也會同時改變畫面中綁定的 play 的 Bool 值 false -> true

@StateObject

我們再次設計一個新的畫面

import SwiftUI

struct LoveView: View {

@State var isSingle = true
@State var age = 19

var body: some View {
VStack(spacing: 30) {
HStack {
Spacer()

Image("霞柱")
.resizable()
.scaledToFill()
.frame(width: 200, height: 200)
.clipShape(Circle())

Spacer()

VStack(spacing: 50) {
Text("時透無一郎")
Text("歲數:\(age)")
Image(systemName: isSingle ? "heart" : "heart.fill")
.font(.title)
}
Spacer()

}

Button {
isSingle.toggle()
} label: {
Text("改變感情狀態")
}


Button {
age = age + 1
} label: {
Text("增加歲數")
}


}
}
}

struct LoveView_Previews: PreviewProvider {
static var previews: some View {
LoveView()
}
}

以上程式碼蠻好理解的,當按下 改變感情狀態 Button 時,isSingle 的 true 就會變成 false,也會同步改變顯示的 heart -> heart.fill, 按下 增加歲數 Button 時,age 就會增加 1 歲,執行結果如下

在未來程式中,也許我們會有更多的 property 要寫,若有其他的 property 就以此類推新增下去

@State var isSingle = true
@State var age = 19
@State var name = "Jason"
@State var height = 170
@State var weight = 55
.
.

上面的寫法,會讓程式碼變得很雜亂,所以我們需要一個自定義一個資料型別,將我們的 property 都包起來

新增一個 Swift File

輸入以下程式碼

import Foundation

class Lover: ObservableObject {
var isSingle = true
var age = 19
}

只有 class 能遵從 ObservableObject(可觀察的物件), struct 不能

在這的 property 會在前面加上 @Published

import Foundation

class Lover: ObservableObject {
@Published var isSingle = true
@Published var age = 19
}

當 property 數值改變時,就會 publish (發布) 觀察 Lover view 更新畫面

回到 LoveView 進行改寫程式碼,這次我們使用自定義型別時,我們在產生 Lover() 時,最前面我們會加上 @StateObject

程式碼

import SwiftUI

struct LoveView: View {

@StateObject var lover = Lover()

var body: some View {
VStack(spacing: 30) {
HStack {
Spacer()

Image("霞柱")
.resizable()
.scaledToFill()
.frame(width: 200, height: 200)
.clipShape(Circle())

Spacer()

VStack(spacing: 50) {
Text("時透無一郎")
Text("歲數:\(lover.age)")
Image(systemName: lover.isSingle ? "heart" : "heart.fill")
.font(.title)
}
Spacer()

}

Button {
lover.isSingle.toggle()
} label: {
Text("改變感情狀態")
}


Button {
lover.age = lover.age + 1
} label: {
Text("增加歲數")
}


}
}
}

struct LoveView_Previews: PreviewProvider {
static var previews: some View {
LoveView()
}
}

程式執行結果

新增下一頁面 ShowDetail,在這一頁會顯示 時透無一郎 的年齡

import SwiftUI

struct ShowDetail: View {

@ObservedObject var lover: Lover

var body: some View {

VStack {
Text("年齡")
.font(.largeTitle)
Image(systemName: "\(lover.age).circle")
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)

Stepper(value: $lover.age) {
Text("Age")
}


}
}

}

struct ShowDetail_Previews: PreviewProvider {
static var previews: some View {
ShowDetail(lover: Lover())
}
}

執行結果

在 ShowDetail 中,我們產生的 lover 跟前面 Binding 的概念很相似,我們並不需要產生出一個新的 Lover ,我們只需要去參考 LoveView 中的 lover 資料即可,所以我會在 ShowDetail 頁面中的 lover 前面加上 @ObservedObject

struct ShowDetail: View {
// 不需要產生 Lover(), 只需要給它型別即可
@ObservedObject var lover: Lover
struct LoveView: View {

@StateObject var lover = Lover()

主頁的 LoveView 怎麼修改

import SwiftUI

struct LoveView: View {

@StateObject var lover = Lover()
@State var showBool = false


var body: some View {
VStack(spacing: 30) {
HStack {
Spacer()

Image("霞柱")
.resizable()
.scaledToFill()
.frame(width: 200, height: 200)
.clipShape(Circle())

Spacer()

VStack(spacing: 50) {
Text("時透無一郎")
Text("歲數:\(lover.age)")
Image(systemName: lover.isSingle ? "heart" : "heart.fill")
.font(.title)
}
Spacer()

}

Button {
lover.isSingle.toggle()
} label: {
Text("改變感情狀態")
}


Button {
lover.age = lover.age + 1
} label: {
Text("增加歲數")
}

Button {
showBool.toggle()
} label: {
Text("nextpage")
// isPresented 傳入 true 時,跳轉一頁
}.sheet(isPresented: $showBool) {
ShowDetail(lover: lover)
}


}
}
}

struct LoveView_Previews: PreviewProvider {
static var previews: some View {
LoveView()
}
}

在最底下我會新增到下一頁的 Button nextpage,同時也會新增一個 showBool 來控制是否觸發跳轉下一頁

            Button {
showBool.toggle()
} label: {
Text("nextpage")
// isPresented 傳入 true 時,跳轉一頁
}.sheet(isPresented: $showBool) {
// 跳轉 ShowDetail
ShowDetail(lover: lover)
}

執行結果

用圖片來幫助理解

在 LoveView 中,我們會先產生出一個 var lover = Lover()

struct LoveView: View {

@StateObject var lover = Lover()

在這個 lover 中,包含了以下的 property

class Lover: ObservableObject {
@Published var isSingle = true
@Published var age = 19
}

今天我想在 另一個頁面 ShowDetial 中,也能夠拿到同一份資料(LoveView 中的 lover),這時後必須在 ShowDetial 中,產生一個 lover,但是這個 lover 是參考別人的資料,所以只需要告訴它,它的資料型態

struct ShowDetail: View {

@ObservedObject var lover: Lover

這跟 Binding 的概念相同,可以對比上方來查看

今天你在 LoveView 中修改了年齡,由於在 ShowDetial 也是參考同一份資料,所以在 ShowDetial 也會拿到同份資料

反之你在 ShowDetail 修改了年齡,也會修改到同一份來自 LoveView 中的資料

@EnvironmentObject

新增一個的 EnvironmentView

import SwiftUI

struct EnvironmentView: View {

@StateObject var lover = Lover()
@State var showBool = false

var body: some View {
VStack {
Image(systemName: "\(lover.age).circle")
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)
Stepper(value: $lover.age) {
Text("Age")

}
Button {
showBool.toggle()
} label: {
Text("nextpage")
// isPresented 傳入 true 時,跳轉一頁
}.sheet(isPresented: $showBool) {
// 跳轉 ShowDetail
Page1View(lover: lover)
}

}
}

}

struct EnvironmentView_Previews: PreviewProvider {
static var previews: some View {
EnvironmentView(lover: Lover())
}
}

新增一個的 Page1View

import SwiftUI

struct Page1View: View {

@ObservedObject var lover: Lover
@State var showBool = false

var body: some View {
VStack {
Text("Page1")
.font(.largeTitle)
Image(systemName: "\(lover.age).circle")
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)

Stepper(value: $lover.age) {
Text("Age")
}
Button {
showBool.toggle()
} label: {
Text("nextpage")
// isPresented 傳入 true 時,跳轉一頁
}.sheet(isPresented: $showBool) {
// 跳轉 ShowDetail
Page2View(lover: lover)
}

}
}
}

struct Page1View_Previews: PreviewProvider {
static var previews: some View {
Page1View(lover: Lover())
}
}

新增一個的 Page2View

import SwiftUI

struct Page2View: View {

@ObservedObject var lover: Lover


var body: some View {
VStack {
Text("Page2")
.font(.largeTitle)
Image(systemName: "\(lover.age).circle")
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)

Stepper(value: $lover.age) {
Text("Age")
}


}
}
}

struct Page2View_Previews: PreviewProvider {
static var previews: some View {
Page2View(lover: Lover())
}
}

執行結果

跟剛剛的概念其實相同,剛剛新增的三個頁面都吃同一份資料

但是這裡會發現有個問題,也就是我們要不斷把第一頁的 lover 丟到下一頁

也許有更簡單的寫法,使用 EnvironmentObject

EnvironmentView 中的 VStack 底下新增以下程式碼

struct EnvironmentView: View {

@StateObject var lover = Lover()

var body: some View {
VStack {

}
.environmentObject(lover)

Page1View Page2View 中的 ObservedObject 改成 EnvironmentObject

struct Page1View: View {

@EnvironmentObject var lover: Lover
struct Page2View: View {

@EnvironmentObject var lover: Lover

這時候在產生 Page1View Page2View 時,就不需要將 lover 丟入其中

            Button {
showBool.toggle()
} label: {
Text("nextpage")
// isPresented 傳入 true 時,跳轉一頁
}.sheet(isPresented: $showBool) {
// 跳轉 Page1View
Page1View()
}
// 原本需要丟入 lover
Page1View(lover: lover)
// 現在不需要丟入 lover
Page1View()
// 原本需要丟入 lover
Page2View(lover: lover)
// 現在不需要丟入 lover
Page2View()

這時候其實會出現錯誤,原因在於 preview 也要呼叫 environmentObject

struct Page1View_Previews: PreviewProvider {
static var previews: some View {
Page1View()
.environmentObject(Lover())
}
}
struct Page2View_Previews: PreviewProvider {
static var previews: some View {
Page2View()
.environmentObject(Lover())
}
}

完整程式碼

EnvironmentView

import SwiftUI

struct EnvironmentView: View {

@StateObject var lover = Lover()
@State var showBool = false

var body: some View {
VStack {
Image(systemName: "\(lover.age).circle")
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)
Stepper(value: $lover.age) {
Text("Age")

}
Button {
showBool.toggle()
} label: {
Text("nextpage")
// isPresented 傳入 true 時,跳轉一頁
}.sheet(isPresented: $showBool) {
// 跳轉 Page1View
Page1View()
}

}
.environmentObject(lover)
}

}

struct EnvironmentView_Previews: PreviewProvider {
static var previews: some View {
EnvironmentView(lover: Lover())
}
}

Page1View

import SwiftUI

struct Page1View: View {

@EnvironmentObject var lover: Lover
@State var showBool = false

var body: some View {
VStack {
Text("Page1")
.font(.largeTitle)
Image(systemName: "\(lover.age).circle")
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)

Stepper(value: $lover.age) {
Text("Age")
}
Button {
showBool.toggle()
} label: {
Text("nextpage")
// isPresented 傳入 true 時,跳轉一頁
}.sheet(isPresented: $showBool) {
// 跳轉 ShowDetail
Page2View()
}

}
}
}

struct Page1View_Previews: PreviewProvider {
static var previews: some View {
Page1View()
.environmentObject(Lover())
}
}

Page2View

import SwiftUI

struct Page2View: View {

@EnvironmentObject var lover: Lover


var body: some View {
VStack {
Text("Page2")
.font(.largeTitle)
Image(systemName: "\(lover.age).circle")
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)

Stepper(value: $lover.age) {
Text("Age")
}


}
}
}

struct Page2View_Previews: PreviewProvider {
static var previews: some View {
Page2View()
.environmentObject(Lover())
}
}

執行結果

使用 EnvironmentObject 要注意的地方就是,需要在最初的畫面加入 .environmentObject(lover),以及每一個畫面的 Preview 都需要加上 .environmentObject(Lover()) 即可

GitHub

Reference

--

--