How to map optional Binding to Bool


Greetings, traveler!

In SwiftUI, there is a common way of presenting modals. Check out this example:

struct ContentView: View {
    
    @State private var text: String?
    @State private var isPresenting = false
    
    var body: some View {
        Button("Press me!") {
            text = .greeting
            isPresenting = true
        }
        .alert(isPresented: $isPresenting) {
            Alert(title: Text(text ?? ""))
        }
    }
    
}

extension String? {

    static var greeting: Self {
        [
            "Good morning!",
            "Good afternoon!",
            "Good evening!",
            "Good night!"
        ].randomElement()
    }
    
}

While it seems a convenient way, we need to handle two variables. We must toggle the isPresented value and set the new text to the alert. Moreover, we must assign a nil value to the text after the alert presentation. Something like that:

.onChange(of: isPresenting) { _, newValue in
    guard !newValue else { return }
    text = nil
}

Looks a bit weird, isn’t it? To fix this issue, we can create an extension to the optional Binding, which will accept nil as false. We must take care of setting a new value only if it is false because we can’t use true to set a new value.

extension Binding where Value == Bool {
    
    init<Wrapped: Sendable>(mapped: Binding<Wrapped?>) {
        self.init(
            get: {
                mapped.wrappedValue != nil
            },
            set: { newValue in
                guard !newValue else { return }
                mapped.wrappedValue = nil
            }
        )
    }
    
}

extension Binding {
    
    func boolValue<Wrapped: Sendable>() -> Binding<Bool> where Value == Wrapped? {
        Binding<Bool>(mapped: self)
    }
    
}

Now, we can use it.

struct ContentView: View {
    
    @State private var text: String?

    var body: some View {
        Button("Press me!") {
            text = .greeting
        }
        .alert(isPresented: $text.boolValue()) {
            Alert(title: Text(text ?? ""))
        }
    }
    
}

Nice!

Conclusion

This simple workaround can make our lives a bit easier.