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

Posted

技术标签:

【中文标题】如何防止 SwiftUI 像使用 @StateObject 一样重新初始化我的包装属性?【英文标题】:How can I prevent SwiftUI from reinitializing my wrapped property just like it does with @StateObject? 【发布时间】:2020-08-20 10:19:02 【问题描述】:

根据我所读到的,每当您在视图中自己实例化一个对象时,您应该使用@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 所做的那样?

【问题讨论】:

【参考方案1】:

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

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

最后,不要将@StateObject 用于视图状态,当然也不要尝试使用它们实现旧的“视图模型”模式。为此,我们有 @State@Binding@StateObject 仅适用于模型对象(如在您的域类型中:Book、Person 等)、加载器或提取器。

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

【讨论】:

以上是关于如何防止 SwiftUI 像使用 @StateObject 一样重新初始化我的包装属性?的主要内容,如果未能解决你的问题,请参考以下文章

如何防止 SwiftUI clipShape 影响子视图

如何防止 TextField 在 SwiftUI 列表中消失?

如何防止 TextEditor 在 SwiftUI 中滚动?

SwiftUI如何防止视图重新加载整个身体

如何防止键盘在 SwiftUI 中向上推视图? [复制]

如何防止 SwiftUI SegmentedControl 中的文本“弹跳”