Skip to content

Mapping Optional Binding to Bool


When displaying an alert in SwiftUI, if the value used to calculate whether the alert is presented is both Optional and does not conform to Identifiable1 it is often recommended to use a separate flag, similar to:

struct ContentView: View {
    @State private var alertText: String?
    @State private var isPresentingAlert = false

    var body: some View {
        Button("Show Alert") {
            self.alertText = "Alert Text"
            self.isPresentingAlert = true
        }
        .alert(isPresented: $isPresentingAlert) {
            Alert(title: Text(alertText!))
        }
    }
}

There are 2 main downsides to this:

  1. alertText is not set back to nil, which may cause bugs and will increase memory usage (even if only a little in this case)
  2. The isPresentingAlert flag needs to be managed

To work around these issues I create a small extension to Binding the allows this same code to be updated to:

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

    var body: some View {
        Button("Show Alert") {
            self.alertText = "Alert Text"
        }
        .alert(isPresented: $alertText.mappedToBool()) {
            Alert(title: Text(alertText!))
        }
    }
}

The extension is fairly small and simple:

import os.log
import SwiftUI

extension Binding where Value == Bool {
    /// Creates a binding by mapping an optional value to a `Bool` that is
    /// `true` when the value is non-`nil` and `false` when the value is `nil`.
    ///
    /// When the value of the produced binding is set to `false` the value
    /// of `bindingToOptional`'s `wrappedValue` is set to `nil`.
    ///
    /// Setting the value of the produce binding to `true` does nothing and
    /// will log an error.
    ///
    /// - parameter bindingToOptional: A `Binding` to an optional value, used to calculate the `wrappedValue`.
    public init<Wrapped>(mappedTo bindingToOptional: Binding<Wrapped?>) {
        self.init(
            get: { bindingToOptional.wrappedValue != nil },
            set: { newValue in
                if !newValue {
                    bindingToOptional.wrappedValue = nil
                } else {
                    os_log(
                        .error,
                        "Optional binding mapped to optional has been set to `true`, which will have no effect. Current value: %@",
                        String(describing: bindingToOptional.wrappedValue)
                    )
                }
            }
        )
    }
}

extension Binding {
    /// Returns a binding by mapping this binding's value to a `Bool` that is
    /// `true` when the value is non-`nil` and `false` when the value is `nil`.
    ///
    /// When the value of the produced binding is set to `false` this binding's value
    /// is set to `nil`.
    public func mappedToBool<Wrapped>() -> Binding<Bool> where Value == Wrapped? {
        return Binding<Bool>(mappedTo: self)
    }
}

The extension isn't tied directly to showing an alert or a sheet and can be used in any context, but this is one of the better examples of its usage.

This extension is available on GitHub under the MIT license.

1 If it does conform to Identifiable use alert(item:content:)

Nevis version 1.1.0


Release Notes

  • Add support for opening PKGs, including PKGs inside ZIPs and DMGs
  • Improve detection of downloaded files
  • Ensure app is brought forward when opening from status bar icon
  • Fix handling of DMGs if Nevis is force quit and reopened

Nevis version 1.0.1


Release Notes

  • Improve handling of watch folder
  • Improve notification text when an error occurs

Supporting Multiple Swift Package Versions Without Breaking Compatibility


The Xcode 12 beta includes Swift 5.3 but drops support for iOS 8.x. This means that Swift packages that support iOS 8 will cause a warning:

The iOS deployment target 'IPHONEOS_DEPLOYMENT_TARGET' is set to 8.0, but the range of supported deployment target versions is 9.0 to 14.0.99.

It's not possible to remove this warning within a project that depends on a Swift package with a deployment target of iOS 8, but it is possible to fix this in the dependency without removing support for iOS 8 for older versions of Swift. There are multiple way this can be accomplished.

Keep Reading