【W(wǎng)WDC2019 之 SwiftUI】03 - SwiftUI 的數(shù)據(jù)流

這篇文章我們來看一下在 SwiftUI 中如何將數(shù)據(jù)作為依賴連接起來,同時保持 UI 的顯示是正確并可預測的。這里主要講解 SwiftUI 中的五個數(shù)據(jù)流工具:Property@State、@Binding、@ObjectBinding@EnvironmentObject。

數(shù)據(jù)流工具

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 的內(nèi)部被更改,并且被更改后,與 isPlaying 相關(guān)的 View 也會更新,本例中 Image 就會在 pause.circleplay.circle 之間切換。

總結(jié):@State 的作用是讓被它標記的屬性可以在 View 內(nèi)部修改,并且 View 也會重新渲染。

@Binding

有時候我們想讓 Child View 修改 Parent View 傳給它的數(shù)據(jù),并且數(shù)據(jù)修改后,Parent View 重新渲染。這時我們就得用到 @Binding。

我們把 @State 例子中的 Button 重構(gòu)為 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 屬性,意味著可以對傳入的數(shù)據(jù)進行修改;在 PlayerView 使用時,傳入的屬性 isPlaying 需要有 $ 前綴,并且被傳入屬性不能是普通的屬性,而要求是可讀可寫的屬性(被@State / @Binding / @ObjectBinding 標記)。

@Binding 在很多系統(tǒng)自帶的 View 中使用,如 Toggle、TextFieldSlider 等等。

@ObjectBinding

其實在很多情況下,我們的數(shù)據(jù)來源于外部的數(shù)據(jù)模型。我們也想要在當外部數(shù)據(jù)發(fā)生變化時,能及時更新我們的 UI。而 @ObjectBinding 就是為這種需求而設(shè)計的。

對于 @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 一起推出的響應(yīng)式編程框架 Combine 的一個協(xié)議。所以想要熟練使用 BindableObject, 學習 Combine 是必不可少的。

下面是 @ObjectBinding 的演示代碼:

class MyModelObject : BindableObject {
    var didChange = PassthroughSubject<Void, Never>()
    
    func changeData() {
        // 修改數(shù)據(jù)
        // ...
        
        // 通知訂閱者數(shù)據(jù)發(fā)生變化
        didChange.send()
    }
}

struct MyView : View {
    @ObjectBinding var model: MyModelObject
    
    // ...
}

當調(diào)用 didChange.send() 之后,MyView 接收到通知,View 就會重新渲染。

@ EnvironmentObject

我們剛剛學習的 Property@Binding 都只能從 Parent View 一層一層的往 Child View 傳遞。所以當我們的 View 層級關(guān)系比較復雜、有些屬性只在很深層級的 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 包含了 ChildView1ChildView2,兩個 Child View 都持有被 @EnvironmentObject 標記的 MyModelObject 類型的屬性,當 ChildView1 的按鈕被點擊時,MyModelObject 的數(shù)據(jù)被更新,ChildView2 的 View 重新渲染。整個過程中兩個 Child View 沒有從 RootView 中直接接受參數(shù),只有 RootView在初始化的時,通過 environmentObject() 方法把 MyModelObject 注入到整個 View 層級中,這個層級中所有的 View 都可以通過 @Environment的方式訪問 MyModelObject 。需要注意的一點是,使用 environmentObject() 注入的對象必須是 BindableObject 類型。

五個數(shù)據(jù)流工具總結(jié)

  • Property:當 View 所需要的屬性只要求可讀,則使用 Property。
  • @State: 當 View 所需要的屬性只在當前 View 和它的 Child Views 中使用,并且在用戶的操作過程中會發(fā)生變化,然后導致 View 需要作出改變,那么使用 @State。 因為只在當前 View 和它的 Child Views 中使用,跟外界無關(guān),所以被 @State 標記的屬性一般在定義時就有初始值。
  • @Binding:當 View 所需要的屬性是從它的直接 Parent View 傳入,在內(nèi)部會對這個屬性進行修改,并且修改后的值需要反饋給直接 Parent View,那么使用 @Binding
  • @ObjectBinding:用于直接綁定外部的數(shù)據(jù)模型和 View。
  • @EnvironmentObject:Root View 通過 environmentObject()BindableObject 注入到 View 層級中,其中的所有 Child Views 可以通過 @EnvironmentObject 來訪問被注入的 BindableObject。

接收其他外部變化

有時我們的 View 需要監(jiān)聽外部的其他變化,并做出相應(yīng)的改變,可以使用 receive(on:),這里面的 closure 參數(shù)是在主線程執(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
        }
    }
}

總結(jié)

數(shù)據(jù)在整個 App 中是非常重要的一部分,在使用上面講到的工具之前,先仔細研究自己的數(shù)據(jù)結(jié)構(gòu),然后選擇合適的工具,把數(shù)據(jù)注入到 UI 中。

想要更詳細了解文章的內(nèi)容,可以點擊查看下面的視頻。想及時看到我的新文章的,可以關(guān)注我。

參考資料

Data Flow Through SwiftUI - WWDC 2019 - Videos - Apple Developer

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容