I have some questions related to the @State property wrapper and how it can/should be used in combination with injecting a @Observable model into the view.
My understanding is as follows:
-
Generally speaking,
@Stateshould only be used for private (i.e. not injected) models that are directly initialized within the view like below:@Observable final class Model { ... } struct SomeView: View { @State private var model = Model() var body: some View { ... } }This way, the lifetime of
modelis managed by SwiftUI (i.e. it's bound to the lifetime ofSomeView).
Interestingly, I wasn't able to find an example, where it would make a difference whether@Stateis used or not (i.e. even with justprivate var model = Model()everything worked as expected). But I assume this was mainly caused by the fact that I was only testing with a toy app. -
The main reason for why we shouldn't inject
@Stateattributes seems to be, that upon initialization we can only set it's initial value (but not it's wrapped value). So when the view is already presented on screen, we wouldn't change the actual value that's visible (wrappedValue). -
When we need to inject
@Observablemodels into views, the advice is to just use a plain property (because the lifetime of the model is managed outside of the view -> However, can't there be scenarios in which this isn't true?).
Now consider the following example:
// parent
@Observable
final class ParentViewModel {
var number = 0
init() { print("parent init") }
deinit { print("parent deinit") }
func increment() { number += 1 }
}
struct ParentView<CV: View>: View {
@State var parentViewModel: ParentViewModel
@State private var isChildViewVisible = false
let childView: CV
var body: some View {
VStack {
if isChildViewVisible {
childView
}
Button("toggle child view visibility") { isChildViewVisible.toggle() }
Text(parentViewModel.number.description)
Button("increment", action: parentViewModel.increment)
}
}
}
// child
@Observable
final class ChildViewModel {
var number = 0
init() { print("child init") }
deinit { print("child deinit") }
func decrement() { number -= 1 }
}
struct ChildView: View {
@State var childViewModel: ChildViewModel
var body: some View {
VStack {
Text(childViewModel.number.description)
Button("decrement", action: childViewModel.decrement)
}
}
}
// composition
enum Assembly {
static func assemble() -> UIViewController {
let childView = ChildView(
childViewModel: ChildViewModel()
)
let view = ParentView(
parentViewModel: ParentViewModel(),
childView: childView
)
return UIHostingController(rootView: view)
}
}
Perspective 1:
The usage of @State var parentViewModel: ParentViewModel and @State var childViewModel: ChildViewModel is appropriate here.
Since we are never constructing Parent/ChildView in the body of other views (we only initialize it in Assembly) it's fine that we only set the initial value of the view models. To some degree, this might be even intended. However, you could argue that this is not ideal because now the views are coupled to a specific use case (i.e. they can/should only be initialized via the Assembly).
Perspective 2:
The usage of @State var parentViewModel: ParentViewModel and @State var childViewModel: ChildViewModel is not appropriate here and we should instead use let parentViewModel: ParentViewModel and let childViewModel: ChildViewModel. If required, we could also use @Bindable to get automatic bindings for individual attributes on the view models.
UIHostingController has the rootView attribute so one could argue that the lifetime of the view models is managed outside of the views because of the dependency graph (rootView -> parentViewModel and rootView -> childView -> childViewModel). So the lifetime of the view models would be bound to the lifetime of the UIHostingController. In the real app we use a custom hosting controller that wraps UIHostingController and looks something like this:
class HostingController<Content: View>: UIViewController {
private var rootView: Content
public init(rootView: Content) {
self.rootView = rootView
...
}
...
}
What are your thoughts on this? Which perspective is "correct"?