Home > Software engineering >  Bridging Optional Binding to Non-Optional Child (SwiftUI)
Bridging Optional Binding to Non-Optional Child (SwiftUI)

Time:10-23

I have a parent state that might exist:

class Model: ObservableObject {
    @Published var name: String? = nil
}

If that state exists, I want to show a child view. In this example, showing name.

If name is visible, I'd like it to be shown and editable. I'd like this to be two-way editable, that means if Model.name changes, I'd like it to push to the ChildUI, if the ChildUI edits this, I'd like it to reflect back to Model.name.

However, if Model.name becomes nil, I'd like ChildUI to hide.

When I do this, via unwrapping of the Model.name, then only the first value is captured by the Child who is now in control of that state. Subsequent changes will not push upstream because it is not a Binding.

Question

Can I have a non-optional upstream bind to an optional when it exists? (are these the right words?)

Complete Example

import SwiftUI

struct Child: View {
    // within Child, I'd like the value to be NonOptional
    @State var text: String
    
    var body: some View {
        TextField("OK: ", text: $text).multilineTextAlignment(.center)
    }
}

class Model: ObservableObject {
    // within the parent, value is Optional
    @Published var name: String? = nil
}

struct Parent: View {
    @ObservedObject var model: Model = .init()
    
    var body: some View {
        VStack(spacing: 12) {
            Text("Demo..")

            // whatever Child loads the first time will retain
            // even on change of model.name
            if let text = model.name {
                Child(text: text)
            }
            
            // proof that model.name changes are in fact updating other state
            Text("\(model.name ?? "<waiting>")")
        }
        .onAppear {
            model.name = "first change of optionality works"
            loop()
        }
    }
    
    @State var count = 0
    func loop() {
        async(after: 1) {
            count  = 1
            model.name = "updated: \(count)"
            loop()
        }
    }
}

func async(_ queue: DispatchQueue = .main,
           after: TimeInterval,
           run work: @escaping () -> Void) {
    queue.asyncAfter(deadline: .now()   after, execute: work)
}

struct OptionalEditingPreview: PreviewProvider {
    static var previews: some View {
        Parent()
    }
}

CodePudding user response:

Bind your var like this. Using custom binding and make your child view var @Binding.

struct Child: View {
    @Binding var text: String //<-== Here
   // Other Code 

if model.name != nil {
   Child(text: Binding($model.name)!)
}

CodePudding user response:

Child should take a Binding to the non-optional string, rather than using @State, because you want it to share state with its parent:

struct Child: View {
    // within Child, I'd like the value to be NonOptional
    @Binding var text: String

    var body: some View {
        TextField("OK: ", text: $text).multilineTextAlignment(.center)
    }
}

Binding has an initializer that converts a Binding<V?> to Binding<V>?, which you can use like this:

            if let binding = Binding<String>($model.name) {
                Child(text: binding)
            }

If you're getting crashes from that, it's a bug in SwiftUI, but you can work around it like this:

            if let text = model.name {
                Child(text: Binding(
                    get: { model.name ?? text },
                    set: { model.name = $0 }
                ))
            }
  • Related