Accessibility in iOS 14 Widgets With SwiftUI

7 minute read

This summer, Apple announced widgets for iOS 14 that you can have on your home screen alongside your existing apps. Widgets help you to personalise the screen with glanceable relevant data upfront.

This article covers the foundational adaptation of creating a good VoiceOver, Voice Control, and adaptive display experience with your widgets using SwiftUI.

Accessibility in SwiftUI

It’s refreshing to see how easy it is to make your application accessible using SwiftUI. Adding an accessibility label to a settings icon is as simple as:

.accessibilityLabel(Settings)

Developers can take advantage of the straightforward APIs and implement these in their apps.

And also widgets.

So let’s look at some of the topics of accessibility and how you can implement them in your widgets!

VoiceOver and Widgets

VoiceOver is a screen reader built into iOS that allows blind and low-vision users to use a touch screen by audibly describing elements of the screen as they pan or swipe around it.

I’ll focus on some general accessibility APIs that you can adapt to your apps for a great experience for VoiceOver users.

  • accessibilityLabel(_:)** **You’ll usually use this method for providing labels to images and logos. You may have the logo of your app on the widget, which you may want the user to hear, and not logo_image or something along that line.
Image("logo_image").accessibilityLabel(model.appName)
  • accessibilityValue(_:)** **If your widget has dynamic value wtih a static label, you should use this method. For example, in a book-reading tracker widget, the number of books read, which will be dynamic, can be defined using this method.
Text("Number of books read")
    .accessibilityValue("\(model.booksRead)")
  • accessibilityHint(_:) The hint is used for communicating to the user what happens after they perform an action on the accessibility elements. This helps provide information that cannot be accommodated in the main accessibilityLabel** **itself. For example, on a stack of widgets, the hint provided by the system is — “Swipe up or down to select a custom action, then double-tap to activate.” When you swipe up or down on an element in a widget that is in the stack, the custom actions will be “Next Widget” and “Previous Widget.” Although, a widget is either static data or deep links that would tell the user that it is a button. I haven’t come across a widget created by Apple where custom hints are provided, as it is something not needed in this case.
  • accessibilityAction(_:_:)** **An action on a widget is either nothing for glanceable data, or it directly opens the app through the deep link on the click of an element. So a custom action may not be needed.
  • accessibilityElement(children:)** **This instance method is particularly useful when you may want to combine a lot of text to be read at once. For example, the Podcasts app combines the name of the podcast, author, data, and the description into one element.
VStack {
    Text("\(model.date)")
    Text(model.author)
    Text(model.title)
    Text(model.description)
}
.accessibilityElement(children: .combine)
  • accessibilityHidden(_:)** **There may be cases when you want to hide the image completely from VoiceOver to read the element. In this case, you should use this method. For example, in the Weather widget, the icon for describing the weather is not read out by the reader. Do note that Image also has an initialiser, where the readers skip reading the image element.
Image(decorative: model.imageName)
  • accessibilityAddTraits(_:)** **For best describing the accessibility element and its characteristics, this method is used. For example, you may have title text in your app, and for distinguishing it from other texts, you can use the isHeader type property for it.
VStack {
    Text("Gradient Game")
        .accessibilityAddTraits(.isHeader)

    ForEach(model.histories, id: \.self, content: HistoryView.init)
}

Apart from that, to configure the widget using intents, the user may use a long double-press to edit the widget and open the configuration menu. In my general observation, the list in the menu has access to accessibility benefits natively.

For example, when providing a title in INObjectSection, it automatically configures it with the header trait.

INObjectSection(title: "Categories", items: categories)

With this topic coming to an end, I hope you create a wonderful experience for VoiceOver users!

Voice Control and Widgets

Voice Control is an accessibility feature that lets you control your iPhone, iPad, and Mac using just your voice. It enables users to enter text, launch apps, and do anything they would otherwise do with their fingers. For users with severe motor disabilities, in particular, it is a life-transforming tool.

If your widget is VoiceOver accessible, it is usually Voice Control accessible as well. The part that can be improved is adding alternate names to labels, which can get long and repetitive in a systemMedium or systemLarge widget.

The input labels method accessibilityInputLabels(_:) gives an alternate label while using Voice Control and Full Keyboard Access.

I’ve spent around two hours trying on the latest versions of Xcode to get this working, but unfortunately, it doesn’t work with widgets yet, though it works perfectly fine on the app. This is a bug, and I’ve filed feedback regarding the same.

When this works in the future, the code may seem similar to this where VoiceOver reads all the elements as one, whereas Voice Control only shows the title, to reduce clutter on the screen while making it easier for the user to say the title for Voice Control to process that command.

VStack {
    Text("\(model.date)")
    Text(model.author)
    Text(model.title)
    Text(model.description)
}
.accessibilityElement(children: .combine)
.accessibilityInputLabels([model.title])

Display/Text Sizes and Widgets

Dynamic Type is a feature that helps the user to change system text size according to their preference. For many, it may be just a choice, but for some, it is a necessity.

Another great thing about SwiftUI is that this feature is supported out of the box. If you’re using the normal Font and not hardcoding the sizes, SwiftUI will automatically scale the text sizes according to the system preference. And this applies to widgets as well.

.font(.caption2)

The best way to support this feature is to test it. If you find your design getting out of the widget even in non-accessibility category sizes, you may need to rethink your designs in an accessibility-oriented way because widgets are supported on the iPod Touch (4” display) and SE(4.7” display) as well, and it should look good on all the devices.

struct SimpleEntryView: View {
    var entry: Provider.Entry
    @Environment(\.sizeCategory) var category

    var body: some View {
        if category.isAccessibilityCategory {
            // Handle design for accessibility text sizes
        } else {
            // Handle design for normal text sizes
        }
    }
}

But I understand. Widgets are small, and you may find it difficult to support the accessibility category sizes. Or even xxxLarge/xxLarge sizes. In that case, you can try to handle these cases with a different design altogether.

struct SimpleEntryView: View {
    var entry: Provider.Entry
    @Environment(\.sizeCategory) var category

    var body: some View {
        if category >= .extraExtraLarge {
            // Handle design for large text sizes
        } else {
            // Handle design for normal text sizes
        }
    }
}

Apart from scalable fonts, if you go to Settings > Display & Text Size, you’ll find several preferences for the users to set like Reduce Transparency, Increase Contrast, Differentiate Without Color, etc.

To respect the user’s preferences, SwiftUI gives us powerful environment values that you can read from and add conditions in your widgets accordingly.

I’ll briefly list one and how to use it.

  • legibilityWeight This reflects the value of the Bold Text display setting. By default, the view adapts to it automatically. But if you have specified a font weight, you can add the condition to respect the value of legibilityWeight.
struct SimpleEntryView : View {
    var entry: Provider.Entry
    @Environment(\.legibilityWeight) var legibilityWeight

    var body: some View {
        Text("Bold Text")
            .fontWeight(legibilityWeight == .bold ? .bold : .light)
            .font(.body)
    }
}

The whole list can be found in the EnvironmentValues documentation, which lists the major ones that you can adapt in your widgets. Most of them can be configured in that way by providing a conditional view or modifier, like in the case of sizeCategory and legibilityWeight.

  • accessibilityDifferentiateWithoutColor Use a different design with shapes and symbols to convey meaning instead of just relying on color when this condition is true.
  • accessibilityReduceTransparency With this condition true, the background should not be transparent or blurred but opaque instead. A great example is the Batteries widget, which has a translucent background but becomes opaque when this boolean is set to true.
  • colorSchemeContrast You can adapt your UI to handle the ColorSchemeContrast increased case by increasing the contrast ratio by providing alternate high contrast images and colors.

There’s a great WWDC 2020 session on making your app visually accessible that I recommend you watch. One particular line that stood out for me by Drew Haas was the following:

“Early stages of development are the best time to start thinking about visual accessibility accommodations, but it’s really never too late to dig in.”

Conclusion

Widgets are great for relevant information that you want to present to your user without them opening your app.

And making them accessible for everyone is the cherry on the cake. You may have already started working on your widgets, or already shipped some great ones by now. But if you haven’t, it’s a great time to think and design from an accessibility perspective!

I hope you gained some value from this article and will implement it in your widgets.