SwiftUI: reference types leaks memory when used in State property wrapper.

In next program SimpleClass do not call deinit causing memory leak.

final class SimpleClass {
    deinit {
        print("deinit SimpleClass")
    }
}

struct ContentView: View {
    var body: some View {
        Text("Empty")
            .onAppear {
                simple = .init()
            }
    }

    @State private var simple = SimpleClass()
}

Tested on iPhone 12 mini, iOS 16.2, Version 14.2 (14C18).

Am I using @State wrapper wrong?

Answered by DelawareMathGuy in 744894022

hi,

how SwiftUI handles the initialization

 @State private var simple = SimpleClass()

is not exactly what you expect; this SimpleClass instance is created for SwiftUI's internal use (i.e., you don't own it) and eventually space in the heap will be created for you that is a reference to it ... you are free to change that reference if you wish to something else, but SwiftUI still has ownership of what it created.

when you replace what you think is the value of simple with a new instance of SimpleClass in .onAppear, you are updating the heap reference that you own to a second instance of a SimpleClass object; but SwiftUI still holds on to the SimpleClass object that it owns.

your concern about "leaking memory" will eventually be taken care of by SwiftUI when ContentView goes away (remember, SwiftUI frequently discards and recreates View structs and thus does a lot of its own memory management).

in your case, you only have one view at the main level and it is likely the view is never discarded; when the app quits, whatever memory SwiftUI owns will be cleaned up.

so, consider the following code experiment:

(1) it would help if you knew exactly when objects of type SimpleClass come and go, so i will add an id as well as an initializer.

final class SimpleClass {
	let id = UUID()
	init() {
		print("initialize SimpleClass, id = \(id.uuidString.prefix(8))")
	}
	deinit {
		print("deinit SimpleClass, id = \(id.uuidString.prefix(8))")
	}
}

(2) suppose your ContentView is not the main view of the app. try this, where the main view is a list of one item, with a navigation link to open your view, which is now renamed to be LeakyView:

struct ContentView: View {
	var body: some View {
		NavigationStack {
			List {
				NavigationLink(value: "LeakyView") {
					Text("Show LeakyView")
				}
			}
			.navigationDestination(for: String.self) {_ in
				LeakyView()
			}
		}
	}
}

struct LeakyView: View {
	@State private var simple = SimpleClass()
	init() {
		print("called init of LeakyView")
	}
	var body: some View {
			Text("Empty")
				.onAppear {
					print("on appear occurs")
					simple = .init()
				}
	}
}

(3) run the app. you'll see that when you navigate to and then back from LeakyView to the main ContentView, all SimpleClass instances will have been released. in my case, the output i received is

(a) tap on navigation link. navigation occurs (yes, SwiftUI created a view struct, discarded it, then created a new one)

initialize SimpleClass, id = 106EB234
called init of LeakyView
deinit SimpleClass, id = 106EB234
initialize SimpleClass, id = 9646766A
called init of LeakyView
on appear occurs
initialize SimpleClass, id = C9026337

(b) tap on Back button. even though it looks like the object with id 9646766A has been orphaned, SwiftUI will give it back when the view goes away.

deinit SimpleClass, id = C9026337
deinit SimpleClass, id = 9646766A

all is in order (!)

hope that helps,

DMG

No need for the 2nd init of simple in on Appear, so yes, it will leak memory as each instance is eventually taken care of by ARC.

Accepted Answer

hi,

how SwiftUI handles the initialization

 @State private var simple = SimpleClass()

is not exactly what you expect; this SimpleClass instance is created for SwiftUI's internal use (i.e., you don't own it) and eventually space in the heap will be created for you that is a reference to it ... you are free to change that reference if you wish to something else, but SwiftUI still has ownership of what it created.

when you replace what you think is the value of simple with a new instance of SimpleClass in .onAppear, you are updating the heap reference that you own to a second instance of a SimpleClass object; but SwiftUI still holds on to the SimpleClass object that it owns.

your concern about "leaking memory" will eventually be taken care of by SwiftUI when ContentView goes away (remember, SwiftUI frequently discards and recreates View structs and thus does a lot of its own memory management).

in your case, you only have one view at the main level and it is likely the view is never discarded; when the app quits, whatever memory SwiftUI owns will be cleaned up.

so, consider the following code experiment:

(1) it would help if you knew exactly when objects of type SimpleClass come and go, so i will add an id as well as an initializer.

final class SimpleClass {
	let id = UUID()
	init() {
		print("initialize SimpleClass, id = \(id.uuidString.prefix(8))")
	}
	deinit {
		print("deinit SimpleClass, id = \(id.uuidString.prefix(8))")
	}
}

(2) suppose your ContentView is not the main view of the app. try this, where the main view is a list of one item, with a navigation link to open your view, which is now renamed to be LeakyView:

struct ContentView: View {
	var body: some View {
		NavigationStack {
			List {
				NavigationLink(value: "LeakyView") {
					Text("Show LeakyView")
				}
			}
			.navigationDestination(for: String.self) {_ in
				LeakyView()
			}
		}
	}
}

struct LeakyView: View {
	@State private var simple = SimpleClass()
	init() {
		print("called init of LeakyView")
	}
	var body: some View {
			Text("Empty")
				.onAppear {
					print("on appear occurs")
					simple = .init()
				}
	}
}

(3) run the app. you'll see that when you navigate to and then back from LeakyView to the main ContentView, all SimpleClass instances will have been released. in my case, the output i received is

(a) tap on navigation link. navigation occurs (yes, SwiftUI created a view struct, discarded it, then created a new one)

initialize SimpleClass, id = 106EB234
called init of LeakyView
deinit SimpleClass, id = 106EB234
initialize SimpleClass, id = 9646766A
called init of LeakyView
on appear occurs
initialize SimpleClass, id = C9026337

(b) tap on Back button. even though it looks like the object with id 9646766A has been orphaned, SwiftUI will give it back when the view goes away.

deinit SimpleClass, id = C9026337
deinit SimpleClass, id = 9646766A

all is in order (!)

hope that helps,

DMG

SwiftUI: reference types leaks memory when used in State property wrapper.
 
 
Q