Search code examples
swiftui

SwiftUI onSubmit makes instance unable to deallocate


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

Solution

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