2

From what I've read whenever you instantiate an object yourself in your view, you should use @StateObject instead of @ObservedObject. Because apparently if you use @ObservedObject, SwiftUI might decide in any moment to throw it away and recreate it again later, which could be expensive for some objects. But if you use @StateObject instead then apparently SwiftUI knows not to throw it away.

Am I understanding that correctly?

My question is, how does @StateObject communicate that to SwiftUI? The reason I'm asking is because I've made my own propertyWrapper which connects a view's property to a firebase firestore collection and then starts listening for live snapshots. Here's an example of what that looks like:

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)
                }
            }
        }
    }
}

The closure inside @Collection is optional, but can be used to build a more precise query for the collection.

Now this works very nicely. The code is expressive and I get nice live-updating data. You can see that when the user would click on a room title, the navigation view would navigate to that chat room. The chatroom is a view which shows all the messages in that room. Here's a simplified view of that code:

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)
        }
    }
}

But what I've noticed, is that SwiftUI initializes a new messages-collection every single time the user interacts with the UI in any way. Like even when an input box is clicked to give it focus. This is a huge memory leak. I don't understand why this happens, but is there a way to tell SwiftUI to only initialize @Collection once, just like it does with @StateObject?

Evert
  • 2,022
  • 1
  • 20
  • 29

1 Answers1

2

Use @StateObject when you want to define a new source-of-truth reference type owned by that View and tied to its life-cycle. The object will be created just before the body is run for the first time and is stored in a special place by SwiftUI. If the View struct is recreated (e.g. by a parent View's body recompute by a state change) then the previous object will be set on the property instead of a new object. If the View is no longer init during body updates then the object will be deinit.

When you pass a @StateObject into a child View then in that child View uses @ObservedObject to enable the body of the child view to be recomputed when the object changes, just the same way the body of the parent View will also be recomputed because it used an @StateObject. If you use @ObservedObject for an object that was not an @StateObject in a parent View then since no View is owning it then it will be lost every time the View is init during body updates. Also, any objects that you create in the View init, like your Collection, those are immediately lost too. These excessive heap allocations could cause a leak and slows down SwiftUI. Objects the View owns must be wrapped in @StateObject to avoid this.

Lastly, don't use @StateObject for view state and certainly don't try and implement a legacy "View Model" pattern with them. We have @State and @Binding for that. @StateObject is only for model objects (as in your domain types: Book, Person etc.), loaders or fetchers.

WWDC 2020 Data Essentials in SwiftUI explains all this very nicely.

malhal
  • 26,330
  • 7
  • 115
  • 133