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:
body
.NavigationLink
is not lazy.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.)
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 you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With