Swiftui - Possible Memory Leak

SwiftUI memory leak

If you didn't have to use selection in your List, you could use any unique & constant id, for example:

class TestObj: Hashable, Identifiable {
let id = UUID()

/* ... */
}

And then your List with the implicit id: \.id:

List(model.objs) { obj in
Text(obj.text)
}

This works great. It works because now you are no longer identifying the rows in the list by a reference type, which is kept by SwiftUI. Instead you are using a value type, so there aren't any strong references causing TestObjs to not deallocate.

But you need selection in List, so see more below about how to achieve that.


To get this working with selection, I will be using OrderedDictionary from Swift Collections. This is so the list rows can still be identified with id like above, but we can quickly access them. It's partially a dictionary, and partially an array, so it's O(1) time to access an element by a key.

Firstly, here is an extension to create this dictionary from the array, so we can identify it by its id:

extension OrderedDictionary {
/// Create an ordered dictionary from the given sequence, with the key of each pair specified by the key-path.
/// - Parameters:
/// - values: Every element to create the dictionary with.
/// - keyPath: Key-path for key.
init<Values: Sequence>(_ values: Values, key keyPath: KeyPath<Value, Key>) where Values.Element == Value {
self.init()
for value in values {
self[value[keyPath: keyPath]] = value
}
}
}

Change your Model object to this:

class Model: ObservableObject {
@Published var objs: OrderedDictionary<UUID, TestObj>

init() {
let values = (1..<100).map { TestObj(text: "\($0)")}
objs = OrderedDictionary<UUID, TestObj>(values, key: \.id)
}
}

And rather than model.objs you'll use model.objs.values, but that's it!

See full demo code below to test the selection:

struct ContentView: View {
@StateObject private var model = Model()
@State private var selection: Set<UUID> = []

var body: some View {
NavigationView {
VStack {
List(model.objs.values, selection: $selection) { obj in
Text(obj.text)
}

Button(action: {
var i = 1
model.objs.removeAll(where: { _ in
i += 1
return i % 2 == 0
})
}) {
Text("Remove half")
}
}
.onChange(of: selection) { newSelection in
let texts = newSelection.compactMap { selection in
model.objs[selection]?.text
}

print(texts)
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
EditButton()
}
}
}
}
}

Result:


SwiftUI - Possible Memory Leak

Your code appears to be perfectly acceptable SwiftUI, and there does appear to be a memory leak somewhere, as switching back and forth (even with a manual Toggle() instead of the asyncAfter() call) leads to increasing memory.

I believe this is a bug with List, because if you change the List to another type of view, the issue disappears, and I haven't noticed it when using this same pattern with all other kinds of views.

I'd recommend you file feedback with Apple, and post the feedback number here so others affected can file their own and reference it.

SwiftUI - memory leak in NavigationView

You don't need to split the close button out in its own view. You can solve this memory leak by adding a capture list to the NavigationView's closure: this will break the reference cycle that retains your viewModel.

You can copy/paste this sample code in a playground to see that it solves the issue (Xcode 11.4.1, iOS playground).

import SwiftUI
import PlaygroundSupport

struct ModalView: View {
@Environment(\.presentationMode) private var presentation
@ObservedObject var viewModel: ViewModel

var body: some View {
// Capturing only the `presentation` property to avoid retaining `self`, since `self` would also retain `viewModel`.
// Without this capture list (`->` means `retains`):
// self -> body -> NavigationView -> Button -> action -> self
// this is a retain cycle, and since `self` also retains `viewModel`, it's never deallocated.
NavigationView { [presentation] in
Text("Modal is presented")
.navigationBarItems(leading: Button(
action: {
// Using `presentation` without `self`
presentation.wrappedValue.dismiss()
},
label: { Text("close") }))
}
}
}

class ViewModel: ObservableObject { // << tested view model
init() {
print(">> inited")
}

deinit {
print("[x] destroyed")
}
}

struct TestNavigationMemoryLeak: View {
@State private var showModal = false
var body: some View {
Button("Show") { self.showModal.toggle() }
.sheet(isPresented: $showModal) { ModalView(viewModel: ViewModel()) }
}
}

PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.setLiveView(TestNavigationMemoryLeak())

SwiftUI memory leak when referencing property from closure inside Form/NavigationView and swapping views

I found a solution: Make a weak viewModel in your action. It seems that Apple changed the behavior of closures. This means that the NavigationView is storing a strong reference to viewModel. After a couple days of debugging, it finally worked for me.

Button(action: { 
[weak viewModel] in viewModel?.dismissButtonPressed.send(())
}) {
Image("crossmark")
.padding()
.foregroundColor(Color.white)
}
}

In your problem, this will be solved like this:

NavigationView { 
[weak viewModel] in Button(action: { viewModel?.logOut() }) {
Text("X").frame(width: 40, height: 40)
}
}

Tested on the latest Xcode 11.5, with iOS 13.5. Now, after dismissing the view, the viewModel is correctly deallocated.

How do you fix a SwiftUI memory leak caused by compound modifiers + animation?

This appears to be related to embedding mutation inside a ForEach, which looking at the memory graphs may involve List (which also seems to have a memory leak when its elements are mutated). I recommend opening a Feedback on this.

It can be eliminated by removing the ForEach:

func square(at offset: Int) -> some View {
let mySquare = mySquares[offset]
return MySquareView()
.offset(x: mySquare.offsetX,
y: mySquare.offsetY)
}

var body: some View {
VStack {
ZStack {
square(at: 0)
square(at: 1)
square(at: 2)
square(at: 3)
}
.scaleEffect(0.8)
}
}

You can also hack your way around this by injecting a recursive AnyView rather than using ForEach. There may be other clever solutions like this; it may be worth exploring further, since losing both ForEach and List is quite obnoxious.

func loopOver<C: Collection, V: View>(_ list: C, content: (C.Element) -> V) -> AnyView
{
guard let element = list.first else { return AnyView(EmptyView()) }
return AnyView(Group {
content(element)
loopOver(list.dropFirst(), content: content)
})
}

var body: some View {
VStack {
ZStack {
loopOver(mySquares) { MySquareView().offset(x: $0.offsetX, y: $0.offsetY )}
}
.scaleEffect(0.8)
}
}

(Using AnyView like this will get in the way of various SwiftUI optimizations, so it's a last resort, but in this case it is likely necessary.)



Related Topics



Leave a reply



Submit