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 :)
I am developing an app which uses UIKit. I have integrated a UIKit UIViewController inside SwiftUI and everything works as expected. I am still wondering if there is a way to 'know' when a SwiftUI View is completely gone.
My understanding is that a #StateObject knows this information. I now have some code in the deinit block of the corresponding class of the StateObject. There is some code running which unsubscribes the user of that screen.
The problem is that it is a fragile solution. In some scenario's the deinit block isn't called.
Is there any recommended way to know if the user pressed the back button in a SwiftUI View (or swiped the view away)? I don't want to get notified with the .onDisppear modifier because that is also called when the user taps somewhere on the screen which adds another view to the navigation stack. I want to run some code once when the screen is completely gone.
Is there any recommended way to know if the user pressed the back button in a SwiftUI View (or swiped the view away)?
This implies you're using a NavigationView and presenting your view with a NavigationLink.
You can be notified when the user goes “back” from your view by using one of the NavigationLink initializers that takes a Binding. Create a custom binding and in its set function, check whether the old value is true (meaning the child view was presented) and the new value is false (meaning the child view is now being popped from the stack). Example:
struct ContentView: View {
#State var childIsPresented = false
#State var childPopCount = 0
var body: some View {
NavigationView {
VStack {
Text("Child has been popped \(childPopCount) times")
NavigationLink(
"Push Child",
isActive: Binding(
get: { childIsPresented },
set: {
if childIsPresented && !$0 {
childPopCount += 1
}
childIsPresented = $0
}
)
) {
ChildView()
}
}
}
}
}
struct ChildView: View {
var body: some View {
VStack {
Text("Sweet child o' mine")
NavigationLink("Push Grandchild") {
GrandchildView()
}
}
}
}
struct GrandchildView: View {
var body: some View {
VStack {
Text("👶")
.font(.system(size: 100))
}
}
}
Note that these initializers, and NavigationView, are deprecated if your deployment target is iOS 16. In that case, you'll want to use a NavigationStack and give it a custom Binding that performs the pop-detection.
So I'm relatively new to swift and coding in general, I'm currently trying to develop an app that essentially operates like a to do list. My goal for the home page is pretty basic. The logo centered at the top and a button to create "New Lists". I've gone back and forth on how to manage the various lists (using buttons or using listviews). But ideally I want the New List button to create a new button (which accesses the newly created list) and also take the user to the newly created list to name it and add contents etc.
I'm currently exploring the use of NavigationLinks and Navigation View (Code Below)
import SwiftUI
struct ContentView: View {
#State private var isShowingNewList = false
var body: some View {
NavigationView{
VStack(spacing: 30) {
NavigationLink(destination: Text("This is gonna be a new list"), isActive: $isShowingNewList) { EmptyView()}
Button("New List!!") {
self.isShowingNewList = true
}
.buttonStyle(.bordered)
.offset(y: -100)
}
.navigationBarTitle("Nudge")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I'm thinking navigation view is not the way to go because even when creating a button to control the link it controls the entire screen. Any pointers?
This is a complex question so any sort of guidance or suggestions of strategies that could be worth looking into would be great!
This is more a general question: I'm working on my first SwiftUI project and using MVVM for the first time as well. After programming some views I realized that I need for almost each view two different view models. Often it's the view model for the current view and the view model of the previous/mother view. Is this "normal" or is this a hint that I've designed my project wrong and abusing MVVM?
For example:
I have a view where I list all flashcard decks. For this view I have a decksViewModelthat looks like this:
class DeckListViewModel: ObservableObject{
#Published var decks = [Deck]
#Published var showDeck = false // this value will be true if i tab on a deck and the deck will shown in a detailed view. This value is checked in the list
#Published var expandButton = false
#Published var showDownloadCenter = false
#Published var showCreateDeck = false
}
Now I have a deckDetailView for the detailed view of my deck. The deckDetailViewModel stores the selected item. But to remove this view I need to change the value of the showDeck? in decksViewModel`. So I need to pass this view model as well.
I wouldn't say that's a good way to use MVVM.
Instead of storing showDeck in DeckListViewModel, you could just have it as a local #State variable in whatever view you're using it in. Or if you're using a NavigationView, just use a NavigationLink, and there'll be no need for any state variable.
struct DecksView: View {
#ObservedObject var deckListVM = DeckListViewModel()
var body: some View {
NavigationView {
VStack {
ForEach(deckListVM.decks) { deck in
NavigationLink(destination: DeckDetailView(deckDetailVM: DeckDetailViewModel(deck: deck))) {
// some view
}
}
}
}
}
}
I'm not sure how you implemented your DeckDetailViewModel, just guessing there.
I don't know how to navigate between views with buttons.
The only thing I've found online is detail view, but I don't want a back button in the top left corner. I want two independent views connected via two buttons one on the first and one on the second.
In addition, if I were to delete the button on the second view, I should be stuck there, with the only option to going back to the first view being crashing the app.
In storyboard I would just create a button with the action TouchUpInSide() and point to the preferred view controller.
Also do you think getting into SwiftUI is worth it when you are used to storyboard?
One of the solutions is to have a #Statevariable in the main view. This view will display one of the child views depending on the value of the #Statevariable:
struct ContentView: View {
#State var showView1 = false
var body: some View {
VStack {
if showView1 {
SomeView(showView: $showView1)
.background(Color.red)
} else {
SomeView(showView: $showView1)
.background(Color.green)
}
}
}
}
And you pass this variable to its child views where you can modify it:
struct SomeView: View {
#Binding var showView: Bool
var body: some View {
Button(action: {
self.showView.toggle()
}) {
Text("Switch View")
}
}
}
If you want to have more than two views you can make #State var showView1 to be an enum instead of a Bool.