0

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")
            }
        }
    }
}
BenJacob
  • 957
  • 10
  • 31

2 Answers2

2

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

Pavel Orel
  • 144
  • 10
  • You don't need the withUnwrappedValue extension function. There is a built in Binding initializer that does it. – Jonathan. Aug 31 '23 at 09:32
  • @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. – Pavel Orel Sep 01 '23 at 16:12
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")
            }
        }
    }
}
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • 1
    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. – BenJacob Sep 18 '20 at 15:54
  • 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. – BenJacob Sep 18 '20 at 16:26