4

Imagine a view with some @Binding variables:

init(isEditing: Binding<Bool>, text: Binding<Bool>)

How can we have the selection working with an internal @State if it is not provided in the initializer?

init(text: Binding<Bool>)

This is how to make TextField become first responder in SwiftUI

Note that I know we can pass a constant like:

init(isEditing: Binding<Bool> = .constant(false), text: Binding<Bool>)

But!

This will kill the dynamicity of the variable and it won't work as desire. Imagine re-inventing the isFirstResponder of the UITextField.

  • It can't be .constant(false). The keyboard will be gone on each view update.
  • It can't be .constant(true). The view will take the keyboard on each view update.

Maybe! Apple is doing it somehow with TabView.

pawello2222
  • 46,897
  • 22
  • 145
  • 209
Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278

2 Answers2

3

You may create separate @State and @Binding properties and sync them using onChange or onReceive:

struct TestView: View {
    @State private var selectionInternal: Bool
    @Binding private var selectionExternal: Bool

    init() {
        _selectionInternal = .init(initialValue: false)
        _selectionExternal = .constant(false)
    }

    init(selection: Binding<Bool>) {
        _selectionInternal = .init(initialValue: selection.wrappedValue)
        _selectionExternal = selection
    }

    var body: some View {
        if #available(iOS 14.0, *) {
            Toggle("Selection", isOn: $selectionInternal)
                .onChange(of: selectionInternal) {
                    selectionExternal = $0
                }
        } else {
            Toggle("Selection", isOn: $selectionInternal)
                .onReceive(Just(selectionInternal)) {
                    selectionExternal = $0
                }
        }
    }
}
struct ContentView: View {
    @State var selection = false

    var body: some View {
        VStack {
            Text("Selection: \(String(selection))")
            TestView(selection: $selection)
            TestView()
        }
    }
}
pawello2222
  • 46,897
  • 22
  • 145
  • 209
  • It is a good idea and +1 for that. But I need this for a `UIViewRepresentable`. So I can't observe for `onChange`. – Mojtaba Hosseini Oct 31 '20 at 09:38
  • Great solution to fallback on dummy binding if no binding is provided! Thanks! – XY L Aug 09 '23 at 13:33
  • @pawello2222 Because the binding value can also be changed from the outside, should there be another `onChange` observer to ensure that whenever the value gets changed externally, it will be synced with the internal state? – XY L Aug 09 '23 at 14:07
2

One solution is to pass an optional binding and use a local state variable if the binding is left nil. This code uses a toggle as an example (simpler to explain) and results in two interactive toggles: one being given a binding and the other using local state.

import SwiftUI

struct ContentView: View {
    
    @State private var isOn: Bool = true
    
    var body: some View {
        VStack {
            Text("Special toggle:")
            SpecialToggle(isOn: $isOn)
                .padding()
            SpecialToggle()
                .padding()
        }
    }
    
}

struct SpecialToggle: View {
    
    /// The binding being passed from the parent
    var isOn: Binding<Bool>?
    /// The fallback state if the binding is left `nil`.
    @State private var defaultIsOn: Bool = true
    
    /// A quick wrapper for accessing the current toggle state.
    var toggleIsOn: Bool {
        return isOn?.wrappedValue ?? defaultIsOn
    }
    
    init(isOn: Binding<Bool>? = nil) {
        if let isOn = isOn {
            self.isOn = isOn
        }
    }
    
    var body: some View {
        Toggle(isOn: isOn ?? $defaultIsOn) {
            Text("Dynamic label: \(toggleIsOn.description)")
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Optional binding toggle.

Lineous
  • 1,642
  • 8
  • 8
  • This will cause variables to not be sync always. But +1 for the working solution, Although it raises `Modifying state during view update, this will cause undefined behavior.` – Mojtaba Hosseini Oct 24 '20 at 07:26
  • Yeah, this solution might not be the best if you alternate between passing it a binding and using its local state. I briefly tried using `.onChange()` to keep them in sync but couldn't get it working. – Lineous Oct 24 '20 at 13:37