[SC]()

iOS. Apple. Indies. Plus Things.

Modifier Monday: .accessibilityActions()

// Written by Jordan Morgan // Jul 25th, 2022 // Read it in about 3 minutes // RE: SwiftUI

Welcome to Modifier Monday! Today’s modifier is .accessibilityActions(_:), and its docs are here:

/// Adds multiple accessibility actions to the view.
@available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *)

public func accessibilityActions<Content>(@ViewBuilder _ content: () -> Content) -> some View where Content : View

Let’s see it in action.

Examples

The ability to add accessibility custom actions has been around in SwiftUI, so that’s nothing new. Plus, most of the time, SwiftUI controls are accessible right out of the box. Thankfully, for everyone involved - that means you get a lot for free.

However, there are times where you’ve either made a customized control that you need to express with more intention to the accessibility engine (i.e. it should do Y when it’s tapped), or users would benefit from flattening your view hierarchy to make VoiceOver navigation easier. These are both times where accessibility custom actions can help.

For example, if we had code like this:

HStack {
    VStack(alignment: .leading) {
        Text("Favorite")
        Text("Last changed on 7/25/22")
            .font(.caption2)
    }
    Spacer()
    Toggle("", isOn: .constant(true))
}

What the VoiceOver user may really want here is to toggle whether or not they’ve favorited the game, especially if they’ve used your app for quite a long time and are familiar with it. As such, the edit time stamp may get redundant after a bit, so you could use accessibility custom content to vend it optionally - or you could flatten the hierarchy. Those are two of a few options, but if you opted for the latter it might look like this:

HStack {
    VStack(alignment: .leading) {
        Text("Favorite")
        Text("Last changed on 7/25/22")
            .font(.caption2)
    }
    Spacer()
    Toggle("", isOn: .constant(true))
}
.accessibilityElement(children: .ignore)
.accessibilityLabel("Favorite - last changed on 7/25/22")
.accessibilityValue("Favorited")
.accessibilityAction(named: "Toggle Favorite") {
    // Toggle
}

let concatenatedThoughts = """

Be sure to never take away things from VoiceOver, that's not the lesson here. Always make the data available, just be aware that there are several ways and APIs you can use to do that. In this case, removing the time stamp label outright would be the wrong move.

"""

Cool - easier than diving through the two Text controls, and then finally arriving at the Toggle to change things.

Now, expand this idea out. Maybe to something bigger like this:

An iOS mock up built with SwiftUI showing a video game viewer app. Mega Man X's cover it shown along with metadata.

Now, we’ve got a lot the user can do - and a lot of code to write:

struct AXActionsView: View {
    var body: some View {
        VStack {
            Text("Mega Man X")
                .font(.title.weight(.semibold))
                .frame(minWidth:0, maxWidth: .infinity, alignment: .leading)
            Image("MegaMan")
                .resizable()
                .scaledToFit()
                .clipShape(RoundedRectangle(cornerRadius: 6.0, style: .continuous))
                .shadow(radius: 10)
            GroupBox("Play History") {
                HStack {
                    VStack(alignment: .leading) {
                        Text("Favorite")
                        Text("Last changed on 7/25/22")
                            .font(.caption2)
                    }
                    Spacer()
                    Toggle("", isOn: .constant(true))
                }
                .accessibilityElement(children: .ignore)
				.accessibilityLabel("Favorite state - last changed on 7/25/22")
				.accessibilityValue("Favorited")
				.accessibilityAction(named: "Toggle Favorite") {
    				// Toggle
				}
                Divider()
                HStack {
                    VStack(alignment: .leading) {
                        Text("Completed")
                        Text("Finished on 7/25/92")
                            .font(.caption2)
                    }
                    Spacer()
                    Toggle("", isOn: .constant(true))
                }
                .accessibilityElement(children: .ignore)
				.accessibilityLabel("Completion state - last changed on 7/25/92")
				.accessibilityValue("Completed")
				.accessibilityAction(named: "Toggle Completed") {
    				// Toggle
				}
                Divider()
                Button("Start Emulator") {
                    
                }
                .buttonStyle(.bordered)
                .padding(.top, 8)
            }
            .padding(.vertical, 32)
            ScrollView {
                Text("Truncated for clarity")
            }
        }
        .padding(.horizontal, 32)
        .background(Color(uiColor: .systemBackground))
        .tint(Color.blue)
    }
}

In this case, or ones like it where you’ve got several custom accessibility actions and a complex view hierarchy, you can now aggregate those in one go. Here’s what it looks like using the new .accessibilityActions() modifier:

GroupBox("Play History") {
	// GroupBox code - truncated for clarity
}
.accessibilityActions {
    Button("Toggle Favorite") {
        // Toggle
    }
    Button("Toggle Completed") {
        
    }
    Button("Start Emulation") {
        
    }
}

Now, when the user arrives at the GroupBox, they can use accessibility custom actions (i.e. “Actions Available” from VoiceOver) to swipe through those all in one go. What this new modifier gives us is the ability to attach multiple accessibility actions. That’s the real value-add here, implementation wise.

As with anything accessibility related, test it with folks who really rely on this stuff. I find that there are times where I think I know how some ax-related flow should work, but I actually introduced more friction. So, apply that thinking here - just because we can add accessibility custom actions to a parent View for several of its children, it certainly doesn’t mean it’s the defacto choice each time.

Until next time ✌️

···

Spot an issue, anything to add?

Reach Out.