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