理解SwiftUI数据流(二)

498 阅读3分钟

回顾上一篇文章

操作值类型

当数据是一个值类型的时候 (比如 struct,enum 或者是一个不可变对象),我们有三种选择:

  • 使用普通的属性,
  • 使用 @State 属性,
  • 或者使用 @Binding 属性。

当一个 view 按照只读的方式 来使用这个值时,普通的属性就够了, 我们可以把这个值通过初始化的方式被传入,如果想要以不同的值渲染同样的View的话,只需 要传入另外的值就行了。

struct ContentView: View { 
    var content: String
    var body:some View { 
        VStack{
           Text(content) 
        } 
     }
}

@State存储值类型。它会创建一个由 SwiftUI 进行管理的可读可写属性。当 view 被创建前,SwiftUI 会初始化 @State 属性,并且当 view 被创建时,属性值会被持久地保存到内存中,@State 总是只存在于当前的 view 树中的,你不应该从 view 层级的上层访问 (或者甚至初始化) 一个 state 变量。因此,我们会习惯于将一个 @State 变量标记为私有。这一限制 (不能随意访问或初始化) 没有在文档中写明,但是在 Swift 论坛上是被提到过的。

struct ContentView: View { 
    @State private var content = "daming"
    var body:some View { 
        VStack {
            Text(content) 
            Button("改内容") {
                self.content = "daming" 
            }
         } 
     }
}

当一个 view 需要对某个存储在别的地方的值进行读写时,可以用 @Binding 进行声明,绑定 (binding) 是信息源的延伸:比如它可能来源于一个 @State 或一个 ObservableObject (可观察对象)。它为 view 提供获取、设置和观察底层值的方法。换句话说,这个值不属于 view,SwiftUI 中的大部分可以对值进行更改的组件,都是以绑定的方式来获取这个值的:比如 TextView 中的文本属性,以及 Slider 的滑块值,都是绑定。

struct ContentView: View { 
    @State private var content = "daming"
    var body:some View { 
        VStack{
            MultilineTextField($content) 
         } 
     }
}

struct MultilineTextField: View { 
    @Binding var content: String 
}

ObservableObject

操作对象

在几乎所有的实际 app 中,我们会使用到我们自己的模型对象。为了让我们的模型对象可以被 SwiftUI 观察到,它的类型需要遵守被定义在 Combine 框架中的 ObservableObject 协议。

举例来说,当我们创建一个地址簿的 app 时,我们会需要一个代表联系人的 Contact 类:

class Contact: ObservableObject, Identifiable { 
    let id = UUID() 
    @Published var name: String 
    @Published var city: String

    init(name: String, city: String) { 
        self.name = name 
        self.city = city
    }
}

ObservableObject 协议的唯一要求是实现 objectWillChange,它是一个 publisher,会在对象变更时发送事件。通过在 name 和 city 属性前面添加 @Published,框架会为我们创建一个 objectWillChange 的实现,在每次这两个属性发生改变的时候发送事件。

public protocol ObservableObject : AnyObject {

    associatedtype ObjectWillChangePublisher : Publisher = ObservableObjectPublisher where Self.ObjectWillChangePublisher.Failure == Never
    
    var objectWillChange: Self.ObjectWillChangePublisher {
        get
     } 
     
}

我们需要使用 @ObservedObject 来定义 Detail 中的 contact 属性

struct Detail: View { 
    @ObservedObject var contact: Contact = Contact.init(name: "daming", city: "beijing")
    var body: some View { 
        VStack {
            Text(contact.name)
            Text(contact.city)
             Button("变身") {
                 self.contact.name = "Superhero \(self.contact.name)"
             }
        } 
     }
}

struct ContentView: View {
  @State var count = 0
  var body: some View {
    VStack{
      Text("\(count)")
      Button("+1") {
        count += 1
      }
      Detail().padding()
    }
  }
}

当我们点击变身时候,我们如愿以偿的得到了 Superhero,目前来说看似是没有任何问题,但是当我们点击ContentView的button时候我们发现什么,我们的Superhero不见了,这并不是我们想要的结果。

@StateObject

正常开发中,我们只想改变ContentView中Text的内容,并不想重制Detail内容,我们需要用@StateObject进行优化。

struct Detail: View { 
    @StateObject var contact: Contact = Contact.init(name: "daming", city: "beijing")
    var body: some View { 
        VStack {
            Text(contact.name)
            Text(contact.city)
             Button("变身") {
                 self.contact.name = "Superhero \(self.contact.name)"
             }
        } 
     }
}

ContentView button 点击之后Text的内容加1,我们的英雄还在。

ObservedObject 不管存储,会随着 View 的创建被多次创建。而 @StateObject 保证对象只会被创建一次。因此,如果是在 View 里自行创建的 ObservableObject model 对象,大概率来说使用 @StateObject 会是更正确的选择。@StateObject 基本上来说就是一个针对 class 的 @State 升级版。