I have a binding with optional String
as a type and in the parent view I have if condition which checks whether it is has value or not. Depending on this condition I show or hide the child view. When I make name value nil
the app is crashing, below you find code example.
class Model: ObservableObject {
@Published var name: String? = "name"
func makeNameNil() {
name = nil
}
}
struct ParentView: View {
@StateObject var viewModel = Model()
var nameBinding: Binding<String?> {
Binding {
viewModel.name
} set: { value in
viewModel.name = value
}
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Name is \(viewModel.name ?? "nil")")
Button("Make name nil") {
viewModel.makeNameNil()
}
if let name = Binding(nameBinding) { /* looks like */
ChildView(selectedName: name) /* this causes the crash*/
}
}
.padding()
}
}
struct ChildView: View {
@Binding var selectedName: String
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Selected name: \(selectedName)")
HStack {
Text("Edit:")
TextField("TF", text: $selectedName)
}
}
}
}
Here is stack of the crash.
Thread 1: EXC_BREAKPOINT (code=1, subcode=0x107e1745c)
AG::Graph::UpdateStack::update() ()
AG::Graph::update_attribute(AG::data::ptr<AG::Node>, unsigned int) ()
AG::Subgraph::update(unsigned int) ()
Looks like a switfui bug for me, should I avoid using such constructions?
There is an undocumented method by Apple that allows you to see how, what, when SwiftUI View
s are loaded.
let _ = Self._printChanges()
If you add this to the body
of both View
s
struct BindingCheckView: View {
@StateObject var viewModel = Model()
var nameBinding: Binding<String?> {
Binding {
viewModel.name
} set: { value in
viewModel.name = value
}
}
var body: some View {
let _ = Self._printChanges()
VStack(alignment: .leading, spacing: 8) {
Text("Name is \(viewModel.name ?? "nil")")
Button("Make name nil") {
viewModel.makeNameNil()
}
if viewModel.name != nil{
ChildView(selectedName: $viewModel.name ?? "")
}
}
.padding()
}
}
struct ChildView: View {
@Binding var selectedName: String
var body: some View {
let _ = Self._printChanges()
VStack(alignment: .leading, spacing: 8) {
Text("Selected name: \(selectedName)")
HStack {
Text("Edit:")
TextField("TF", text: $selectedName)
}
}
}
}
You will see something like
You will notice that the child is being redrawn before the parent.
So for a split second you are trying to set a non-Optional
String
to an Optional<String>
I would submit this as a bug report because Apple has addressed similar issues before in order to stabilize Binding
but to address your immediate issue I would use an optional binding solution from here or a combination of both.
Or a little bit different set of solutions that combines the solutions from there
///This method returns nil if the `description` `isEmpty` instead of `rhs` or `default`
func ??<T: CustomStringConvertible>(lhs: Binding<Optional<T>>, rhs: T) -> Binding<T> {
Binding(
get: { lhs.wrappedValue ?? rhs },
set: {
lhs.wrappedValue = $0.description.isEmpty ? nil : $0
}
)
}
with the option above if name == ""
it will change to name == nil
///This is for everything that doesn't conform to `CustomStringConvertible` there is no way to set `nil` from here. Same from link above.
func ??<T>(lhs: Binding<Optional<T>>, rhs: T) -> Binding<T> {
Binding(
get: { lhs.wrappedValue ?? rhs },
set: { lhs.wrappedValue = $0 }
)
}
with the option above if name == ""
it will stay name == ""
and name == nil
will look like name == ""