(SwiftUI) @State property @Binding @StateObject @ObservedObject @EnvironmentObject 學習筆記
@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()
}
}
也許我們可以讓程式變得更加簡單,可以將 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 時,切換 heart
及 heart.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()) 即可