Reducing memory leaks in SwiftUI when using StateObject and NavigationView

Josué Quiño
4 min readJun 5, 2024

--

iOS 15+

Photo by João Paulo Carnevalli de Oliveira on Unsplash

When using SwiftUI, there are cases where SwiftUI is retaining memory references to the view and StateObjects due to how these objects are handled in memory (separate of the view).

The intention of this arcticle is to have a few suggestions that could be considered workarounds for this memory references than are not needed when destroying the view.

Consider this one more step in the mastery of iOS memory management 🕵🏼.

Photo by Neil Soni on Unsplash

This is how the references could look after closing the view:

As you can see, these references are not coming from my source code.

The following list of suggestions could achieve something more like this:

All those remaining references are not significant given that are caused by @Published properties.

The intention of these are to be suggestions that are not intrusive with your current pattern/architecture and at the same time to help this framework in the meantime this is fixed in future releases:

Navigation View Style

A NavigationView could retain a reference for some views given a default behavior under the navigationViewStyle modifier. The main reason for this is that a SwiftUI view can support more than one view i.e. an iPad.

The solution consists on setting the modifier manually when using a NavigationView as follows:

NavigationView {
/// My awesome code for the view
MyView()
}
.navigationViewStyle(.stack)

Presentation mode

The presentationMode environment object could retain a reference when used in some cases like inside a NavigationView. Especially when using modifiers like toolbar.

We could either set a capture list for the environment object…

@Environment(\.presentationMode) var presentation

var body: some View {
NavigationView { [presentation] in
content
.toolbar {
Button {
presentation.wrappedValue.dismiss()
} label: { Text("This is a cool button 🤟") }
}
}
}

or in my personal opinion, the best strategy, using dismiss environment object (presentationMode has been marked as deprecated by Apple):

@Environment(\.dismiss) var dismiss

var body: some View {
NavigationView { [dismiss] in
content /// My perfect content view
.toolbar {
Button {
dismiss()
} label: { Text("This is a cool button 😎") }
}
}
}

Closures when using MVVM architecture pattern

When using the MVVM pattern, we commonly try to avoid circular references (at least we should).

We accomplish this by avoiding a reference to the View from the ViewModel. On architectures like VIPER, commonly we do have weak references to the view on UIKit, since the arrive of SwiftUI paradigm, this has changed.

However, there are some cases where we need a method related to the view (like dismissing a view or scrolling) that only exists in the View. The most common approach is using a closure, if this is the case, SwiftUI framework for iOS 15+ retains a reference of the ViewModel only because of the closure. Even if you are not using any self-reference inside them.

We can use optional closures on the ViewModel and set them into the onAppear modifier (Also it does not hurt to use the capture list for the ViewModel instance)…

@Environment(\.dismiss) var dismiss
@StateObject var viewModel: MyViewModel

...
\\\ My astonishing code goes here 🤠🚀
...

.onAppear { [weak viewModel] in
viewModel?.dismiss = {
dismiss()
}
}

and then release those references to the closures on the onDisappear modifier:

.onDisappear {
viewModel.dismiss = nil
}

StateObject/ObservableObject passed reference on closure

When passing these kinds of objects, the references are not always destroyed.

Use the capture list to “weakify” the used instances:

.onAppear { [weak viewModel] in
viewModel?.dismiss = {
dismiss()
}
}

Conclusion

At the end of the day, every iOS version would probably reduce the number of leaks when using these objects in the way they are intented to be used. Even though we “can’t” do anything that it’s outside of our code, I think it’s worth to have workaround like the ones we saw to reduce the load as possible.

Sometimes, the little details are the ones that matter.

Probably you could wonder… Why we are seeing iOS15+ issues that maybe are not happening anymore. Great question actually! Remember that in the industry and even Apple, recommends to support 3 versions of iOS (some old apps support even more).

Photo by Joshua Reddekopp on Unsplash

Thanks for reading and happy coding. 🫡

--

--

Josué Quiño

iOS Engineer, bartending enthusiast, starting as techno DJ