[SOLVED] SwiftUI: ObservableObject does not persist its State over being redrawn

Issue

Problem

In Order to achieve a clean look and feel of the App’s code, I create ViewModels for every View that contains logic.

A normal ViewModel looks a bit like this:

class SomeViewModel: ObservableObject {

    @Published var state = 1

    // Logic and calls of Business Logic goes here
}

and is used like so:

struct SomeView: View {

    @ObservedObject var viewModel = SomeViewModel()

    var body: some View {
        // Code to read and write the State goes here
    }
}

This workes fine when the Views Parent is not being updated. If the parent’s state changes, this View gets redrawn (pretty normal in a declarative Framework). But also the ViewModel gets recreated and does not hold the State afterward. This is unusual when you compare to other Frameworks (eg: Flutter).

In my opinion, the ViewModel should stay, or the State should persist.

If I replace the ViewModel with a @State Property and use the int (in this example) directly it stays persisted and does not get recreated:

struct SomeView: View {

    @State var state = 1

    var body: some View {
        // Code to read and write the State goes here
    }
}

This does obviously not work for more complex States. And if I set a class for @State (like the ViewModel) more and more Things are not working as expected.

Question

  • Is there a way of not recreating the ViewModel every time?
  • Is there a way of replicating the @State Propertywrapper for @ObservedObject?
  • Why is @State keeping the State over the redraw?

I know that usually, it is bad practice to create a ViewModel in an inner View but this behavior can be replicated by using a NavigationLink or Sheet.
Sometimes it is then just not useful to keep the State in the ParentsViewModel and work with bindings when you think of a very complex TableView, where the Cells themself contain a lot of logic.
There is always a workaround for individual cases, but I think it would be way easier if the ViewModel would not be recreated.

Duplicate Question

I know there are a lot of questions out there talking about this issue, all talking about very specific use-cases. Here I want to talk about the general problem, without going too deep into custom solutions.

Edit (adding more detailed Example)

When having a State-changing ParentView, like a list coming from a Database, API, or cache (think about something simple). Via a NavigationLink you might reach a Detail-Page where you can modify the Data. By changing the data the reactive/declarative Pattern would tell us to also update the ListView, which would then “redraw” the NavigationLink, which would then lead to a recreation of the ViewModel.

I know I could store the ViewModel in the ParentView / ParentView’s ViewModel, but this is the wrong way of doing it IMO. And since subscriptions are destroyed and/or recreated – there might be some side effects.

Solution

Finally, there is a Solution provided by Apple: @StateObject.

By replacing @ObservedObject with @StateObject everything mentioned in my initial post is working.

Unfortunately, this is only available in ios 14+.

This is my Code from Xcode 12 Beta (Published June 23, 2020)

struct ContentView: View {

    @State var title = 0

    var body: some View {
        NavigationView {
            VStack {
                Button("Test") {
                    self.title = Int.random(in: 0...1000)
                }

                TestView1()

                TestView2()
            }
            .navigationTitle("\(self.title)")
        }
    }
}

struct TestView1: View {

    @ObservedObject var model = ViewModel()

    var body: some View {
        VStack {
            Button("Test1: \(self.model.title)") {
                self.model.title += 1
            }
        }
    }
}

class ViewModel: ObservableObject {

    @Published var title = 0
}

struct TestView2: View {

    @StateObject var model = ViewModel()

    var body: some View {
        VStack {
            Button("StateObject: \(self.model.title)") {
                self.model.title += 1
            }
        }
    }
}

As you can see, the StateObject Keeps it value upon the redraw of the Parent View, while the ObservedObject is being reset.

Answered By – KonDeichmann

Answer Checked By – Cary Denson (BugsFixing Admin)

Leave a Reply

Your email address will not be published. Required fields are marked *