SwiftUI之深入解析@StateObject、@ObservedObject和@EnvironmentObject的联系和区别

一、@State 属性包装器

① 什么是 @State 属性包装器?

  • 状态在任何现代应用程序中都是不可避免的,但在 SwiftUI 中,重要的是所有的视图都是它们状态的简单函数,我们不需要直接改变视图,而是操纵状态,让状态决定结果。SwiftUI 提供了几种在应用程序中存储状态的方法,但它们之间有细微的差别,为了正确地使用它们,理解它们之间的差别是很重要的。
  • 处理 state 的最简单的方法是 @State 属性包装器,使用如下:
struct ContentView: View {

    @State private var tapCount = 0

    var body: some View {
        Button("Tap count: \(tapCount)") {
            tapCount += 1
        }
    }
}
  • 这在视图中创建了一个属性,但是它使用 @State 属性包装器来要求 SwiftUI 管理内存。这一点非常重要,我们所有的视图都是结构体,这意味着它们不能被改变,如果甚至不能在应用程序中修改一个整数,那么我们也做不了什么。所以,当我们说 @State 来创建属性时,把它的控制权交给 SwiftUI,这样只要视图存在,它就会一直存在于内存中;当状态发生变化时,SwiftUI 会自动重新加载带有最新变化的视图,这样它就可以反映它的新信息。
  • @State 对于属于特定视图且永远不会在该视图之外使用的简单属性非常有用,因此,将这些属性标记为 private 非常重要,以强化这样一种想法,即此类状态是专门设计的,永远不会逃逸其视图。

SwiftUI之深入解析@StateObject、@ObservedObject和@EnvironmentObject的联系和区别_第1张图片

② 为什么 @State 只适用于结构体?

  • SwiftUI 的 State 属性包装器是为当前视图本地的简单数据而设计的,但是一旦您想在视图之间共享数据,它就不再有用了。让我们用一些代码来分解它,如下所示,是一个存储用户名字和姓氏的结构:
struct User {
    var firstName = "Bilbo"
    var lastName = "Baggins"
}
  • 现在可以通过创建 @State 属性并将内容附加到 $user.firstName 和 $user.lastName 来在 SwiftUI 视图中使用它,如下所示:
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 足够聪明,可以理解一个对象包含所有数据,并且会在任一值更改时更新 UI。在背后,实际发生的情况是,每次结构中的值发生变化时,整个结构都会发生变化,每次键入名字或姓氏的键时,就像一个新用户,这样听起来可能很浪费,但实际上非常快。
  • 类和结构体之间的区别,有两个重要的区别:
    • 首先,结构体始终具有唯一的所有者,而对于类,多个事物可以指向相同的值;
    • 其次,类在更改其属性的方法之前不需要 mutating 关键字,因为可以更改常量类的属性。
  • 实际上,这意味着如果我们有两个 SwiftUI 视图,并且向它们发送相同的结构体以供使用,那么它们实际上每个都有该结构体的唯一副本;如果一个更改它,另一个将不会看到该更改;另一方面,如果创建一个类的实例并将其发送到两个视图,它们将共享更改。
  • 对于 SwiftUI 开发人员来说,这意味着如果我们想在多个视图之间共享数据,希望两个或多个视图指向相同的数据,以便在一个更改时它们都会得到这些更改,那么需要使用的是类而不是结构。
  • 因此,请将 User 结构体更改为一个类,如下:
struct User {}

// -->
class User {}
  • 现在再次运行程序,会发生什么呢?可以看到,它不再起作用了,当然还可以像以前一样在文本字段中输入,但上面的文本视图不会改变。
  • 当使用@State 时,我们要求 SwiftUI 观察属性的变化。因此,如果改变一个字符串,改变一个布尔值,添加到一个数组等,属性已经改变,SwiftUI 将重新调用视图的 body 属性。
  • 当 User 是一个结构体时,每次修改该结构体的属性时,Swift 实际上都是在创建该结构体的一个新实例。@State 能够发现这种变化,并自动重新加载视图。现在我们改成了一个类,这种行为不再发生:Swift 可以直接修改值。
  • 还记得我们是如何对修改属性的结构体方法使用 mutating 关键字的吗?这是因为如果我们将结构体的属性创建为变量,但结构体本身是常量,无法更改属性,Swift 需要能够在属性更改时销毁并重新创建整个结构体,而这对于常量结构;类不需要 mutating 关键字,因为即使类实例被标记为常量 Swift 仍然可以修改变量属性。

二、什么是 @StateObject?

① @StateObject 的概念和使用

  • SwiftUI 的 @StateObject 属性包装器旨在填补状态管理中的一个非常具体的空白:当需要在一个视图中创建引用类型并确保它保持活动状态以在该视图和共享的其他视图中使用时。例如,考虑一个简单的 User 类,如下:
class User: ObservableObject {
    var username = "@twostraws"
}
  • 如果想在各种视图中使用它,需要在 SwiftUI 外部创建它并将其注入,或者在一个 SwiftUI 视图中创建它并使用 @StateObject,如下所示:
struct ContentView: View {
    @StateObject var user = User()

    var body: some View {
        Text("Username: \(user.username)")
    }
}
  • 这将确保 User 实例在视图更新时不会被破坏。以前可能已经使用 @ObservedObject 来获得相同的结果,但这是有风险的。有时 @ObservedObject 可能会意外释放它正在存储的对象,因为它不是设计为最终的真相来源目的,但 @StateObject 就不会发生这种情况,因此应该改用它。
  • 在 @State 和 @ObservedObject 之间存在 @StateObject,这是 @ObservedObject 的特有版本,它们几乎是相同的工作方式:必须符合 ObservableObject 协议,可以使用 @Published 将属性标记为导致更改通知,和任何观察 @StateObject 的视图都会在对象发生变化时刷新它们的主体。
  • @StateObject 和 @ObservedObject 之间有一个重要的区别,即所有权,哪个视图创建了对象,哪个视图只是在监视它。规则是这样的:首先创建对象的视图必须使用 @StateObject,告诉 SwiftUI 它是数据的所有者并负责保持它的活动,所有其它视图都必须使用 @ObservedObject 来告诉 SwiftUI,它们想要观察对象的变化但不直接拥有它。
  • 每个对象应该只使用一次 @StateObject,它应该在负责创建对象的任何视图中,共享对象的所有其它视图都应使用 @ObservedObject。

② 如何使用 @StateObject 创建和监控外部对象?

  • SwiftUI 的 @StateObject 属性包装器是 @ObservedObject 的一种特殊形式,具有所有相同的功能,但有一个重要的补充:它应该用于创建观察对象,而不仅仅是存储从外部传入的对象。
  • 当使用 @StateObject 向视图添加属性时,SwiftUI 认为该视图是可观察对象的所有者,传递该对象的所有其他视图都应使用 @ObservedObject,这非常重要,如果弄错,可能会发现对象被意外破坏了,这会导致应用程序看似随机崩溃。因此明确一点:我们应该使用 @StateObject 在某处创建可观察对象,并且在传递该对象的所有后续地方应该使用 @ObservedObject。
  • 如下所示:
// An example class to work with
class Player: ObservableObject {
    @Published var name = "Taylor"
    @Published var age = 26
}

// A view that creates and owns the Player object.
struct ContentView: View {
    @StateObject var player = Player()

    var body: some View {
        NavigationView {
            NavigationLink(destination: PlayerNameView(player: player)) {
                Text("Show detail view")
            }
        }
    }
}

// A view that monitors the Player object for changes, but
// doesn't own it.
struct PlayerNameView: View {
    @ObservedObject var player: Player

    var body: some View {
        Text("Hello, \(player.name)!")
    }
}
  • 如果发现很难记住区别,可以尝试以下操作:每当在属性包装器中看到“State”时,例如 @State、@StateObject、@GestureState,表示“当前视图拥有这些数据”。

三、什么是 @ObservedObject?

① @ObservedObject 的概念和使用

  • 对于一些更复杂的属性,当想要使用的自定义类型的时候,可能会有多个属性和方法,或者可能在多个视图之间共享,那么通常会使用 @ObservedObject。SwiftUI 提供了 @ObservedObject 属性包装器,以便视图可以观察外部对象的状态,并在重要的事情发生变化时得到通知,它在行为上与@StateObject 相似,除了它不能用于创建对象。@ObservableObject 只能用于在其他地方创建的对象,否则 SwiftUI 可能会意外破坏该对象。如下所示:
class Order: ObservableObject {
    @Published var items = [String]()
}

struct ContentView: View {
    @ObservedObject var order: Order

    var body: some View {
        // your code here
    }
}
  • 该 Order 类使用 @Published 因此它会在项目更改时自动发送更改通知,而 ContentView 使用 @ObservedObject 来监视这些通知,如果没有@ObservedObject,更改通知将被发送但被忽略。
  • @ObservedObject 与 @State 也非常相似,只是现在使用的是外部引用类型,而不是简单的本地属性(如字符串或整数)。如果仍然说视图依赖于将要更改的数据,除了现在它是我们自己负责管理的数据,那么需要创建类的一个实例,创建它自己的属性等。
  • @ObservedObject 使用的任何类型都应该符合 ObservableObject 协议,当向可观察对象添加属性时,需要决定对每个属性的更改是否应该强制监视对象的视图刷新,通常可能会这样做,但这不是必需的。这反过来意味着它必须是一个类而不是结构体,这不是可选的,SwiftUI 要求使用一个类。
  • 不过,还是有号几种方法可以让被观察对象通知视图重要数据发生了更改,但最简单的方法是使用 @Published 属性包装器,符合 ObservableObject 的类型被赋予一个默认的 objectWillChange 发布者,以根据需要制作自定义公告。如果需要更多的控制,也可以使用 Combine 框架中的自定义发布者,但实际上这是非常少见的。如果这个可观察对象碰巧有几个视图在使用它的数据,任何一个选项都会自动通知所有视图(注意:当使用自定义发布程序来宣布对象已更改时,这必须在主线程中发生)。
  • 观察对象是专门为视图外部的数据设计的,这意味着它可能会在多个视图之间共享。 @ObservedObject 属性包装器将自动确保密切关注该属性,以便重要的更改将重新加载使用它的任何视图。这也意味着数据必须在别处创建,然后发送到您的视图中。

② 如何使用 @ObservedObject 管理来自外部对象的状态?

  • 在使用观察对象时,我们需要处理三个关键的事情:ObservableObject 协议与某种可以存储数据的类一起使用,@ObservedObject 属性包装器用于在视图中存储可观察对象实例,以及 @ObservedObject 属性包装器已发布的属性包装器被添加到观察对象内的任何属性,这些属性在视图更改时应导致视图更新。
  • 需要注意的是,仅将 @ObservedObject 用于从其它地方传入的视图,不应该使用这个属性包装器来创建一个可观察对象的初始实例,这就是 @StateObject 的用途。如下所示,是一个符合 ObservableObject 的 UserProgress 类:
class UserProgress: ObservableObject {
    @Published var score = 0
}
  • 不过你可能会说,这看起来不像很多代码,但那是因为 SwiftUI 为我们做了大量的工作,有两点很重要:
    • ObservableObject 一致性允许在视图中使用此类的实例,以便在发生重要更改时,视图将重新加载;
    • @Published 属性包装器告诉 SwiftUI,对 score 的更改应触发视图重新加载。
  • 我们可以使用 UserProgress 类和这样的代码:
class UserProgress: ObservableObject {
    @Published var score = 0
}

struct InnerView: View {
    @ObservedObject var progress: UserProgress

    var body: some View {
        Button("Increase Score") {
            progress.score += 1
        }
    }
}

struct ContentView: View {
    @StateObject var progress = UserProgress()

    var body: some View {
        VStack {
            Text("Your score is \(progress.score)")
            InnerView(progress: progress)
        }
    }
}
  • 如您所见,除了使用带有进度的 @ObservedObject 属性包装器外,其他一切或多或少看起来都一样,SwiftUI 为我们处理了所有的实现细节。但是,有一个重要区别:progress 属性未声明为私有,这是因为绑定对象可以被多个视图使用,所以公开共享它是很常见的。
  • 需要注意,请不要使用 @ObservedObject 创建对象的实例,如果这是想要做的,请改用 @StateObject。

四、什么是 @EnvironmentObject?

  • 我们已经看到 @State 如何为一个类型声明简单的属性,当它改变时会自动导致视图刷新,以及@ObservedObject 如何为一个外部类型声明一个属性,当它改变时可能会也可能不会导致视图刷新,这两者都必须由视图设置,但 @ObservedObject 可能与其它视图共享。
  • 还有另一种类型的属性包装器可供使用,它是 @EnvironmentObject,这是一个通过应用程序本身提供给视图的值,它是每个视图都可以读取的共享数据。因此,如果应用程序有一些所有视图都需要读取的重要模型数据,可以将它从一个视图传递到另一个视图,或者只是将其放入每个视图都可以即时访问它的环境中。
  • 当需要在应用程序周围传递大量数据时,@EnvironmentObject 将带来巨大的便利,因为所有的视图都指向同一个模型,如果一个视图改变了模型,所有的视图都会立即更新,没有让应用程序的不同部分出现不同步的风险。
  • SwiftUI 的 @EnvironmentObject 属性包装器,可以创建依赖于共享数据的视图,通常跨整个 SwiftUI 应用程序。 例如,如果创建一个将在应用程序的许多部分共享的用户,则应使用@EnvironmentObject。例如,我们可能有一个像这样的 Order 类:
class Order: ObservableObject {
    @Published var items = [String]()
}
  • 这符合 ObservableObject,这意味着可以将它与 @ObservedObject 或 @EnvironmentObject 一起使用。在这种情况下,我们可能会创建一个使用@EnvironmentObject 的视图,如下所示:
struct ContentView: View {
    @EnvironmentObject var order: Order

    var body: some View {
        // your code here
    }
}
  • 请注意 order 属性是如何没有给出默认值的,通过使用 @EnvironmentObject,我们说该值将由 SwiftUI 环境提供,而不是由该视图显式创建。
  • @EnvironmentObject 与@ObservedObject 有很多共同点:两者都必须引用一个符合 ObservableObject 的类,两者都可以在多个视图之间共享,并且都将在发生重大变化时更新任何正在观察的视图。但是,@EnvironmentObject 特指此对象将从某个外部实体提供,而不是由当前视图创建或专门传入。
  • 实际上,想象一下如果您有视图 A,并且视图 A 有一些视图 E 想要的数据,使用@ObservedObject 视图 A 需要将对象交给视图 B,视图 B 将把它交给视图 C,然后是视图 D,最后是视图 E,所有中间视图都需要发送对象,即使它们实际上并没有需要它。
  • 使用 @EnvironmentObject 时,视图 A 可以创建一个对象并将其放入环境中;然后,其中的任何视图都可以随时通过请求访问该环境对象,而不必显式传递它,这样会使我们的代码更简单。
  • 当显示使用 @EnvironmentObject 的视图时,SwiftUI 将立即在环境中搜索正确类型的对象,如果找不到这样的对象,即如果你忘记把它放在环境中,那么你的应用程序将立即崩溃。当您使用 @EnvironmentObject 时,实际上是在保证对象将在需要时存在于环境中,有点像使用隐式解包选项。

五、三者区分

  • @State 用于属于单个视图的简单属性,它们通常应标记为私有。
  • @ObservedObject 用于可能属于多个视图的复杂属性,大多数情况下,如果使用的是引用类型,那么应该为它使用 @ObservedObject。
  • 对使用的每个可观察对象使用 @StateObject 一次,无论代码的哪个部分负责创建它。
  • @EnvironmentObject 用于在应用程序中其它地方创建的属性,例如共享数据。

你可能感兴趣的:(SwiftUI,State属性包装器,ObservedObject,StateObject,Environment)