When accessing view’s @State
or @Binding
from onSubmit
closure, it makes view unable to deinit. Basic example: we have a button and it switches condition to show target view with onSubmit
modifier or not. So, tapping button many times gives us following diagnostics:
[Input] constructed: (1)
[Input] constructed: (2)
[Input] deallocating: (1)
[Input] constructed: (2)
[Input] deallocating: (1)
[Input] constructed: (2)
[Input] deallocating: (1)
We can see: one instance is always alive!
Let’s comment line with onSubmit
, or replace it with onTapGesture
and everything will be fine:
[Input] constructed: (1)
[Input] deallocating: (0)
[Input] constructed: (1)
[Input] deallocating: (0)
[Input] constructed: (1)
[Input] deallocating: (0)
[Input] constructed: (1)
[Input] deallocating: (0)
code example:
import SwiftUI
struct ContentView : View {
@State private var state: Bool = false
var body: some View {
VStack {
Button("push") { state.toggle() }
if state { Input() } else { Color.red }
}
}
}
struct Input : View {
@State private var text: String
@State private var whatever: Bool
private let tracing: InstanceTracer
init() {
self.text = "text"
self.whatever = true
self.tracing = InstanceTracer("Input")
}
var body: some View {
TextField("placeholder", text: $text)
.onSubmit { whatever.toggle() } // <- causes instance unable to deallocate
}
}
final class InstanceTracer {
private static var instances: [String : Int] = [ : ]
private let name: String
init(_ name: String) {
self.name = name
print("[(name)] constructed: ((increment(numberOf: name)))")
}
deinit { print("[(name)] deallocating: ((decrement(numberOf: name)))") }
private func increment(numberOf name: String) -> Int {
var number: Int = if let number: Int = Self.instances[name] { number } else { 0 }
number += 1
Self.instances[name] = number
return number
}
private func decrement(numberOf name: String) -> Int {
var number: Int = if let number: Int = Self.instances[name] { number } else { 0 }
if number < 1 { return 0 }
number -= 1
Self.instances[name] = number
return number
}
}
3
The way to ensure correct instance constructing and deallocating is to use onCommit
parameter of TextField
instead of onSubmit
modifier:
var body: some View {
TextField("placeholder", text: $text, onCommit: { whatever.toggle() })
// instead of: TextField("placeholder", text: $text).onSubmit { whatever.toggle() }
}
SwiftUI Views are stack values that have no lifetime, i.e. like ints, they are not heap object instances. They are gone almost as soon as they are init.
Once you know that then you’ll realise you shouldn’t init an instance of a classes like InstanceTracer
inside a View
struct because it’s a memory leak. View
structs cannot own objects since they have no lifetime, you have to use .task
or @StateObject
if you want a longer lifetime tied to UI visible on screen.
In your case, your InstanceTracer
doesn’t need a longer lifetime because it’s just data and doesn’t do anything asyncronous, so just change it to a @State
struct, e.g.
@State private var tracing = InstanceTracer()
struct InstanceTracer {
mutating func increment(numberOf name: String) -> Int {
...
2