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
}
}
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() }
}