3

I have a view with a State variable which is an Optional. I render the view by first checking if the optional variable is nil, and, if it is not, force unwrapping it and passing it into a subview using a Binding.

However, if I toggle the optional variable between a value and nil, the app crashes and I get a EXC_BAD_INSTRUCTION in the function BindingOperations.ForceUnwrapping.get(base:). How can I get the expected functionality of the view simply displaying the 'Nil' Text view?

struct ContentView: View {
    @State var optional: Int?
    
    var body: some View {
        VStack {
            if optional == nil {
                Text("Nil")
            } else {
                TestView(optional: Binding($optional)!)
            }
            
            Button(action: {
                if optional == nil {
                    optional = 0
                } else {
                    optional = nil
                }
            }) {
                Text("Toggle")
            }
        }
    }
}

struct TestView: View {
    @Binding var optional: Int
    
    var body: some View {
        VStack {
            Text(optional.description)
            
            Button(action: {
                optional += 1
            }) {
                Text("Increment")
            }
        }
    }
}

2 Answers 2

4

I found a solution that doesn't involve manually created bindings and/or hard-coded defaults. Here's a usage example:

if let unwrappedBinding = $optional.withUnwrappedValue {
  TestView(optional: unwrappedBinding)
} else {
  Text("Nil")
}

If you want, you could also provide a default value instead:

TestView(optional: $optional.defaulting(to: someNonOptional)

Here are the extensions:

protocol OptionalType: ExpressibleByNilLiteral {
  associatedtype Wrapped
  var optional: Wrapped? { get set }
}
extension Optional: OptionalType {
  var optional: Wrapped? {
    get { return self }
    mutating set { self = newValue }
  }
}

extension Binding where Value: OptionalType {
  /// Returns a binding with unwrapped (non-nil) value using the provided `defaultValue` fallback.
  var withUnwrappedValue: Binding<Value.Wrapped>? {
    guard let unwrappedValue = wrappedValue.optional else {
      return nil
    }
    
    return .init(get: { unwrappedValue }, set: { wrappedValue.optional = $0 })
  }

  /// Returns a binding with non-optional `wrappedValue` (`Binding<T?>` -> `Binding<T>`).
  func defaulting(to defaultValue: Value.Wrapped) -> Binding<Value.Wrapped> {
    .init(get: { self.wrappedValue.optional ?? defaultValue }, set: { self.wrappedValue.optional = $0 })
  }
}
Sign up to request clarification or add additional context in comments.

5 Comments

You don't need the withUnwrappedValue extension function. There is a built in Binding initializer that does it.
@Jonathan. try running the code in the original post - it crashes when the optional toggle switches to nil, even if you use the Binding init you're referring to, instead of force unwrapping. If you figure out a way to make that work, let me know.
Yep the built in initializer only replaces the withUnwrappedValue function, you still need the defaulting(to:) function. The original code has a force unwrap, so of course it will crash when it's set to to nil, as then the initializer with return nil for the Binding which is then force unwrapped
@Jonathan. well what I was saying is that even if you refactor that code to use the Binding.init?(_:) and unwrap it with if let, it still crashes. I'm not sure why, because I would've assumed the built-in init is implemented roughly the same as what I wrote for withUnwrappedValue, but that doesn't seem to be the case, since it crashes even without force unwrapping.
It makes no sense how Binding.init? crashes like that. What is the point of that init!? And it seems like the only fix is to avoid it entirely and manually implement your own version which is written correctly. Bonkers.
1

Here is a possible approach to fix this. Tested with Xcode 12 / iOS 14.

demo

The-Variant! - Don't use optional state/binding & force-unwrap ever :)

Variant1: Use binding wrapper (no other changes)

CRTestView(optional: Binding(
        get: { self.optional ?? -1 }, set: {self.optional = $0}
    ))

Variant2: Transfer binding as-is

struct ContentView: View {
    @State var optional: Int?

    var body: some View {
        VStack {
            if optional == nil {
                Text("Nil")
            } else {
                CRTestView(optional: $optional)
            }

            Button(action: {
                if optional == nil {
                    optional = 0
                } else {
                    optional = nil
                }
            }) {
                Text("Toggle")
            }
        }
    }
}

struct CRTestView: View {
    @Binding var optional: Int?

    var body: some View {
        VStack {
            Text(optional?.description ?? "-1")

            Button(action: {
                optional? += 1
            }) {
                Text("Increment")
            }
        }
    }
}

2 Comments

This does work in this example, however, in a larger app this would mean having to always check the optional which isn't ideal. The point of having the nil check is to avoid having an optional in the subview.
Thanks, this is helpful, I'm not going to mark it as the answer though because it seems like providing a default value after you have done a nil check is superfluous.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.