這篇文章我們來看一下在 SwiftUI 中如何將數據作為依賴連接起來,同時保持 UI 的顯示是正確并可預測的卦停。這里主要講解 SwiftUI 中的五個數據流工具:Property
助泽、@State
奔滑、@Binding
妖谴、@ObjectBinding
和 @EnvironmentObject
簿煌。
數據流工具
Property
Property 是我們目前開發(fā)中最常見的磁携,它就是一個簡單的屬性褒侧,沒什么特別。例子:
struct ContentView : View {
var body: some View {
ChildView(text: "Demo")
}
}
struct ChildView: View {
let text: String
var body: some View {
Text(text)
}
}
ChildView
需要 Parent View 給它傳一個字符串,并且 ChildView
本省不需要對這個字符串進行修改闷供,所以直接定義一個 Property烟央,在使用的時候,直接讓 Parent View 告訴它就好了歪脏。
@State
我們先看一個官方給的錯誤例子:
struct PlayerView : View {
let episode: Episode
var isPlaying: Bool
var body: some View {
VStack {
Text(episode.title)
Text(episode.showTitle).font(.caption).foregroundColor(.gray)
Button(action: {
// 錯誤:Cannot use mutating member on immutable value: 'self' is immutable
self.isPlaying.toggle()
}) {
Image(systemName: isPlaying ? "pause.circle" : "play.circle")
}
}
}
}
上面的代碼中疑俭,我們想在 Button
被點擊后直接使用 self.isPlaying.toggle()
切換 isPlaying
的值,但這是不行的婿失,因為 PlayerView
是 struct 類型钞艇,self
是不可變的,并且 isPlaying
是一個普通的屬性豪硅。為了達到我們的需求哩照,@State
的作用就來了。我們把上面的代碼改成:
struct PlayerView : View {
let episode: Episode
@State private var isPlaying: Bool = false
var body: some View {
VStack {
Text(episode.title)
Text(episode.showTitle).font(.caption).foregroundColor(.gray)
Button(action: {
self.isPlaying.toggle()
}) {
Image(systemName: isPlaying ? "pause.circle" : "play.circle")
}
}
}
}
我們用 @State
標記 isPlaying
屬性舟误,這樣 isPlaying
就可以在 View 的內部被更改葡秒,并且被更改后,與 isPlaying
相關的 View 也會更新嵌溢,本例中 Image
就會在 pause.circle
和 play.circle
之間切換。
總結:@State
的作用是讓被它標記的屬性可以在 View 內部修改蹋岩,并且 View 也會重新渲染赖草。
@Binding
有時候我們想讓 Child View 修改 Parent View 傳給它的數據,并且數據修改后剪个,Parent View 重新渲染秧骑。這時我們就得用到 @Binding
。
我們把 @State
例子中的 Button
重構為 PlayButton
扣囊,代碼如下:
struct PlayerView : View {
let episode: Episode
@State private var isPlaying: Bool = false
var body: some View {
VStack {
Text(episode.title)
Text(episode.showTitle).font(.caption).foregroundColor(.gray)
PlayButton(isPlaying: $isPlaying)
}
}
}
struct PlayButton : View {
@Binding var isPlaying: Bool
var body: some View {
Button(action: {
self.isPlaying.toggle()
}) {
Image(systemName: isPlaying ? "pause.circle" : "play.circle")
}
}
}
在 PlayButton
中乎折,用 @Binding
標記 isPlaying
屬性,意味著可以對傳入的數據進行修改侵歇;在 PlayerView
使用時骂澄,傳入的屬性 isPlaying
需要有 $
前綴,并且被傳入屬性不能是普通的屬性惕虑,而要求是可讀可寫的屬性(被@State
/ @Binding
/ @ObjectBinding
標記)坟冲。
@Binding
在很多系統(tǒng)自帶的 View 中使用,如 Toggle
溃蔫、TextField
和 Slider
等等健提。
@ObjectBinding
其實在很多情況下,我們的數據來源于外部的數據模型伟叛。我們也想要在當外部數據發(fā)生變化時私痹,能及時更新我們的 UI。而 @ObjectBinding
就是為這種需求而設計的。
對于 @ObjectBinding
標記的屬性紊遵,它必須遵循 BindableObject
協(xié)議账千,這個協(xié)議的定義如下:
public protocol BindableObject : AnyObject, DynamicViewProperty, Identifiable, _BindableObjectViewProperty {
associatedtype PublisherType : Publisher where Self.PublisherType.Failure == Never
var didChange: Self.PublisherType { get }
}
Publisher
是與 SwiftUI 一起推出的響應式編程框架 Combine 的一個協(xié)議。所以想要熟練使用 BindableObject
癞蚕, 學習 Combine 是必不可少的蕊爵。
下面是 @ObjectBinding
的演示代碼:
class MyModelObject : BindableObject {
var didChange = PassthroughSubject<Void, Never>()
func changeData() {
// 修改數據
// ...
// 通知訂閱者數據發(fā)生變化
didChange.send()
}
}
struct MyView : View {
@ObjectBinding var model: MyModelObject
// ...
}
當調用 didChange.send()
之后,MyView
接收到通知桦山,View 就會重新渲染攒射。
@ EnvironmentObject
我們剛剛學習的 Property
和 @Binding
都只能從 Parent View 一層一層的往 Child View 傳遞。所以當我們的 View 層級關系比較復雜恒水、有些屬性只在很深層級的 View 才用到時会放,用 Property
和 @Binding
的方式就會非常麻煩。蘋果使用 @ EnvironmentObject
來解決這個問題钉凌。
我們先看一個 demo咧最,然后通過 demo 來講解 @ EnvironmentObject
的使用。
class MyModelObject : BindableObject {
var didChange = PassthroughSubject<Void, Never>()
var count = 0
func updateCount() {
count += 1
didChange.send()
}
}
struct ContentView : View {
var body: some View {
RootView().environmentObject(MyModelObject())
}
}
struct RootView: View {
var body: some View {
VStack(spacing: 20) {
ChildView1()
ChildView2()
}
}
}
struct ChildView1: View {
@EnvironmentObject var model: MyModelObject
var body: some View {
Button(action: {
self.model.updateCount()
}) {
Text("Button")
}
}
}
struct ChildView2: View {
@EnvironmentObject var model: MyModelObject
var body: some View {
Text("\(model.count)")
}
}
RootView
包含了 ChildView1
和 ChildView2
御雕,兩個 Child View 都持有被 @EnvironmentObject
標記的 MyModelObject
類型的屬性矢沿,當 ChildView1
的按鈕被點擊時,MyModelObject
的數據被更新酸纲,ChildView2
的 View 重新渲染捣鲸。整個過程中兩個 Child View 沒有從 RootView
中直接接受參數,只有 RootView
在初始化的時闽坡,通過 environmentObject()
方法把 MyModelObject
注入到整個 View 層級中栽惶,這個層級中所有的 View 都可以通過 @Environment
的方式訪問 MyModelObject
。需要注意的一點是疾嗅,使用 environmentObject()
注入的對象必須是 BindableObject
類型外厂。
五個數據流工具總結
- Property:當 View 所需要的屬性只要求可讀,則使用 Property代承。
-
@State: 當 View 所需要的屬性只在當前 View 和它的 Child Views 中使用汁蝶,并且在用戶的操作過程中會發(fā)生變化,然后導致 View 需要作出改變次泽,那么使用
@State
穿仪。 因為只在當前 View 和它的 Child Views 中使用,跟外界無關意荤,所以被@State
標記的屬性一般在定義時就有初始值啊片。 -
@Binding:當 View 所需要的屬性是從它的直接 Parent View 傳入,在內部會對這個屬性進行修改玖像,并且修改后的值需要反饋給直接 Parent View紫谷,那么使用
@Binding
齐饮。 - @ObjectBinding:用于直接綁定外部的數據模型和 View。
-
@EnvironmentObject:Root View 通過
environmentObject()
把BindableObject
注入到 View 層級中笤昨,其中的所有 Child Views 可以通過@EnvironmentObject
來訪問被注入的BindableObject
祖驱。
接收其他外部變化
有時我們的 View 需要監(jiān)聽外部的其他變化,并做出相應的改變瞒窒,可以使用 receive(on:)
捺僻,這里面的 closure 參數是在主線程執(zhí)行的。
以下是官方的 Demo 代碼:
struct PlayerView : View {
let episode: Episode
@State private var isPlaying: Bool = true
@State private var currentTime: TimeInterval = 0.0
var body: some View {
VStack {
// ...
Text("\(playhead, formatter: currentTimeFormatter)")
}
.onReceive(PodcastPlayer.currentTimePublisher) { newCurrentTime in
self.currentTime = newCurrentTime
}
}
}
總結
數據在整個 App 中是非常重要的一部分崇裁,在使用上面講到的工具之前匕坯,先仔細研究自己的數據結構,然后選擇合適的工具拔稳,把數據注入到 UI 中葛峻。
完
想要更詳細了解文章的內容,可以點擊查看下面的視頻巴比。想及時看到我的新文章的术奖,可以關注我。
參考資料
Data Flow Through SwiftUI - WWDC 2019 - Videos - Apple Developer