SwiftUI - How to deinit a StateObject when navigating back? - swift

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

Related

SwiftUI navigation link does not load

When clicking on the navigation link in my SwiftUI application, the screen freezes and I can see the memory doubing every second - almost getting to a 1GB of memory before I terminate the application.
I have a simple navigation link as follows:
NavigationLink {
FeedbackView(viewModel: .init())
} label: {
HStack {
Label("Feedback", systemImage: "bubble.left.circle")
.fontWeight(.semibold)
Spacer()
Image(systemName: "chevron.right")
}
}
Upon clicking on this navigtion link, the screen does not go to the next view and instead freezes. I am unable to tap anything else in the iOS simulator. The memory skyrockets and continues to do so until I stop the application.
The view model is it initializing in the FeedbackView call is as follows.
import Foundation
import Dependencies
class FeedbackViewModel: ObservableObject {
}
The view is below.
import SwiftUI
struct FeedbackView: View {
#ObservedObject var viewModel: FeedbackViewModel
var body: some View {
Text("loaded feedback")
}
}
If I remove .init() from the FeedbackView call within the NavigationLink, and instead initialize the FeedbackViewModel in the FeedbackView itself, I also get the same issue. I am rather new to iOS development and am not sure of which xCode tools to use that could help me diagnose this bug.
First: use #StateObject instead:
struct FeedbackView: View {
#StateObject var viewModel: FeedbackViewModel
var body: some View {
Text("loaded feedback")
}
}
Why: unlike #ObservedObject, #StateObject won't get destroyed and re-instantiated every time the view struct redraws. Never create #ObservedObject from within the view itself (Check this article for more details)
Second: how to initialize the #StateObject?
Really depends on your use case, but you could do this:
struct FeedbackView: View {
...
init(_ providedViewModel: ViewModel = ViewModel()) {
_viewModel = StateObject(wrappedValue: providedViewModel)
}
...
The "special notation" _viewModel refers to actual property wrapped inside StateObject property wrapper.
This way parent can either pass the model in (if there's some data it needs to initialize), or let the default model to be created:
NavigationLink {
FeedbackView()
}

Why is viewModel not deiniting with a NavigationView?

I've got a problem with an object not being destroyed when put into the environment and used with a NavigationView. If a view creates an object, creates a NavigationView and inserts the object into the environment, then when that view disappears the object should be deallocated, but it's not.
The example app below should demonstrate this issue!
Look at the LoggedInView where the ViewModel is created: When the user logs out, the LoggedInView disappears, but the deinit of ViewModel is not called.
What needs to change so that the ViewModel is deinitialized when the LoggedInView disappears?
Here is an example I've created to demo the issue:
struct ContentView: View {
#State private var loggedIn = false
var body: some View {
if loggedIn {
LoggedInView(loggedIn: $loggedIn)
} else {
Button("Log In") {
loggedIn = true
}
}
}
}
struct LoggedInView: View {
#Binding var loggedIn: Bool
#StateObject private var viewModel = ViewModel()
var body: some View {
NavigationView {
Button("Log Off") {
loggedIn = false
}
}
.environmentObject(viewModel)
}
}
class ViewModel: ObservableObject {
#Published var name = "Steve"
private let uuid = UUID().uuidString
init() {
print("initing " + uuid)
}
deinit {
print("deiniting " + uuid)
}
}
If I comment out either the NavigationView { or the .environmentObject(viewModel) then the ViewModel is deallocated as expected.
If I log in and out a few times the console looks like this:
initing 5DA78F23-6EC7-4F06-82F8-6066BB82272E
deiniting 5DA78F23-6EC7-4F06-82F8-6066BB82272E
initing 5BEABAF1-31D0-465E-A35E-94E5E141C453
deiniting 5BEABAF1-31D0-465E-A35E-94E5E141C453
initing 7A4F54FA-7C32-403B-9F47-1ED6289F68B5
deiniting 7A4F54FA-7C32-403B-9F47-1ED6289F68B5
However, if I run the code as above then the console looks like this:
initing D3C8388D-A0A0-42C9-B80A-21254A868060
initing 8DFFF7F7-00C4-4B9F-B592-85422B3A01C0
initing F56D4869-A724-4E10-8C1A-CCA9D99EE1D3
initing ED985C33-C107-4D8C-B51D-4A9D1320F784
deiniting 8DFFF7F7-00C4-4B9F-B592-85422B3A01C0
initing CCD18C41-0196-44B0-B414-697B06E0DE2F
deiniting ED985C33-C107-4D8C-B51D-4A9D1320F784
initing EAA0255A-5F2E-406D-805A-DE2D675F7964
initing 49908FAA-8573-4EBE-8E8B-B0120A504DDA
What's interesting here is that the User object does sometimes get deallocated but it seems only after it's created multiple instances and it never seems to deallocated the first few that were created.
This is happening, IMO, because you are creating two strong pointers to the class User, one by making it an #StateObject and other by putting it on "environmentObject".
Try to decouple your User model as a struct and store the UUID there
Create a class to serve as a Storage Only View Model it will only Publish the User struct
Instantiate this View Model as #StateObject into your view and use your model as you wish.
Get rid of your environmentObject, pass along your view model as needed.
PS: Only use environmentObject after a deep understanding about what they are. For now I'll not lecture on environmentObjects
The most straight-forward solution for your simplified example would be to move the .environmentObject(viewModel) modifier: instead of applying it to the NavigationView, apply it to the Button.
This works if the ViewModel is only needed in children of LoggedInView which aren't wrapped in a NavigationLink.
However, when you apply this approach to a scenario that actually expects the ViewModel to be accessible in any destination views wrapped in NavigationLinks, you would run into the problem of NavigationLink shielding its destination from any .environmentObjects that aren't either a) applied to the enclosing NavigationView or b) applied to NavigationLink's destinations directly.
I've tried a couple of approaches to work around this in your specific setup, but SwiftUI's "optimizations" around if/else statements, as well as #StateObject initialization, cause even solutions using the .id() modifier, AnyView(), and EquatableView() to fail.
All of these should provide custom control over a View's identity, as described in this WWDC talk: Demystify SwiftUI but I believe the specific structure you're looking for touches on too many idiosyncratic components to provide a clean outcome, leaving me to recommend the following as the only solution that maintains your described hierarchy but also deinits the ViewModel as desired:
Remove .environmentObject() from the NavigationView
Apply .environmentObject(viewModel) to all immediate child views (or a Group/Container) inside LoggedInView that need access to the ViewModel
Do the same for any destination views inside of NavigationLinks, but apply .environmentObject(viewModel) directly to the destination view, not to the enclosing NavigationLink.
So a sample implementation looks like this:
struct LoggedInView: View {
#Binding var loggedIn: Bool
#StateObject private var viewModel = ViewModel()
var body: some View {
NavigationView {
List {
Button("Log Off") {
loggedIn = false
}
DeeperViewDependingOnViewModel()
.environmentObject(viewModel)
NavigationLink {
DeeperViewDependingOnViewModel()
.environmentObject(viewModel)
} label: {
Text("Go Deeper")
}
}
// This alone would suffice if no NavigationLink destinations need to access ViewModel:
//.environmentObject(viewModel)
}
}
}
This is an insane Apple issue which I'm dealing last several days. As I correctly understood such behaviour is a kinda of specific Apple logic for NavigationView/NavigationStackView added to the root of an app hierarchy.
Here is my investigations on it, maybe you can find a workaround which works for you:
NavigationView retains in memory all ObservableObjects shared via .environmentObject() method applied on it if and only if NavigationView is placed as a root view of an app.
So if you try to replace some view with NavigationView in a root of an hierarchy (login/logout like in your example), then all shared environment objects of navigation would retain in memory.
To prove this statement I tried to present NavigationView modally (using .sheet() or .fullScreenCover()) and share ObservableObject via .environmentObject() on it. When presented NavigationView dismisses, SwiftUI successfully deallocates all shared environments objects from memory.
Conclusion:
I assume that this is an Apple "feature" we didn't ask for...
The thing is that there are a lot of optimisations in SwiftUI and many of them are related to the memory management - inits/deinits of reference type state objectes stored in SwiftUI Views are optimised as well.
So Apple may assume that if NavigationView is a root view, then it always should be there (kinda logical... no? :D) and if you even replace it with other view, you always would return to NavigationView...
So for the sake of optimizations Apple doesn't remove state objects attached to it 🤔
Of course this is just an assumption. But you may try to present authorized flow view with navigation modally from your authorization view... this is the only solution I've found so far. If you find something better, ping me pleas :)
Also, you may change your architecture and make your ViewModel a shared one for the whole app and clean its state on logout.
But the most correct way, I suppose, is that we have to file a bug for Apple. Cause it's definitely a bug, not a feature :)

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.

SwiftUi Fullscreen cover without using state variable

i'm trying to do a simple login screen using swiftUI.
i put my isLogin into a class and set it as an EnviromentObject Variable.
How do I use it in a fullscreen cover?
My class
class AuthUser:ObservableObject {
#Published var isLogin:Bool = false
#Published var isCorrect:Bool = false
}
My View :
struct UIController: View {
#EnvironmentObject var userAuth : AuthUser
#State var showLogin:Bool = false
var body: some View {
homeLogin()
.fullScreenCover(isPresented: $showLogin, content: AfterLogin.init)
}
}
You can use binding directly to property of environment object, like
struct UIController: View {
#EnvironmentObject var userAuth : AuthUser
var body: some View {
homeLogin()
.fullScreenCover(isPresented: $userAuth.isLogin, content: AfterLogin.init)
}
}
The structure you're attempting to follow is called MVVM or Model-View-View-Model, which is nearly a requirement for swiftUI I believe the only thing that you're missing is the actual usage of that ViewModel, or in your case the AuthUser. So let's dig into that.
View Model
class AuthUser: ObservableObject {
#Published var isLogin = false
//Any other code, methods, constructors you want.
}
View
struct UIController: View {
#ObservedObject var userAuth = AuthUser()
var body: some View {
homeLogin()
.fullScreenCover(isPresented: $userAuth.isLogin, content: AfterLogin.init)
}
}
What did I change? I changed your #EnvironmentObject to a ObservedObject because in your case you're not likely to actually be using an environment object. What are those special tags?
ObservableObject, this means that it can be observed for state changes.
ObservedObject, an object that is watching for state changes.
Published, an object that has public availability.
EnvironmentObject, an object that is created for the environment and accessible wherever in the app. This particular object is not immediately disposed which is why we removed it in your example.
So what's the purpose of doing things this way? Well it has to do with abstraction and readability. MVVM frameworks provide a way to Bind a view to an object. That binding means that any changes on the view change the data, and any changes to the data change the view. Swift makes this concept simple enough. In your case we're binding the isPresented to the ObservedObject and checking that objects isLogin state. If it changes, the view is also changed.

ObervableObject being init multiple time, and not refreshing my view

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