[SwiftUI 知識碎片] 為什麼 @State 只能在結構體中工作

貓克杯發表於2020-03-11
譯自 Why @State only works with structs
更多內容,歡迎關注公眾號 「Swift花園」

我們知道,SwiftUI 的State 屬性包裝器被設計用於儲存當前檢視的本地資料。不過一旦你需要在檢視之間共享資料,它就不管用了。

讓我們把理論分解為程式碼 —— 下面是一個結構體,儲存了使用者的姓和名。

struct User {
    var firstName = "Bilbo"
    var lastName = "Baggins"
}複製程式碼

我們可以在 SwiftUI 檢視中建立一個 @State 的 User 屬性,然後把 $user.firstName$user.lastName 同一些檢視繫結,像這樣:

struct ContentView: View {
    @State private var user = User()

    var body: some View {
        VStack {
            Text("Your name is \(user.firstName) \(user.lastName).")

            TextField("First name", text: $user.firstName)
            TextField("Last name", text: $user.lastName)
        }
    }
}複製程式碼

上面的程式碼可以完美工作:SwiftUI 很聰明,知道整個 User 物件包含了我們的全部的資料,並且會在 User 內部的值發生變化時更新 UI 。在幕後實際發生的事情是:每當我們的結構體中的某個值改變時,整個結構體隨之改變 —— 就如同我們重新輸入姓和名構建了一個新的 User 那樣。聽起來好像很浪費,但實際上這個過程非常快。

此前我們研究過類和結構體之間的差異,我提及它們有兩個重要的差異。其一,結構體總是隻有唯一的所有者,而對於類,可以有很多個物件指向同一份資料。其二,類不需要在方法名前寫mutating 關鍵字以便修改它們的屬性,因為即使是常量類,屬性也可以直接修改。

實踐中,這意味著如果我們有兩個 SwiftUI 的檢視,並且我們用相同的結構體例項賦給它們,它們實際上是各自擁有一份唯一的結構體拷貝;如果其中一個改變,另外一個並不會隨著改變。另一方面,如果我們建立一個類例項,賦給兩個檢視,它們會共享改變。

對於 SwiftUI 開發者,這意味著如果我們想在多個檢視之間共享資料,或者說讓兩個或者更多檢視引用相同的資料,以便一個改變,全部跟隨改變 —— 這種情況下我們需要用類而不是結構體。

所以,我們是不是可以把 User 結構體改成一個類,把下面的程式碼:

struct User {複製程式碼

改成這樣:

class User {複製程式碼

現在執行程式碼,看看會發生什麼?

我們搞砸了:app 無法正常工作。 是的,當我們像之前那樣往文字框裡輸入字串時,文字檢視不再改變了。這是為什麼?

當我們使用 @State的時候,我們是在要求 SwiftUI 為我們監視某個屬性的變化。這樣讓我們改變一個字串,反轉一個布林型,或者往陣列裡加東西的時候,屬性會變化,而 SwiftUI 會重新呼叫檢視的 body屬性。

User 還是一個結構體的時候,每當我們修改它的屬性時,Swift 實際上建立了一個新的結構體例項。@State 能夠看穿這種變化,並自動重新載入檢視。現在我們把它改成類,這種行為不再發生:因為 Swift 能夠直接修改目標物件的值 —— 沒有新例項產生。

還記得我們需要在結構體的方法前新增 mutating 關鍵字以便修改結構體的屬性對吧?這是因為雖然我們是以變數的方式建立結構體的屬性,但結構體本身是不可變的,我們無法修改它的屬性值 —— Swift 需要銷燬並重建整個結構體以完成屬性的改動。(mutating 相當於向編譯器表態我就是要這個過程發生)。類並不需要 mutating 關鍵字,因為即便類本身是一個常量,Swift 仍然可以直接修改它的變數屬性。

上面是一個很費解的理論點。讓我針對我們的動作簡單解釋:由於 User 現在變成類了, 類中的屬性值雖然在變化,但 @State 無法監控到這一點,所以沒有重新載入檢視以反映變化。

為了解決共享資料的問題,先把 @State 放一放。我們接下來要引入一個更強大的屬性包裝器,它叫 @ObservedObject,請拭目以待。

譯自 Sharing SwiftUI state with @ObservedObject

用 @ObservedObject 共享 SwiftUI 狀態

當我們用類承載 SwiftUI 的資料時,需要跨越多個檢視共享這些資料時是怎麼實現的呢?—— 對此 SwiftUI 給了我們兩個有用的屬性包裝器: @ObservedObject@EnvironmentObject。 Environment object 我們稍後再討論,現在先來聚焦在 observed object 。

下面是一份建立承載使用者資料的 User 類,以及在檢視中顯示這些使用者資料的程式碼:

class User {
    var firstName = "Bilbo"
    var lastName = "Baggins"
}

struct ContentView: View {
    @State private var user = User()

    var body: some View {
        VStack {
            Text("Your name is \(user.firstName) \(user.lastName).")

            TextField("First name", text: $user.firstName)
            TextField("Last name", text: $user.lastName)
        }
    }
}複製程式碼

我們給 user 屬性標記了 @State,因此我們應該是希望輸入到文字框,然後上面的文字檢視跟著更新。但是,程式碼沒有像我們想象的那樣運作:

為了修正這個問題,我們需要讓 SwiftUI 知道,類中的哪些我們感興趣的部分發生了變化。這裡說的 “感興趣的部分”,指的是會導致 SwiftUI 需要監控並根據它們重新載入檢視的部分—— 很有可能你的類裡面有很多屬性,但只有一小部分需要以這種方式暴露給外面。

User 類有兩個屬性:firstNamelastName。無論什麼時候,這兩個屬性中的任何一個變化,我們都希望通知監視這個類的檢視有變化發生以便它們重新載入。 所以我們可以對這兩個屬性使用 @Published 屬性觀察者,就像這樣:

class User {
    @Published var firstName = "Bilbo"
    @Published var lastName = "Baggins"
}複製程式碼

@Published 起的作用類似於一半的 @State:它也可以在屬性值發生變化的時候發表“宣告”。

那麼宣告給誰聽?這就要用到另一個屬性包裝器@ObservedObject,相當於另一半的 @State—— 它告訴 SwiftUI 要監視變化的目標物件。

因此,我們把 user 屬性改成這樣:

@ObservedObject var user = User()複製程式碼

我同時把 private 訪問控制移除了,不過實際上要不要這麼做取決於你的需求。

由於我們用了 @ObservedObject,我們的程式碼現在無法編譯通過了。這不是大問題,實際上修正很簡單:@ObservedObject 屬性包裝器只能用在遵循了 ObservableObject 協議的型別上。這個協議沒有具體要求,只是簡單表明 “我們想要某些別的東西能夠觀察這個型別的物件”。

User 類稍作調整:

class User: ObservableObject {
    @Published var firstName = "Bilbo"
    @Published var lastName = "Baggins"
}複製程式碼

現在程式碼不僅編譯通過,而且可以正常工作了。執行 app ,你會發現上面的文字檢視可以正確地跟隨文字框裡的輸入變化同步更新。

如你所見,相比用 @State宣告本地狀態,要實現共享狀態我們需要三個步驟:

  • 建立一個遵循 ObservableObject 協議的類
  • 標記類裡的某些屬性為 @Published 以便使用這個類的檢視能夠根據這些屬性的變化來更新
  • @ObservedObject 屬性包裝器來建立類的例項

這三個步驟的成果是,我們可以把狀態儲存在外部物件中,甚至可以在多個檢視中使用這個物件,讓所有檢視都指向相同的資料。

我的公眾號 這裡有Swift及計算機程式設計的相關文章,以及優秀國外文章翻譯,歡迎關注~

[SwiftUI 知識碎片] 為什麼 @State 只能在結構體中工作


相關文章