Fritzables
Fritzables

Reputation: 383

Confusion with correct way to call Observable?

I am seeing that there may be two ways to call a Class from other Views or Models. The following is what I have created:

import Foundation
import Observation

@Observable 
class User {
    var EmailAddress = ""
    var Password = ""
}

And have seen it called either:

@State var user: User

or

@State var user = User()

This one is the preferred?

Upvotes: 0

Views: 290

Answers (2)

malhal
malhal

Reputation: 30746

@State var user = User() causes unnecessary heap allocations that will slow down SwiftUI (a bit like a memory leak) when User is a class so you certainly should not use that. The reason is every time the View is init a new User object will be init and lost. Its just how State works, a new initial value is init regardless if it already has one, which isn't an issue for memory stack values but is a big problem for heap objects. Avoiding unnecessary heap allocations is mentioned in Demystify SwiftUI performance WWDC 2023 at 3:18.

You can actually still use @StateObject and make the class conform to ObservableObject but just don't have any @Published properties.

However, in the docs it suggests to use an optional @State, e.g.

@State var user: User?

The reason is explained in the docs:

https://developer.apple.com/documentation/swiftui/state

A State property always instantiates its default value when SwiftUI instantiates the view. For this reason, avoid side effects and performance-intensive work when initializing the default value. For example, if a view updates frequently, allocating a new default object each time the view initializes can become expensive. Instead, you can defer the creation of the object using the task(priority:_:) modifier, which is called only once when the view first appears:

struct ContentView: View {
    @State private var library: Library?


    var body: some View {
        LibraryView(library: library)
            .task {
                library = Library()
            }
    }
}

Delaying the creation of the observable state object ensures that unnecessary allocations of the object doesn’t happen each time SwiftUI initializes the view. Using the task(priority:_:) modifier is also an effective way to defer any other kind of work required to create the initial state of the view, such as network calls or file access.

Note, .onAppear is better suited to doing this job since we don't need an async context just to init an object. If the UI reappears you might want to check for nil so you don't init multiple times. You might also want to set it to nil in .onDisappear too to be certain the memory is freed, SwiftUI can be quite greedy on memory with objects. So something like:

struct ContentView: View {
    @State private var library: Library?

    var body: some View {
        Group {
            if let library {
                LibraryView(library: library)
            }
        }
        .onAppear {
            if library == nil {
                 library = Library()
            }
        }
        .onDissapear {
            library = nil
        }
    }
}

This is all a big mess and might not even work, so I think it is best to just continue to use @StateObject or try to not even need an object, e.g. use .task with structs that contain async funcs that return results.

Upvotes: 0

sonle
sonle

Reputation: 9161

The second one with private access control is the correct way to use @State / @StateObject.

After Observation

Since user class no longer had to conform to ObservableObject. @StateObject -> @State is sufficient.

@State private var user = User()

Before Observation

According to Apple documentations about @State and @StateObject:

Even for a configurable state object, you still declare it as private. This ensures that you can’t accidentally set the parameter through a memberwise initializer of the view, because doing so can conflict with the framework’s storage management and produce unexpected results.

So, it should be @StateObject, since you stored an Object rather than a single property.

@StateObject private var user = User()

Notice: you're still allowed to use @State in this circumstance. However, the view that listened to the state object will only re-render if the reference to the object changes, instead of updating when these @Published properties change.

Upvotes: 0

Related Questions