Earlier this week I learned about @StateObject initializer when reviewing a pull request. The idea was to create a view model for a SwiftUI view using some injected parameter and keep the view model as a @StateObject so that the its state is persisted when the view is redrawn. The view creates the @StateObject like so:

class ItemViewModel: ObservableObject {
    @Published var itemName: String
    init(item: Item) {
        self.itemName = item.name
    }
}

struct ItemView: View {
    @StateObject private var viewModel: ItemViewModel
    init(item: Item) {
        _viewModel = StateObject(wrappedValue: ItemViewModel(item: item))
    }

    var body: some View {
        TextField("Item Name", text: viewModel.$itemName)
    }
}

The problem with this initializer is that it’s not meant to be used directly. As Apple documentation says it:

You don’t call this initializer directly. Instead, declare a property with the @StateObject attribute in a View, App, or Scene, and provide an initial value

More interestingly, an answer on the WWDC21’s SwiftUI Lab confirms that this is an acceptable use. I’m not quite happy with this answer though, because it’s not cool to go against the recommended use. So we can fix this by injecting the view model from outside, right?

struct ItemView: View {
    @StateObject var viewModel: ItemViewModel

    var body: some View {
        Text("Test: \(viewModel.id)")
    }
}

struct ItemListView: View {
    @ObservedObject var viewModel: ItemListViewModel

    var body: some View {
        ForEach(viewModel.items) { item in
            ItemView(viewModel: ItemViewModel(item: item))
        }
    }
}

But this doesn’t seem right!

I’ve read several articles about the difference between @StateObject and @ObservedObject, and the general idea is simple: You should use @StateObject if the view you’re using creates the instance of the ObservableObject itself. If it does not, use @ObservedObject instead.

Consider @StateObject something similar to @State but to use with ObservableObject. They both are created and owned by the SwiftUI view. Their values are controlled internally and persisted by SwiftUI internally throughout re-renders of the view. @ObservedObject, however, is not persisted by SwiftUI.

Back to our initial code problem. Using the injected @StateObject is incorrect, so we should go ahead and replace it with @ObservedObject. Normally we’re done here, since we usually want fresh instances of items every time a list redraws itself.

But what if we want to keep the text field value when rotating the device? When device orientation changes, the item list is redrawn, which creates new views for the child items with new instances of their view models. This will reset the content of a ItemView’s text field to its initial value.

The solution is to hold on the the item view models to persist the state of the item views:

class ItemListViewModel: ObservableObject {
    private(set) var itemViewModels: [ItemViewModel]
    @Published var items: [Item]
    
    init(items: Item) {
        self.items = items
        self.itemViewModel = items.map { ItemViewModel(item: $0) }
    }
}

struct ItemListView: View {
    @ObservedObject var viewModel: ItemListViewModel

    var body: some View {
        ForEach(viewModel.items) { item in
            viewModel.itemViewModels.first(where: { $0.item == item }).map { viewModel in
                ItemView(viewModel: viewModel)
            }
        }
    }
}

struct ItemView: View {
    @ObservedObject var viewModel: ItemViewModel

    var body: some View {
        Text("Test: \(viewModel.id)")
    }
}

Since our ItemListViewModel now keeps references of all the items’ view models, the text field of each item view is kept intact when the view is redrawn! πŸŽ‰

So whenever you find yourself needing an ObservableObject in a SwiftUI view, ask yourself two questions:

  • Do I need external (injected) data to create the object? If yes, go for @ObservedObject, otherwise @StateObject.
  • I need to use an @ObservedObject, but do I need to persist its states? If yes, keep a reference of the object somewhere, otherwise, feel free to create a new instance for every view.

I hope these tips are helpful for other folks working with SwiftUI in their projects!

More readings if you’re interested: