Discover how you can build a top-notch accessibility experience for watchOS when you support features like larger text sizes, VoiceOver, and AssistiveTouch. We'll take you through adding visual and motor accessibility support to a SwiftUI app built for watchOS, including best practices around API integration, experience, and more.
♪ Bass music playing ♪ ♪ Daniel Sykes-Turner: Hi, my name is Daniel Sykes-Turner and I'm an accessibility engineer. In this talk, my colleague Virata and I are going to go over some of the accessibility features on watchOS and how you as a developer can build your watch apps to support the people who use these features. In today's talk, you'll first be introduced to accessibility on watchOS. Then I'll dive into the accessibility APIs and show you what you can do to support different types of visual accessibility. And finally, Virata will walk you through how motor accessibility works on the watch and what you can do to support it. Let's get started with accessibility on watchOS. Accessibility is about people using their devices in the way that's best for them. And that means, to give your app the best user experience, accessibility must be considered. On the Apple Watch, we have a large suite of accessibility features that can make using your app easier. For example, assistive technologies such as VoiceOver allow people with visual impairments full use of their Apple Watch by navigating a screen using a series of gestures and taps while content is read back to them. And brand new on watchOS this year is AssistiveTouch, which has been reimagined specifically for the Apple Watch. AssistiveTouch allows those with motor impairments to use their Apple Watch without the need to touch the screen at all. Virata will show you how this works later on and what you can do to support it. watchOS also offers several display accommodations such as Reduce Motion, Bold Text, and also new this year, large accessibility text sizes. Let's talk about visual accessibility on watchOS. Supporting accessibility with the right APIs will ensure that VoiceOver functions correctly for your users, regardless of whether your app is written with WatchKit or SwiftUI. This talk will focus on accessibility for SwiftUI. However, know that all the principles you learn here will apply to WatchKit as well. And when making your apps accessible, don't forget about complications and notifications. Complications and dynamic notifications also need to support our assistive technologies, as they act as another path delivering content from your app. Now, isn't the best way to learn something new to build an app? Luckily I've become quite invested in growing plants at home so I decided to go ahead and build an app to help take care of them. Now, it's not quite finished, but on the main screen here I have all the information on my plants, including an upcoming schedule of plant care tasks: water in five days, fertilize in seven days, and keep in a medium amount of sunlight. I have a few other plants in the list here, too. Then for each plant, I have two buttons that I can use to log a task such as when I water or fertilize. And by tapping on the cell, I can adjust the number of days between watering and fertilizing. Since this is an informative app, each cell contains a fair amount of text. Here I'm showing you what the app looks like when the system text size is at its default size; it looks pretty good. But if I change the system text size to extra small, as shown on the left, you'll notice that while the buttons and the task list text resizes, the plant name title stays the same size. Then when I move up to accessibility extra large on the right, the task list text size has grown so large that it has truncated and no longer fits all the information on the screen. Let's take a look at how our app can better support Dynamic Type. If I inspect the code for my PlantView, I have a VStack with the title and other content inside. Notice the font I've used for my title is using a fixed font size. That's definitely going to stop my title from changing size. Rather than using a fixed font size, I should be using one of the 11 text styles provided. The text styles on the left are displayed at the default system text size. And when scaled up to the largest text size, they grow to the size of the text styles on the right. By using a text style, the system will automatically adjust the font size with the system text size settings. So if I take a look at my PlantView code again, I can make that change quickly and easily by changing my title font to the title3 text style for a smaller-sized title. Next let's fix the truncating text in the task list. In the code for my PlantTaskLabel, I'm assigning a lineLimit of 1 to everything in the HStack, which only allows the text to expand over a single line. To give your UI the flexibility to adjust to as many lines as needed, set the lineLimit to the maximum number of lines you need to support. Or remove it to allow an unlimited number of lines. Now we're making progress. But while the UI certainly is larger and there's no more truncation, in the process it's become a lot more crowded. And it's still not easy to read the information on the screen. Sometimes layouts for larger text styles just need to be structured differently. So when building the layout, I'll create an Environment property wrapper on the sizeCategory to get updates whenever this changes. Next, I just need to adjust my UI to depend on the sizeCategory. In this case, the text in the task list starts wrapping somewhere around extraExtraLarge. So if the size category is less than this, I'll use the PlantView we've seen before. But if it's larger, I'll use this new vertical plant view that stacks each of my labels and buttons, giving them much more space to grow. That looks much better. With the introduction of large accessibility text styles on the watch, we expect to see a lot more people making use of Dynamic Type -- and so should you! For people setting up their Apple Watch, they will now see the option to customize the text size when getting started. And if they don't make any changes, the watch will automatically pick the closest size to what is used on the phone. So to recap Dynamic Type, there are three key things to make sure watch apps behave great with larger text sizes. First, you want to make sure to always use a text style, not a fixed font size. Second, allow your text to wrap to avoid truncating. And third, when necessary, switch to a vertically stacked layout when the content becomes too crowded. Since this only scratches the surface on large text, I'd strongly recommend you check out the talk, "Building apps with Dynamic Type" to really step up your game. And to learn even more about making great visual experiences, be sure to check out the talk, "Make your app visually accessible." OK, so visually, that works great. But what if I weren't a visual user? Let's turn on VoiceOver and have a listen to what the experience is like.
VoiceOver: WWDaisy. Daisy, image. Drop, image. Five days. Leaf, image. Seven days. Brightness higher, image. Medium. Drop, fill, button. Leaf, fill, button. Daniel: OK, so there are definitely some improvements we can make. The first would be to reduce the number of elements that can be interacted with. At the moment, this is a rather complex view. Each cell has four labels, four images, and two buttons. And currently, to get to the second plant, I have to navigate past every item in the first plant cell. Also the image icons for water, fertilize, and sunlight were read out as separate elements to the text, and their labels didn't even make sense for our context. Lastly, the two buttons at the bottom were using the default labels provided by the symbols being used. So the first issue we talked about is that I have to navigate through too many items just to move from plant one to plant two. When I created my NavigationLink, I did this by specifying accessibilityElement grouping of the children as .contain, just to understand the accessibility state of each element. But since this is looking good, I'll remove this line, and NavigationLink will combine all the accessibility information from the children automatically. Now the cell is treated as a single element and the content read out is, "WWDaisy. Five days. Seven days. Medium. Button." That task list still needs some work so next, let's provide some context to the upcoming plant-care tasks by giving each a better label. The label is determined by the PlantTask. So inside my PlantTaskLabel struct, I'll just alter the accessibilityLabel to return a different string for each plant task. This is the same technique that I'll use to put labels on my buttons. Now the content is read out as, "WWDaisy. Watering in five days. Fertilizing in seven days. Keep in medium sunlight. Button." And the water and fertilize buttons will be read out as, "Log watering, button" and, "Log fertilizing, button." So far, a lot of work is done for us automatically, with only a few additional modifiers being needed. In fact, with SwiftUI, most of your accessibility comes for free. You'll probably just write a few lines of code. But every now and then, you might need to build a custom control. I did by building a custom counter so that I could adjust the watering and fertilizing frequency for my plants. Let's take a look. VoiceOver: Watering frequency in days, heading. Remove, button. Eight. Add, button. Add. Daniel: So while this technically works, it's not the ideal experience. The goal here is to turn these three items into a single accessible element. To do this, I'll begin by using our accessibilityElement modifier. This will create a new higher-level element; but this time, I will ignore all the children. This is actually the default behavior for accessibilityElement. So I can leave the parameters blank to get the same behavior. And because I'm ignoring the children, this discards the Add and Remove button labels along with the accessibility actions that were provided automatically. Instead, I'll use accessibilityAdjustableAction to allow the user to increment or decrement the value by swiping up or down on the counter. And now that I've only got one element, I'll give it a single label using the name of the task. This will end up as "Watering Frequency" or "Fertilizing Frequency." Finally, I'll give it a value. The accessibilityValue is read out each time it changes, while the label will only be read out when navigating to the element.
VoiceOver: Watering frequency in days, heading. Watering frequency. Eight days. Adjustable. Nine days. Daniel: Great. That works much better. As you just saw, SwiftUI made it easy to make our watch app accessible to VoiceOver. And what's great is that because this is SwiftUI, the same code works on macOS and iOS as well. To learn more about designing a fantastic experience with SwiftUI, be sure to check out the talk, "Accessibility in SwiftUI." And to learn about the brand-new tools and APIs when working with SwiftUI accessibility, check out the talk, "SwiftUI accessibility: Beyond the basics." Now, before we move on from visual accessibility, I'd like to highlight two more things: complications and notifications. Complications are a high-traffic window into your app, so, of course, they also need to provide information in an accessible way. There are many different types of complications but most of them will be made up of three different components: text, images, and symbols. Text will be picked up by VoiceOver automatically, but if your text contains abbreviations, make sure to add accessibility labels with the nonabbreviated versions. Here we expand the abbreviated, "Wednesday Mar 9" to its full form of, "Wednesday, March 9th." Image-based complications are also very common. Be sure to provide accessibility labels here too; otherwise, the image name will be used instead. "Moon" isn't nearly as descriptive as, "A real-time view of the moon. Third quarter." Certain iconography such as SF Symbols may come with a default accessibility label such as "Drop, fill", but just make sure that the label that comes with the symbol is the one that makes the most sense for you. "Water in three days" makes much more sense for me. As for notifications, these are another way your app may send a lot of information to your user. So while some notifications are fairly straightforward, others such as dynamic notifications can have complex views and so will need the same accessibility support that you've provided for your app. And now I'll turn you over to Virata to tell you all about motor accessibility. Virata Yindeeyoungyeon: Well, thank you, Daniel. My name is Virata Yindeeyoungyeon. I am an engineer on the Accessibility team. Today, I am very excited to talk about motor-accessibility support on Apple Watch. But before we dive into the presentation, let me give you a quick glance of our new feature. Without touching the screen and only using hand gesture, I am able to navigate from the watch face to the Control Center to the Do Not Disturb button, and then turn it on. It is that simple. This year, we are very excited to bring AssistiveTouch to Apple Watch. AssistiveTouch allows full use of your Apple Watch without touch, using only the hand that the Apple Watch is on. People can use hand gestures or hand motions to navigate a cursor around the screen. They can bring up a menu to access additional functionality based on the screen content. For some people, this may be the only way they can interact with their Apple Watch. For people with motor impairments such as missing limbs, loss of functions of hands or arms, AssistiveTouch will enable more options to allow control and performing actions on Apple Watch. Now let's see an example on how people can use AssistiveTouch. The primary way to use AssistiveTouch is through hand gestures. People are able to perform different gesture such as clench to tap, double-clench to bring up the action menu, pinch to navigate to the next element, double-pinch to navigate back to the previous element. For those who are not able to use hand gestures, an alternative is using hand motions. By tilting their wrist, people are able to move the onscreen pointer and interact with the UI elements. Similar to AssistiveTouch on iOS, with Dwell Control you can perform an action by resting the pointer over an element for a set amount of time.
Now let's get into more detail on how AssistiveTouch works. AssistiveTouch is made up of two main features: the cursor and the action menu. When you turn on AssistiveTouch, you see a cursor appear on the screen. The cursor will focus on each element on the screen one at a time, in order from top left to bottom right. The cursor highlights the element for further interactions. For more actions on the focused element, you can bring up the action menu to perform system or custom actions. AssistiveTouch action menu comes with default system actions that allow control of the device such as pressing on the Digital Crown, scrolling navigation, gesture interactions, and much more. You can also add custom actions to this menu as well. So now we know how AssistiveTouch works. Let's take a look at how we can support it in your application. We'll go over these following topics. First, we'll learn what focusable elements are in your view and how we can modify them. Then we'll take a look at the cursor frame and how we can change the frame size. Last, we'll talk more about how to customize the action menu. Let's begin with focusable elements. This table gives a general overview of which elements are focusable by AssistiveTouch. Only interactive elements that respond to user interaction are focusable. SwiftUI provides built-in control elements to handle user interactions; these elements are interactive and focusable. In your view, if you have a button, a toggle, or a NavigationLink, these elements can be focused by default. Actionable elements are focusable by AssistiveTouch because they have an action or are defined to be interactive. A text element can become interactive and focusable by attaching a tap gesture action. You can also add an accessibilityAction to an element as well. If you define an element to have an actionable trait, like a button or adjustable, then it will be treated as interactive and would be focused by AssistiveTouch. Some elements are not focusable. Static elements such as a label or a text that do not respond to user interaction will not be focusable by AssistiveTouch. Elements with user interaction disabled are also not focusable. So let's see an example. In this view, we have a label header, text with drink information, Accept and Cancel button elements. Only the two buttons are focusable, while the label and text are not. Let's take a look at the code for this view. A tap gesture is attached to the main VStack, indicating that you can tap on this view to show the drink detailed view. However, the static elements inside the VStack do not become focusable unless explicitly declared. Here, only the Accept and Cancel buttons are focusable. So how would a user know that they can tap on the view to show the drink detail view? To provide a better user experience, I would like to highlight the drink information text element to show that it is interactable and a tap can be performed. This can be done by setting "true" to accessibilityRespondsToUser Interaction modifier. After applying the modifier, we now have three focusable elements in this view: the drink information text, Accept and Cancel button elements. Now let's learn more about AssistiveTouch cursor frame. As you can see, the focused elements are important for AssistiveTouch to highlight if the element is intractable or an action can be performed. The AssistiveTouch cursor frame is the same as the element's tappable area; elements with small tappable area will have a small cursor frame and it might clip the content inside. We can make this cleaner by adding padding and having the borders match the shape of the object to prevent clipping. In this example, a NavigationLink with the ellipsis image is highlighted with a small circle from the AssistiveTouch cursor. We can improve on this by increasing the size of the tappable area, which also changes the AssistiveTouch cursor frame. By providing a path to the contentShape modifier, you can change the tappable area and the cursor frame of the element. For this NavigationLink, I set the tappable area shape to be a circle one-and-a-half times the size of the element. As a result, the AssistiveTouch cursor frame is larger and the element is much easier to see on the screen. Now let's take a look at AssistiveTouch action menu. AssistiveTouch action menu surfaces default system actions and custom actions in your view to show in the action menu lists. If the focus element has custom actions, those actions will be prioritized and shown at the beginning of the list so it's more convenient to interact. If the element is adjustable, the decrement and increment actions are shown. If an accessibility group element contains custom actions, then those actions will be surfaced when focusing on the group element. If you have already added custom actions for VoiceOver in the element, that's great! These actions will automatically show up in the AssistiveTouch action menu as well. The custom actions will be shown as an icon. The default image will be the first letter of the custom action name. If you want to provide an image for your custom action icon, you can add a label with an image to the accessibilityAction modifier. Well, that's the end of our session. Now you have a better understanding of accessibility features on Apple Watch. Remember to add Dynamic Type, VoiceOver, and AssistiveTouch support in your application. You have the tools to use these APIs and make your watchOS application accessible for everyone. Well, thank you for your time and enjoy the rest of WWDC! ♪