21

I have created an ObservableObject in a View.

@ObservedObject var selectionModel = FilterSelectionModel()

I put a breakpoint inside the FilterSelectionModel's init function and it is called multiple times. Because this View is part of a NavigationLink, I understand that it gets created then and along with it, the selectionModel. When I navigate to the View, the selectionModel is created again.

In this same View I have a "sub View" where I pass the selectionModel as an EnvironmentObject so the sub-view can change it.

AddFilterScreen().environmentObject(self.selectionModel)

When the sub view is dismissed, the selectionModel is once more created and the changes made to it have disappeared.

Interesting Note: At the very top level is a NavigationView. IF I add

.navigationViewStyle(StackNavigationViewStyle())

to this NavigationView, my selectionModel's changes disappear. BUT if I do not add the navigationStyle, the selectionModel's changes made in the sub view remain!! (But I don't want a split nav view, I want a stacked nav view)

In both cases - with or without the navigationStyle, the selectionModel is created multiple times. I can't wrap my head around how any of this is supposed to work reliably.

P. Ent
  • 1,654
  • 1
  • 12
  • 22
  • 3
    *UPDATE*: I "solved" this problem by breaking encapsulation and moving the `FilterSelectionModel` to the top `ContentView`. But I don't like this solution since this model is needed only by the set of views involved in my app's search functions. The entire app does not need to know about this model. – P. Ent Dec 30 '19 at 15:57
  • I have been struggling with the same problem. Do not know how to solve this in an elegant way without breaking encapsulation as you say. – mort Jan 26 '20 at 15:45
  • Any new update? I'm having the same issue. – Luke Irvin Mar 14 '20 at 22:39
  • No updates as of right now. – P. Ent Mar 16 '20 at 20:23
  • 1
    Same issue. I ended up using a singleton for my view model as a workaround... – Genki May 02 '20 at 22:32

2 Answers2

16

Latest SwiftUI updates have brought solution to this problem. (iOS 14 onwards)

@StateObject is what we should use instead of @ObservedObject, but only where that object is created and not everywhere in the sub-views where we are passing the same object.

For eg-

class User: ObservableObject {
    var name = "mohit"
}


struct ContentView: View {
  @StateObject var user = User()

  var body: some View {
    VStack {
      Text("name: \(user.name)")
      NameCount(user: self.user)
   }
  }
}


struct NameCount: View {
  @ObservedObject var user

  var body: some View {
    Text("count: \(user.name.count)")
  }
}

In the above example, only the view responsible (ContentView) for creating that object is annotating the User object with @StateObject and all other views (NameCount) that share the object is using @ObservedObject.

By this approach whenever your parent view(ContentView) is re-created, the User object will not be re-created and it will persist its @State, while your child views just observing to the same User object doesn't have to care about its re-creation.

Nimantha
  • 6,405
  • 6
  • 28
  • 69
mohit kejriwal
  • 1,755
  • 1
  • 10
  • 19
9

You can instantiate the observable object in the init method, in this way you will be able to hold its value or the value won't disappear.

Instantiate this way in the view file.

@ObservedObject var selectionModel : FilterSelectionModel

init() {
   selectionModel = FilterSelectionModel(value : "value to be saved from disappearing")
}

Instantiate this way in the viewModel file.

class FilterSelectionModel : ObservableObject {

  @Published var value : String

  init(value : String) {
     self.value = value
  }

}

This is a workaround that I found, but still, the init method is called multiple times and I didn't get any success with this issue.

In order to stop multiple initializing of the ViewModels as the view is declared in the Navigation View and SwiftUI uses struct which is a value type, so eventually these are initialized before the view is presented, therefore you can convert that view into a LazyView, so that it will only be initialized once the view is about to be presented or shown.

// Use this to delay instantiation when using `NavigationLink`, etc...
struct LazyView<Content: View>: View {
    var content: () -> Content
    var body: some View {
       self.content()
    }
}

You can call it like this...

 NavigationLink(destination: LazyView { ViewTobePresented() }) 
Anshuman Singh
  • 1,018
  • 15
  • 17
  • This should be the accepted answer. The LazyView works. – Alexander Sep 01 '20 at 14:20
  • Objects should only be init in the property wrapped definition, otherwise you'll be initing and destroying objects when the View structs are created on every update. – malhal Apr 06 '21 at 22:03
  • @malhal can you provide an example, how to do it with property wrapper definition. – Anshuman Singh Apr 08 '21 at 10:57
  • 1
    @AnshumanSingh the pattern Apple use (see Data Essentials in SwiftUI WWDC 2020 at 20:20) is to init the @ StateObject with no params and then call a method on the object supplying it with the params in the View's onAppear. – malhal Apr 09 '21 at 17:41