ObervableObject being init multiple time, and not refreshing my view - swift

I have a structure like this:
contentView {
navigationView {
foreach {
NavigationLink(ViewA(id: id))
}
}
}
/// where ViewA contains an request trigger when it appears
struct ViewA: View {
#State var filterString: String = ""
var id: String!
#ObservedObject var model: ListObj = ListObj()
init(id: String) {
self.id = id
}
var body: some View {
VStack {
SearchBarView(searchText: $filterString)
List {
ForEach(model.items.filter({ filterString.isEmpty || $0.id.contains(filterString) || $0.name.contains(filterString) }), id: \.id) { item in
NavigationLink(destination: ViewB(id: item.id)) {
VStack {
Text("\(item.name) ")
}
}
}
}
}
.onAppear {
self.model.getListObj(id: self.id) //api request, fill data and call objectWillChange.send()
}
}
}
ViewB has the same code as ViewA: It receives an id, stores and requests an API to collect data.
But the viewB list is not being refreshed.
I also noticed that viewB's model property
#ObservedObject var model: model = model()
was instantiated multiple times.
Debugging, I found that every navigationLink instantiates its destination even before it is triggered. That's not a problem usually, but in my case i feel like the ViewB model is being instantiated 2 times, and my onAppear call the wrong one, reason why self.objectWillChange.send() not refreshing my view.

There are two issues here:
SwiftUI uses value types that that get initialized over and over again each pass through body.
Related to #1, NavigationLink is not lazy.
#1
A new ListObj gets instantiated every time you call ViewA.init(...). ObservedObject does not work the same as #State where SwiftUI keeps careful track of it for you throughout the onscreen lifecycle. SwiftUI assumes that ultimate ownership of an #ObservedObject exists at some level above the View it's used in.
In other words, you should almost always avoid things like #ObservedObject var myObject = MyObservableObject().
(Note, even if you did #State var model = ListObj() it would be instantiated every time. But because it's #State SwiftUI will replace the new instance with the original before body gets called.)
#2
In addition to this, NavigationLink is not lazy. Each time you instantiate that NavigationLink you pass a newly instantiated ViewA, which instantiates your ListObj.
So for starters, one thing you can do is make a LazyView to delay instantiation until NavigationLink.destination.body actually gets called:
// Use this to delay instantiation when using `NavigationLink`, etc...
struct LazyView<Content: View>: View {
var content: () -> Content
var body: some View {
self.content()
}
}
Now you can do NavigationLink(destination: LazyView { ViewA() }) and instantiation of ViewA will be deferred until the destination is actually shown.
Simply using LazyView will fix your current problem as long as it's the top view in the hierarchy, like it is when you push it in a NavigationView or if you present it.
However, this is where #user3441734's comment comes in. What you really need to do is keep ownership of model somewhere outside of your View because of what was explained in #1.

If your #ObservedObject is being initialized multiple times, it is because the owner of the object is refreshed and recreated every time it has state changes. Try to use #StateObject if your app is iOS 14 and above. It prevents the object from being recreated when the view refreshes.
https://developer.apple.com/documentation/swiftui/stateobject
When a view creates its own #ObservedObject instance it is recreated
every time a view is discarded and redrawn. On the contrary a #State
variable will keep its value when a view is redrawn. A #StateObject is
a combination of #ObservedObject and #State - the instance of the
ViewModel will be kept and reused even after a view is discarded and
redrawn
What is the difference between ObservedObject and StateObject in SwiftUI

Related

SwiftUI: How do I share an ObservedObject between child views without affecting the parent view

TL;DR: If I have a view containing a NavigationSplitView(sidebar:detail:), with a property (such as a State or StateObject) tracking user selection, how should I make it so that the sidebar and detail views observe the user selection, but the parent view does not?
Using SwiftUI's new NavigationSplitView (or the deprecated NavigationView), a common paradigm is to have a list of selectable items in the sidebar view, with details of the selected item in the detail view. The selection, of course, needs to be observed, usually from within an ObservedObject.
struct ExampleView: View {
#StateObject private var viewModel = ExampleViewModel()
var body: some View {
NavigationSplitView {
SidebarView(selection: $viewModel.selection)
} detail: {
DetailView(item: viewModel.selection)
}
}
}
struct SidebarView: View {
let selectableItems: [Item] = []
#Binding var selection: Item?
var body: some View {
List(selectableItems, selection: $viewModel.selected) { item in
NavigationLink(value: item) { Text(item.name) }
}
}
}
struct DetailView: View {
let item: Item?
var body: some View {
// Details of the selected item
}
}
#MainActor
final class ExampleViewModel: ObservableObject {
#Published var selection: Item? = nil
}
This, however, poses a problem: the ExampleView owns the ExampleViewModel used for tracking the selection, which means that it gets recalculated whenever the selection changes. This, in turn, causes its children SidebarView and DetailView to be redrawn.
Since we want those children to be recalculated, one might be forgiven for thinking that everything is as intended. However, the ExampleView itself should not be recalculated in my opinion, because doing so will not only update the child views (intended), but also everything in the parent view (not intended). This is especially true if its body is composed of other views, modifiers, or setup work. Case in point: in this example, the NavigationSplitView itself will also be recalculated, which I don't think is what we want.
Almost all tutorials, guides and examples I see online use a version of the above example - sometimes the viewModel is passed as an ObservedObject, or as an EnvironmentObject, but they all share the same trait in that the parent view containing the NavigationSplitView is observing the property that should only be observed by the children of NavigationSplitView.
My current solution is to initiate the viewmodel in the parent view, but not observe it:
struct ExampleView: View {
let viewModel = ExampleViewModel()
...
}
#MainActor
final class ExampleViewModel: ObservableObject {
#Published var selection: Item? = nil
nonisolated init() { }
}
This way, the parent view will remain intact (at least in regards to user selection); however, this will cause the ExampleViewModel to be recreated if anything else would cause the ExampleView to be redrawn - effectively resetting our user selection. Additionally, we are unable to pass any of the viewModel's properties as bindings. So while it works for my current use-case, I don't consider this an effective solution.

Updating property wrapper like #StateObject, affects other view rendering that does not use that property

When using different property wrappers associated with view updates, changes in one place affect rendering of views that do not use that property.
struct ContentView: View {
#StateObject var viewModel = MyViewModel()
#State var thirdTitle = "thirdTitle"
var body: some View {
VStack {
Text(viewModel.firstTitle)
.background(.random)
Text(viewModel.secondTitle)
.background(.random)
Text(thirdTitle)
.background(.random)
Button("Change First Title") {
viewModel.chageFirstTitle()
}
}
}
}
class MyViewModel: ObservableObject {
#Published var firstTitle = "firstTitle"
#Published var secondTitle = "secondTitle"
func chageFirstTitle() {
firstTitle = "hello world"
}
}
I understand that the reason why the Text exposing the viewModel.secondTitle is re-rendered is because the #StateObject varviewModel = MyViewModel() dependency changed when the `viewModel.firstTitle changed.
However, I don't know why Text using #State var thirdTitle = "thirdTitle" is re-rendered too. In WWDC21 session Demystify SwiftUI, I saw that the view is re-rendered only when the related dependency is updated according to the dependency graph. But, even though the thirdTitle is irrelevant to the change of the viewModel, third Text using that dependency is re-rendered and the background color is changed.
What's even more confusing is that if I seperate the third Text into a separate view ( ThirdView ) and receive the thirdTitle using #Binding, the background color does not change because it is not re-rendering at that time.
struct ContentView: View {
#StateObject var viewModel = MyViewModel()
#State var thirdTitle = "thirdTitle"
var body: some View {
VStack {
Text(viewModel.firstTitle)
.background(.random)
Text(viewModel.secondTitle)
.background(.random)
ThirdView(text: $thirdTitle)
.background(.random)
Button("Change First Title") {
viewModel.chageFirstTitle()
}
}
}
}
struct ThirdView: View {
#Binding var text: String
var body: some View {
Text(text)
.background(.random)
}
}
Regarding the situation I explained, could you help me to understand the rendering conditions of the view?
To render SwiftUI calls body property of a view (it is computable, i.e. executes completely on call). This call is performed whenever any view dependency, i.e. dynamic property, is changed.
So, viewModel.chageFirstTitle() changes dependency for ContentView and ContentView.body is called and every primitive in it is rendered. ThirdView also created but as far as its dependency is not changed, its body is not called, so content is not re-rendered.
A few things wrong here. We don't use view model objects in SwiftUI for view data, it's quite inefficient/buggy to do so. Instead, use a struct with mutating funcs with an #State. Pass in params to sub-Views as lets for read access, #Binding is only when you need write access. In terms of rendering, first of all body is only called if the let property is different from the last time the sub-View is init, then it diffs the body from the last time it was called, if there are any differences then SwiftUI adds/removes/updates actual UIKit UIViews on your behalf, then actual rendering of those UIViews, e.g. drawRect, is done by CoreGraphics.
struct ContentViewConfig {
var firstTitle = "firstTitle"
var secondTitle = "secondTitle"
mutating func changeFirstTitle() {
firstTitle = "hello world"
}
}
struct ContentView: View {
#State var config = Config()
...
struct ThirdView: View {
let text: String
...
Combine's ObservableObject is usually only used when needing to use Combine, e.g. using combineLatest with multiple publishers or for a Store object to hold the model struct arrays in #Published properties that are not tied to a View's lifetime like #State. Your use case doesn't look like a valid use of ObservableObject.

SwiftUI NavigationView popping when observed object changes

There are three views. Main view, a List view (displaying a list of locations) and a Content view (displaying a single location). Both the List view and the Content view are watching for changes from LocationManager(GPS position).
If the user NavigateLinks to Content view from the List view and then the observed LocationManager changes then ContentView is popped off and the List view is displayed.
I have NavigationViewStyle set to .stack (which some found to help) but it had no effect.
Does anyone know what I am doing wrong?
// Main view
#StateObject var locations = Locations()
var body: some Scene {
WindowGroup {
TabView(){
NavigationView {
ListView()
}.navigationViewStyle(.stack).tabItem {
Text("List")
}
}.environmentObject(locations)
}
}
// List view
#EnvironmentObject var locations: Locations
#ObservedObject private var locationManager = LocationManager()
var body: some View {
List(){
ForEach(locations, id: \.id) {
loc in
NavigationLink(destination: ContentView(location: loc, locationManager: locationManager)) {
Text(loc.name)
}.isDetailLink(false)
}
}.listStyle(PlainListStyle())
}.navigationTitle("List")
// Content/Detail view
let location: Location
let locationManager: LocationManager
var body: some View { Text("I am detail") }
What I ended up doing was having two observable objects, one for the List and one for Detail.
The observable objects are GPS coordinates so that when the Content view is displayed (you can keep a track of that by adding a state in onAppear for the View) you don't allow changes to the observable object for the List.
In other words the observable object that the List view had, didn't change if the Content view was visible. I did this in code by simply ignoring any location updates in that object if the current view was Content/Detail.
This worked perfectly.
So instead of this:
I did this
It pops because your objects are not unique. Think about this:
You are inside a view called Alen now you refresh that view and it's called Alenx, SwiftUI pops you back because the data is different.
The way to make it stay is to make the elements unique (Identifiable), for this you can assign some unique id to your models, here's 1 way of doing it:
struct Example: Identifiable {
let id = UUID()
var someValue: String
private enum CodingKeys: String, CodingKey { case someValue }
}

SwiftUI - How to deinit a StateObject when navigating back?

I want my #StateObject to be deinitialized as soon as possible after I navigate back, but it seems that the object is held in memory. "Deint ViewModel" is not being printed on back navigation, its first printed after I navigate again to the View I was coming from. Is there a way to release the #StateObject from memory on back navigation?
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: TestView(), label: { Text("Show Test View") })
}
}
}
struct TestView: View {
#StateObject private var viewModel = ViewModel()
var body: some View {
Text("Test View")
}
}
final class ViewModel: ObservableObject {
deinit {
print("Deint ViewModel")
}
}
I don't have a brilliant answer on all situations that prevent the deinitialization the #StateObject, but I found that leaving background async tasks running prevents the deinitialization.
In my case, I had several cancellables registered to listen to PassthroughSubject and/or CurrentValueSubject (that I used to handle external changes on my model and exposing the result to the view), but I never cancelled them. As soon as I did it in the view using .onDisappear, it worked.
So my views all "subscribe" to the view model (I have a viewModel.subscribe() method) using .onAppear and then "unsubscribe" to the view model (I have a viewModel.subscribe() method) using .onDisappear. Doing so, the #StateObject is deinitialized when the view is dismissed.
Adding to GregP's answer:
If you have removed all of your cancellables on onDisappear and deinit is still not being called, you may use the Debug Memory Graph
Navigate to the object, see its tree and see what else is referencing it.
For example I had it looking like this:
Because there was another object referencing this object it didn't get removed from the memory (ARC). So all I had to do was remove it from being the delegate along with cancelling the cancellables and deinit got called
I think you should use #ObservedObject private var viewModel: ViewModel instead, then inject new ViewModel instance from outside of TestView

SwiftUI: #ObservedObject redraws every view

I try to implement the MVVM way correctly in SwiftUI, so I came up with this (simplified) Model and ViewModel:
struct Model {
var property1: String
var property2: String
}
class ViewModel: ObservableObject {
#Published var model = Model(property1: "this is", property2: "a test")
}
Using this in a View works fine, but I experienced some bad performance issues, as I extended the ViewModel with some computed properties and some functions (as well the Model itself is more complicated). But let's stay with this example, because it demonstrates perfectly, what I think is a big problem in SwiftUI itself.
Imagine, you have those views to display the data:
struct ParentView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
print("redrawing ParentView")
return ChildView(viewModel: self.viewModel)
}
}
struct ChildView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
print("redrawing ChildView")
return VStack {
ViewForTextField(property: self.$viewModel.model.property1)
ViewForTextField(property: self.$viewModel.model.property2)
}
}
}
struct ViewForTextField: View {
#Binding var property: String
var body: some View {
print("redrawing textView of \(self.property)")
return TextField("...", text: self.$property)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
Now entering text into one of the TextField leads to a redraw of every View in my window! The print output is:
redrawing ParentView
redrawing ChildView
redrawing textView of this is
redrawing textView of a test
redrawing ParentView
redrawing ChildView
redrawing textView of this isa
redrawing textView of a test
redrawing ParentView
redrawing ChildView
redrawing textView of this isab
redrawing textView of a test
...
As I can see, SwiftUI redraws every view, because every view is listening to the ObservedObject.
How can I tell SwiftUI, that it only should redraw those views, where really happened any changes?
Actually MVVM means own model for every view, which is updated on model changes, not one model for all views.
So there is no needs to observe viewModel in ParentView as it does not depend on it
struct ParentView: View {
var viewModel: ViewModel // << just member to pass down in child
var body: some View {
print("redrawing ParentView")
return ChildView(viewModel: self.viewModel)
}
}
Alternate is to decompose view model so every view has own view sub-model, which would manage updates of own view.
If all your views observe the same thing, and that thing changes, then all your views will re-render. This is by definition. There's no option to configure to optionally update certain views.
That being said, you can work around it by manipulating what changes you'd like to publish.
E.g.;
class ViewModel: ObservableObject {
#Published var model = Model(property1: "this is", property2: "a test")
var mytext = "some text" // pass this as binding instead of model.propertyX
}
Now when textfield changes, mytext changes, but this change won't be published thus won't trigger further view updates. And you still can access it from view model.
I personally would recommend using #State and #EnvironmentObject instead of "view model". They are the built-in way to handle binding and view updates with tons of safe-guards and supports.
Also you should use value type instead of reference type as much as possible. MVVM from other languages didn't take this into consideration.