Accessible SF Symbol Image in SwiftUI

I’ve been using SwiftUI for a while and recently worked on a watchOS app where I also tried to make it accessible.

When I first started on the app, I forgot to add accessibility labels for the system images, and I found it later while auditing it in the Accessibility Inspector.

SwiftUI doesn’t provide any initializer for Image to have the label as a parameter nor something to ignore it for accessibility purposes. So let’s create one!

SF Symbols with Accessibility Label

I wasn’t sure what to name my custom view because AccessibilityImage seemed too long, while A11YImage didn’t sound ideal. So I decided to term it AYImage instead.

The variables required for this view would be the name, accessibility label, and a boolean value if it would be hidden or not.

private var isHidden = false
private var systemName: String
private var label: Text = Text("")

For creating a labeled system symbol image, I require an initializer that takes the name of the symbol as well as the accessibility label associated with it.

/// Creates a labeled system symbol image that you can use as
/// content for controls, with the specified label for accessibility.
///
/// - Parameters:
///   - systemName: The name of the system symbol image.
///   - label: The label associated with the image used for accessibility.
public init(systemName: String, label: Text) {
    self.systemName = systemName
    self.label = label
}

I only need to have the name of the symbol as the parameter for the initializer. This helps to hide the image for better accessibility in a few cases.

/// Creates an unlabelled, decorative system symbol image which
/// is ignored for accessibility purposes.
///
/// - Parameters:
///   - systemName: The name of the system symbol image.
public init(systemName name: String) {
    self.systemName = name
    self.isHidden = true
}

The accessibility APIs got better, specific method names this year. To make sure that I’m supporting the older version as well, I’ll have to add a check for that and write the modifiers accordingly.

var body: some View {
    if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) {
        Image(systemName: systemName)
            .accessibilityHidden(isHidden)
            .accessibilityLabel(label)
    } else {
        Image(systemName: systemName)
            .accessibility(hidden: isHidden)
            .accessibility(label: label)
    }
}

AYImage

As SF symbols are only available in Big Sur, I’ve to add a check for it as well.

Putting everything together, we’re ready to use our custom view!

@available(macOS 11.0, *)
struct AYImage: View {
    private var isHidden = false
    private var systemName: String
    private var label: Text = Text("")

    /// Creates a labeled system symbol image that you can use as
    /// content for controls, with the specified label for accessibility.
    ///
    /// - Parameters:
    ///   - systemName: The name of the system symbol image.
    ///   - label: The label associated with the image used for accessibility.
    public init(systemName: String, label: Text) {
        self.systemName = systemName
        self.label = label
    }

    /// Creates an unlabelled, decorative system symbol image which
    /// is ignored for accessibility purposes.
    ///
    /// - Parameters:
    ///   - systemName: The name of the system symbol image.
    public init(systemName name: String) {
        self.systemName = name
        self.isHidden = true
    }

    var body: some View {
        if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) {
            Image(systemName: systemName)
                .accessibilityHidden(isHidden)
                .accessibilityLabel(label)
        } else {
            Image(systemName: systemName)
                .accessibility(hidden: isHidden)
                .accessibility(label: label)
        }
    }
}

You can use it like this -

struct ContentView: View {
    var body: some View {
        AYImage(systemName: "gear", label: Text("Settings"))
        AYImage(systemName: "square.and.arrow.up.on.square")
    }
}

There are many instances where you may be using the gear symbol for the settings icon in the navigation bar and don’t want your user to hear gear instead of a button.

Or worse, you don’t want the user to hear something absurd as “square.grid.3x1.folder.fill.badge.plus” for the “Add a folder” button.

I hope you enjoyed this code snippet and will create a better experience for your users!