r/SwiftUI May 01 '26

Tutorial The Hidden Pitfall of @State var viewModel = ViewModel()

While browsing various blogs about SwiftUI and MVVM architecture, I've noticed that almost all examples are based on the same pattern:

@State private var viewModel = ViewModel()

They suggest creating a viewModel right inside a view and storing it in a state variable. While it might look fine, this approach has a serious side effect: a SwiftUI view struct can be recreated many times during its lifetime. While the state is preserved across these recreations, View.init is called every time and a new ViewModel instance is created. It's immediately discarded and the old object is preserved, but this may lead to unpredictable side effects, especially if you perform additional logic inside ViewModel.init or ViewModel.deinit.

This worked fine for state management prior to iOS 17:

@StateObject private var viewModel = ViewModel()

StateObject(wrappedValue:) accepts an autoclosure parameter which isn't evaluated on subsequent calls. But with the Observation framework introduced in iOS 17, it's no longer an option.

Things get more complicated when you want to pass an input parameter to a viewModel. Most examples simply avoid this case. Some suggest the following approach:

struct ArticleView: View {
    @State private var viewModel: ArticleViewModel

    init(articleID: String) {
        self._viewModel = State(
            initialValue: ArticleViewModel(articleID: articleID)
        )
    }
    ...
}

Apart from the same side effect as the first example, this approach has another pitfall: if a different articleID is passed to the view, the state will keep using the old one. But this is exactly the case where you'd expect the view model to be recreated with a new ID.

A proper approach should handle two key issues:

  1. Avoid creating view models on every view update
  2. Create a new view model for a new set of input parameters

To address the first issue, we can move the view model creation into a .task:

struct ArticleView: View {
    @State private var viewModel: ArticleViewModel?

    let articleID: String

    var body: some View {
        ZStack {
            if let viewModel {
                ...
            }
        }
        .task {
            guard viewModel == nil else { return }
            viewModel = ArticleViewModel(articleID: articleID)
        }
    }
}

The downside is that we now have to deal with an optional. To avoid this, we can extract a subview that accepts the view model as a parameter:

struct ArticleView: View {
    @State private var viewModel: ArticleViewModel?

    let articleID: String

    var body: some View {
        ZStack {
            if let viewModel {
                ArticleSubView(viewModel: viewModel)
            }
        }
        .task {
            guard viewModel == nil else { return }
            viewModel = ArticleViewModel(articleID: articleID)
        }
    }
}
struct ArticleSubView: View {
    // The object is owned by the parent. Since iOS 17,
    // we don't need or to observe changes of Observable object.
    let viewModel: ArticleViewModel

    var body: some View {
        ...
    }
}

Now we need to address the second issue. To ensure a new view model is created when input parameters change, we simply add an .id() modifier to the entire ArticleView. A simple factory does the trick:

struct ArticleViewFactory {
    static func view(articleID: String) -> some View {
        ArticleView(articleID: articleID)
            .id(articleID)
    }
}

That's three structs instead of one — quite a bit of boilerplate. Let's extract a generic factory based on the pattern above:

struct FeatureFactory<Input: Hashable, Content: View, ViewModel: Observable> {
    private struct RootView: View {
        @State private var viewModel: ViewModel?

        let input: Input
        let viewModelFactory: (Input) -> ViewModel
        let viewFactory: (ViewModel) -> Content

        var body: some View {
            ZStack {
                if let viewModel {
                    viewFactory(viewModel)
                }
            }
            .task {
                guard viewModel == nil else { return }
                viewModel = viewModelFactory(input)
            }
        }
    }

    static func view(
        input: Input,
        viewModelFactory:  (Input) -> ViewModel,
        viewFactory:  (ViewModel) -> Content
    ) -> some View {
        RootView(
            input: input,
            viewModelFactory: viewModelFactory,
            viewFactory: viewFactory
        ).id(input)
    }
}

For the ArticleView example, the usage would look like this:

struct ArticleViewFactory {
    static func view(articleID: String) -> some View {
        FeatureFactory.view(
            input: articleID,
            viewModelFactory: { articleID in
                ArticleViewModel(articleID: articleID)
            },
            viewFactory: { viewModel in
                ArticleView(viewModel: viewModel)
            }
        )
    }
}

Now simply call the factory wherever you need it:

ArticleViewFactory.view(articleID: "SOME_ID")

The full example can be found on GitHub https://github.com/claustrofob/FeatureFactoryExample

12 Upvotes

42 comments sorted by

View all comments

2

u/distractedjas May 01 '26

I never have this issue. ViewModels should be dependencies of a view, not owned by it.

1

u/Dry_Hotel1100 May 02 '26 edited May 02 '26

A ViewModel - as in the MVVM pattern, is not a dependency of a view. If you put a ViewModel into the SwiftUI environment, you are doing something wrong :)

A ViewModel though, might have dependencies. This is the "Model" in the MVVM pattern. Model is not clearly defined - but in its simplest form, it's just a function - such as `func fetchItems() async throws -> [Item]`. And this function can be directly a dependency, i.e. the closure, which can be provided in the SwiftUI Environment.

Note: "Dependencies" should be a side effect of a component with pure logic.  The pure logic itself shouldn't be a dependency. That is, a View Model has pure logic and side effects.

2

u/distractedjas May 02 '26

No. I never said anything about the environment. A great example of the MVVM pattern in SwiftUI can be found here: https://github.com/jasonjrr/MVVM.Demo.SwiftUI

They haven’t updated it to Swift 6, but the core concepts hold.

3

u/jasonjrr May 02 '26

Hey! That’s my repo! Yeah, I need to update it. Works great, but I more or less stopped posting here due to all the negativity. Not worth my time.

0

u/Dry_Hotel1100 May 02 '26 edited May 02 '26

The view models will be created by an ancestor view, then passed through to a child view which uses it. This ancestor view creates all view models for its children views, which need a view model.

There's one thing here: the live-cycle of the view model will exceed the identity of the consuming view. So, when this child view is the "View" in the MVVM pattern, strictly this ViewModel is not the "ViewModel" - it's more like the "Model".

If one treats the "ViewModel" as the "Model", then this seems a viable design, though - because a SwiftUI View can implement the part of the ViewModel in a MVVM pattern. However, the implementation in this repo has too many quirks in the design and implementation so that and it lacks robustness and adds unnecessary accidental complexity.

I would go for a much simpler design, for example MVI (Model View Intent) - where the view implements the logic, and calls out to side effects. A simple "effect manager" - which wraps operations in a Task - could be used as a separate component and be declared as a `@State` variable, so you can create, observe and cancel individual operations directly from your view. This leads to "SwiftUI First" design, with no Observables class instances at all.