如何防止SwiftUI像@StateObject一样重新初始化包装的属性?

问题描述

从我每次在视图中实例化对象时所阅读的内容来看,应该使用@StateObject而不是@Observedobject。因为很明显,如果您使用@Observedobject,SwiftUI可能会随时决定将其丢弃并稍后再重新创建,这对于某些对象可能会很昂贵。但是,如果您使用@StateObject代替,显然SwiftUI知道不会丢弃它。

我理解正确吗?

我的问题是,@StateObject如何将其传达给SwiftUI?我问的原因是因为我已经创建了自己的propertyWrapper,它将视图的属性连接到Firebase Firestore集合,然后开始侦听实时快照。这是一个看起来像的例子:

struct Room: Model {
    @DocumentID
    var id: DocumentReference? = nil
    var title: String
    var description: String

    static let collectionPath: String = "rooms"
}

struct MacOSView: View {
    @Collection({ $0.order(by: "title") })
    private var rooms: [Room]
    
    var body: some View {
        NavigationView {
            List(rooms) { room in
                NavigationLink(
                    destination: Lazy(ChatRoom(room))
                ) {
                    Text(room.title)
                }
            }
        }
    }
}

@Collection中的闭包是可选的,但可用于为集合建立更精确的查询

在这很好用。该代码具有表现力,并且可以得到很好的实时更新数据。您可以看到,当用户单击房间标题时,导航视图将导航到该聊天室。聊天室是一个视图,显示该房间中的所有消息。这是该代码的简化视图:

struct ChatRoom: View {
    @Collection(wait: true)
    private var messages: [Message]
    
    // I'm using (wait: true) to say "don't open a connection just yet,// because I need to give you more information that I can't give you yet"
    // And that's because I need to give @Collection a query
    // based on the room id,which I can only do in the initializer.

    init(_ room: Room) {
        _messages = Collection { query in
            query
                .whereField("room",isEqualTo: room.id!)
                .order(by: "created")
        }
    }
    
    var body: some View {
        List(messages) { message in
            MessageBubble(message: message)
        }
    }
}

但是我注意到的是,每当用户以任何方式与UI交互时,SwiftUI都会初始化一个新的消息集合。即使单击输入框使其具有焦点也是如此。这是一个巨大的内存泄漏。我不明白为什么会这样,但是有没有办法告诉SwiftUI仅初始化@Collection一次,就像对@StateObject一样?

解决方法

当您要定义该视图拥有的并与其生命周期相关的新的真理来源引用类型时,请使用@StateObject。该对象将在第一次运行主体之前创建,并由SwiftUI存储在特殊位置。如果重新创建View结构(例如,通过状态变化重新计算父View的主体),则将在属性上设置前一个对象,而不是新对象。如果在主体更新过程中View不再处于初始化状态,则该对象将被取消初始化。

当您将@StateObject传递给子视图时,则在该子视图中使用@ObservedObject使对象改变时可以重新计算子视图的主体,就像主体的主体一样父视图也将重新计算,因为它使用了@StateObject。如果对父视图中不是@ObservedObject的对象使用@StateObject,则由于没有视图拥有,因此每次在主体更新期间初始化视图时,该视图都会丢失。另外,您在View初始化中创建的任何对象(例如您的Collection)也将立即丢失。这些过多的堆分配可能导致泄漏并减慢SwiftUI的速度。视图拥有的对象必须包装在@StateObject中,以免发生这种情况。

最后,请勿将@StateObject用于视图状态,当然也不要尝试使用它们来实现传统的“视图模型”模式。为此,我们有@State@Binding@StateObject仅适用于模型对象(如您的域类型:Book,Person等),装载程序或提取程序。

WWDC 2020 Data Essentials in SwiftUI很好地解释了这一切。