用于驱动数据更改的可变属性或 ObservableObject 已发布属性的结构



我想帮助进一步理解使用以下 2 种方法在多个视图之间驱动数据的含义。

我的情况:父视图使用传入的数据初始化多个子视图。

  • 这个数据是一个大对象。
  • 每个视图采用不同的数据切片。
  • 每个视图都可以操作初始数据(过滤、排序等)

使用可观察Obeject存储此数据和每个视图的多个已发布属性:

  • 可以作为环境对象传入,任何视图都可以使用 @EnvironmentObject 访问该对象。
  • 可以创建对已发布属性的绑定并对其进行更改。
  • 在 ObservableObject 类上执行一个方法,并操作使用方法内部objectWillChange.send()发布的属性值。

我已经通过使用带有mutating方法的结构实现了上面列出的预期。在结构中更改这些属性后,绑定到这些属性的视图会导致重新呈现。

我的结构不做任何异步工作。它设置初始值。其属性在用户操作(如单击筛选器按钮)时进行修改。

struct MyStruct {
var prop1 = "hello"
var prop2: [String] = []

init(prop2: [String]) {
self.prop2 = prop2
}


mutating func changeProp2(multiplier: Int) {
let computation = ...
prop2 = computation //<----- This mutates prop2 and so my view Binded to this value gets re-renderd.
}
}
struct ParentView: View {
var initValue: [String] // <- passed in from ContentView
@State private var myStruct: MyStruct

init(initValue: [String]) {
self.myStruct = MyStruct(prop2: initValue)
}

var body: some View {
VStack {

SiblingOne(myStruct: $myStruct)
SiblingTwo(myStruct: $myStruct)
}
}
}

struct SiblingOne: View {
@Binding var myStruct: MyStruct
var body: some View {
HStack{
Button {
myStruct.changeProp2(multiplier: 10)
} label: {
Text("Mutate Prop 2")
}
}
}
}

struct SiblingTwo: View {
@Binding var myStruct: MyStruct
var body: some View {
ForEach(Array(myStruct.prop2.enumerated()), id: .offset) { idx, val in
Text(val)
}
}
}

问题:

使用ObservableObject比使用改变其自身属性的结构有哪些用例?

有重叠的用例,但我希望了解其中的差异:

  1. 某些情况 A 有利于可观察对象
  2. 某些情况 B 倾向于结构突变属性

在我开始之前,当你说"这些属性导致重新渲染"时,实际上没有任何东西被重新渲染 发生的所有事情都是依赖于let的所有body,并且调用了已更改的@State var,并且 SwiftUI 构建了这些值的树。这是超级快的,因为它只是在内存堆栈上创建值。它将此值树与前一个值树进行比较,并且差异用于在屏幕上创建/更新/删除UIView对象。实际渲染是比该级别低的另一个级别。因此,我们将其称为无效而不是渲染。最好"收紧"失效以获得更好的性能,即仅在该View中声明lets/vars,这些在body中实际使用以使其更短。话虽如此,没有人比较过一个大体和许多小体之间的性能,所以目前真正的收益是未知的。由于这些值树是创建并丢弃的,因此重要的是只初始化值类型而不是任何对象,例如,不要将任何NSNumberFormatterNSPredicate对象作为View结构的让,因为它们会立即丢失,这本质上是内存泄漏!对象需要位于属性包装器中,因此它们只初始化一次。

在这两种示例情况下,最好首选值类型,即保存数据的结构。如果只有简单的突变逻辑,则将@State var structmutating func一起使用,并在需要读取访问权限时将其作为let传递到子视图中,如果需要写入访问权限,则将其作为@Binding var struct传递到子视图中。

如果您需要持久化或同步数据,那么此时您将受益于引用类型,即ObservableObject。由于对象是在内存堆上创建的,因此创建这些对象的成本更高,因此我们应该限制它们的使用。如果您希望将对象的生命周期与屏幕上的某些内容相关联,请使用@StateObject。我们通常使用其中之一来下载数据,但现在不再需要它,因为我们.task它具有额外的好处,当视图消失时,它会自动取消下载,没有人记得与@StateObject有关。但是,如果是永远不会被初始化的模型数据,例如模型结构将从磁盘加载并保存(异步),那么最好使用单例对象,并将其作为环境对象传递给View层次结构,例如.environmentObject(Store.shared),那么对于预览,您可以使用与示例数据一起初始化而不是从磁盘加载的模型,例如.environmentObject(Store.preview).这样做的好处是,可以将对象传递到层次结构深处的视图,而无需将它们全部作为let object传递(不是@ObservedObject,因为我们不希望在这些不使用对象的中间视图上调用正文)。

另一个重要的事情是,您的项目结构通常应符合Identifiable以便您可以在ForEach视图中使用它。我注意到在您的代码中,您在数组索引上使用ForEach像 for 循环一样,这是一个错误,会导致崩溃。这是一个View,您需要提供可识别的数据,以便它可以跟踪更改,即移动、插入、删除。这在数组索引中根本不可能,因为如果项目从 0 移动到 1,它仍然显示为 0。

以下是所有内容的一些示例:

struct UserItem: Identifiable {
var username: String
var id: String {
username
}
}
class Store: ObservableObject {
static var shared = Store()
static var preview = Store(preview: true)

@Published var users: [UserItem] = []
init(preview: Bool = false) {
if (preview) {
users = loadSampleUsers()
}
else {
users = loadUsersFromDisk()
}
}
@main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(Store.shared)
}
}
}

struct ContentView: View {
var body: some View {
NavigationView {
List {
ForEach($store.users) { $user in
UserView(user: $user)
}    
}          
} 
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(Store.preview)
}
}
struct UserView: View {
@Binding var user: UserItem
var body: some View {
TextField("Username", text: $user.username)
}
}

最新更新