Accessibility in iOS 27: Reading Apps & Custom Controls

Reading long-form content is a different problem from navigating UI: the goal is moving fluidly through text, not hopping between controls, and the two WWDC26 accessibility sessions split exactly along that seam: one for the reading surface, one for the controls that surround it.

The split matters because the fixes differ in kind. A reading app’s failures are about continuity: text that won’t connect across paragraphs, a read-all that stops at the bottom of a page. A custom control’s failures are about translation: a gesture that conveys everything visually and nothing to VoiceOver. iOS 27 ships APIs aimed at both, and one of them, accessibilityLinkedGroup, is new this year.

TL;DR

  • Reading apps should reach for system text views first. UITextView, TextEditor, and Text with selection enabled adopt the UITextInput protocol and get line/word/character navigation and selection for free1.
  • When layout forces separate text elements, link them so VoiceOver can move across the seam. iOS 18 introduced accessibilityNextTextNavigationElement/accessibilityPreviousTextNavigationElement; iOS 27 adds the SwiftUI accessibilityLinkedGroup modifier for the same effect1.
  • For paginated content, the causesPageTurn trait paired with accessibilityScroll makes Speak Screen and VoiceOver advance pages automatically during a read-all1.
  • Custom-rendered text (scanned pages, advanced typography) loses all of that. Adopting UITextInput in full restores it: geometry via selectionRects, substrings via textInRange, and a tokenizer for line/word/character navigation1.
  • Custom controls follow four guiding principles: purpose, value, actions, feedback. The tools are accessibilityLabel/accessibilityValue, the .adjustable trait with accessibilityAdjustableAction, custom actions for multi-axis controls, and direct touch (allowsDirectInteraction) for gesture-heavy surfaces2.

Reading Apps: Connecting Text That Layout Pulled Apart

The reading-app session is built around a deceptively simple constraint. The presenter’s travel-guide app uses a separate UITextView for each paragraph because the layout demanded it, rather than one view holding the whole page1. Each individual text view is accessible on its own. The problem appears at the boundary between them.

Watch on Apple Developer ↗
Apple shows VoiceOver stuck navigating line-by-line inside one paragraph, unable to cross into the next because each paragraph is a separate view, then connecting them with the text navigation element APIs.

The presenter sets three targets for the app: granular text navigation so VoiceOver and Speak Screen move through text fluidly, a continuous reading experience with no interruptions, and full text selection1. The rest of the session is a tour of which API satisfies each.

For navigation across separate views, the answer is the text navigation element APIs introduced in iOS 18. For each text element, you return the next and previous accessible text element VoiceOver should move to. In the session’s example, paragraph 1 returns paragraph 2 from its accessibilityNextTextNavigationElement, and paragraph 2 returns paragraph 1 from its accessibilityPreviousTextNavigationElement1. Once wired up, VoiceOver moves past the end of one paragraph and onto the first line of the next instead of playing the dead-end sound.

The iOS 27 addition lives in SwiftUI. As the presenter puts it, starting in iOS 27, linking multiple text elements together using the accessibilityLinkedGroup modifier achieves the same effect1. You give the linked elements the same id and namespace, and they inherit the cross-element text navigation behavior without the manual next/previous bookkeeping. AppKit gets the parallel accessibilitySharedTextUIElements for the same result on Mac1. The cluster’s What SwiftUI Is Made Of post covers how SwiftUI modifiers like this one resolve down to the underlying accessibility tree.

Continuity is the second target. Paginated content requires swiping, and a read-all should ignore the page boundaries the way an audiobook does. In the session, Speak Screen stops dead at the bottom of the first page until the presenter applies the causesPageTurn trait to the last paragraph on each page. Paired with accessibilityScroll, Speak Screen and VoiceOver then scroll to the next page automatically when they reach the end, and the trait is available in both UIKit and SwiftUI1.

The third target, selection, mostly comes free from system text views, but the session adds a thoughtful touch: a “Save Recommendation” action exposed through VoiceOver’s edit rotor. The presenter overrides accessibilityCustomActions on the paragraph text view and builds the custom action with the edit category, specifically so it appears in the edit rotor alongside text-selection operations rather than as a generic action1. The guidance is explicit: use the edit category when a custom action is associated with text selection.

When You Render Your Own Text: UITextInput in Full

The second half of the reading session addresses the case where system views are not an option. Custom text shows up in dedicated reading apps for advanced typography, shared cross-app code, or scanned pages, and the presenter’s example is the sharpest: replacing the travel-guide’s text views with scanned-in pages from a handwritten notebook. The cost is total. Swapping to images loses the accessibility behavior UITextView provided for free, down to the most basic thing, reading out the text. VoiceOver just says “Image”1.

The fix is to adopt the UITextInput protocol, which can sit on any accessibility element and make rendered text or text in images as accessible as a standard text view1. The catch, stated plainly in the session, is that you have to implement it in its entirety to get the full benefit. The presenter walks through the load-bearing pieces:

  • Geometry. selectionRects computes the highlight rectangles for a given range. Working from a handwriting image, the presenter uses the known height and width of each line to approximate rects via a custom selectionRectFromImage function, then returns the assembled array1.
  • Substrings. textInRange returns just the portion of text an assistive technology queries for1.
  • A tokenizer. Navigation by line, sentence, word, or character runs through a tokenizer. The session subclasses UIKit’s UITextInputStringTokenizer to match the custom layout1.

One refinement is explicitly optional. To make selection feel complete with handles and highlights, the presenter adds a UITextInteraction to the page view and calls the input delegate when the selection changes so the system updates the visuals. The session notes this step is not required by UITextInput itself; it rounds out the experience to match a standard text view1. And UITextInput composes with the earlier APIs, so causesPageTurn and the navigation elements work on custom text too.

There’s a payoff the session calls out that’s easy to under-appreciate: doing this work doesn’t only serve VoiceOver and Speak Screen. Since iOS 26, the Accessibility Reader can open an app’s content in a display tuned for easier reading, and the same accessible-text practices improve that experience as well1.

Custom Controls: Purpose, Value, Actions, Feedback

The custom-controls session opens with a standard SwiftUI slider and an argument about why it works. You read a track, a handle positioned halfway, a draggable affordance, and immediate feedback, all at a glance. Nobody explained any of it. Then the session asks the obvious question: what if someone can’t see the screen? VoiceOver answers by reading “Brightness, 50%, adjustable” plus a hint to swipe up or down, which conveys the same four things visually: the purpose, the value, the available action, and the feedback as the value changes2.

Watch on Apple Developer ↗
Apple turns a custom coffee-dispenser control from a bare “button, 6 ounces” into an adjustable slider VoiceOver can drive, by adding label, value, and the .adjustable trait with an adjustable action.

Those four words (purpose, value, actions, feedback) are the session’s guiding principles, and every example maps back to them2. The first is a coffee-dispenser control: drag up for more coffee, down for less, the fill level representing ounces. Before any work, VoiceOver reads it as a generic “Button, 6 ounces” with no sense of how to change the value2. The fixes are incremental:

  1. Purpose and value. accessibilityLabel names it “Coffee Dispenser”; accessibilityValue announces the current fill2.
  2. Action. The .adjustable trait tells VoiceOver the control responds to up/down swipes, and accessibilityAdjustableAction provides a closure with a direction parameter of .increment or .decrement to handle each case2.

That gets one-ounce-at-a-time adjustment. For finer control, the session reaches for VoiceOver’s built-in passthrough gesture: a double-tap-and-hold that starts at the control’s accessibilityActivationPoint and sends touch events directly to the control as the finger moves. The presenter sets the activation point to match the current fill level2. Feedback during passthrough is a small lesson in restraint: the session posts an announcement only when the value actually changed and at least 0.3 seconds have passed, because announcing every change would be noisy2.

The equalizer pad raises the bar. It’s a two-dimensional control, and the session is candid that .adjustable is the wrong tool because its increment/decrement actions cover a single axis. The answer is custom actions: the accessibilityAction modifier applied four times for “move up,” “move right,” “move down,” and “move left,” each nudging one axis by a fixed step clamped to range2. Unlike the adjustable action, custom actions support any operation you define, and they reach Switch Control and Voice Control users too2.

Direct Touch: When Gestures Are the Whole Point

The session’s final example is a virtual-cat control where you pet, tap, and pinch for different reactions. Passthrough is a poor fit here, the presenter notes, because people may want to repeat an action over and over or use multiple gestures2. So the control reaches for direct touch.

Watch on Apple Developer ↗
Apple adds the .accessibilityDirectTouch modifier with .requiresActivation to a gesture-driven control so touches pass straight through to the cat instead of being intercepted by VoiceOver.

The allowsDirectInteraction trait marks a region as a direct-touch area: touch events pass straight to the control instead of being processed by VoiceOver, so every gesture the control supports works2. Two options shape the behavior. .requiresActivation keeps the control inert until a double-tap, which lets a person drag across the screen without triggering it by accident, and direct touch then stays active until focus leaves the element. .silentOnTouch keeps VoiceOver quiet over the region, intended for controls that produce their own audio that VoiceOver speech would otherwise talk over2. The virtual cat uses .accessibilityDirectTouch with .requiresActivation2.

The session closes with a caveat that ties back to the platform-accessibility argument in Accessibility As Platform: not everyone can perform direct-touch gestures, so wherever possible expose another path, such as custom actions, so Switch Control and Voice Control users reach the same interactions2.

Adoption Guidance

Both sessions end on the same instruction: turn VoiceOver on and audit your own app. Concretely:

  • For a reading surface on system text views, try the read-all gesture, navigate with the lines rotor, and select text. If a read-all stops at a page boundary, adopt causesPageTurn with accessibilityScroll. If line navigation dead-ends between separate text elements, link them with the navigation element APIs (UIKit) or accessibilityLinkedGroup (SwiftUI, iOS 27)1.
  • If you render your own text, plan for a full UITextInput adoption, not a partial one; the protocol is all-or-nothing, and the optional UITextInteraction step is what makes selection feel native1.
  • For any custom control, walk the four principles in order. Can a VoiceOver user tell what it is (label), what state it’s in (value), what they can do (adjustable trait or custom actions), and what happened (announcements)? Reserve direct touch for controls whose value is the gesture itself, and pair it with a non-gestural fallback2.

The recurring theme: reach for system components first, and treat the custom path as the exception that demands real, complete work. The Three Surfaces of an iOS App post frames accessibility as a first-class surface alongside the visible UI and App Intents; these sessions are what getting that surface right looks like for text and controls.

FAQ

What is the new accessibility API in iOS 27 for reading apps?

The SwiftUI accessibilityLinkedGroup modifier. Starting in iOS 27, linking multiple text elements with the same id and namespace gives them cross-element text navigation, so VoiceOver moves from the last line of one element to the first line of the next. It is the SwiftUI equivalent of the iOS 18 accessibilityNextTextNavigationElement/accessibilityPreviousTextNavigationElement APIs and of AppKit’s accessibilitySharedTextUIElements1.

Do I need to implement UITextInput if I use a standard text view?

No. UITextView (UIKit), TextEditor, and Text with selection enabled (SwiftUI), and NSTextView (AppKit) already adopt UITextInput and provide line/word/character navigation plus selection out of the box. You only adopt UITextInput yourself when you render custom text, such as scanned pages or advanced typography, where those system behaviors are lost1.

When should a custom control use the adjustable trait versus custom actions?

Use the .adjustable trait with accessibilityAdjustableAction for single-axis values where increment and decrement make sense, like a slider. Use custom actions (the accessibilityAction modifier) when one axis isn’t enough, like a two-dimensional pad, or when you want to expose discrete operations VoiceOver reads by name. The session’s equalizer pad uses four custom actions (move up/right/down/left) precisely because the adjustable trait covers only one direction2.

What is direct touch and when should I use it?

Direct touch (the allowsDirectInteraction trait, applied via .accessibilityDirectTouch in SwiftUI) marks a region so touches pass straight to your control instead of being processed by VoiceOver, letting people use every gesture the control supports. Use it for gesture-heavy controls where the passthrough gesture is a poor fit, and pair it with .requiresActivation to prevent accidental triggers. Always offer a non-gestural fallback, such as custom actions, for people who can’t perform direct-touch gestures2.

How does accessible text help features beyond VoiceOver?

The same work pays off in Speak Screen and, since iOS 26, the Accessibility Reader, which opens an app’s content in a display tuned for easier reading. Implementing the text navigation, page-turn, and UITextInput practices the reading session covers improves all three experiences from one set of changes1.

References


  1. Apple, WWDC26 session 219, “Enhance the accessibility of your reading app.” developer.apple.com/videos/play/wwdc2026/219. Source for system text views (UITextView, TextEditor, Text with selection, NSTextView) adopting UITextInput; the iOS 18 accessibilityNextTextNavigationElement/accessibilityPreviousTextNavigationElement APIs and the iOS 27 SwiftUI accessibilityLinkedGroup modifier (AppKit: accessibilitySharedTextUIElements); causesPageTurn with accessibilityScroll; the text-selection action via accessibilityCustomActions with the edit category; full UITextInput adoption for custom text (selectionRects, textInRange, UITextInputStringTokenizer) plus optional UITextInteraction; and the iOS 26 Accessibility Reader. 

  2. Apple, WWDC26 session 220, “Refine accessibility for custom controls.” developer.apple.com/videos/play/wwdc2026/220. Source for the purpose/value/actions/feedback principles; the coffee-dispenser control using accessibilityLabel, accessibilityValue, the .adjustable trait, and accessibilityAdjustableAction; the passthrough gesture at accessibilityActivationPoint with throttled announcements (value-changed plus 0.3 seconds elapsed); the accessibilityAction modifier for the equalizer pad; and direct touch via allowsDirectInteraction (.accessibilityDirectTouch) with .requiresActivation and .silentOnTouch for the virtual-cat control, plus the non-gestural-fallback reminder. 

相關文章

SwiftUI Performance and Interop in iOS 27

How iOS 27 SwiftUI handles lazy-stack scrolling, GPU shader effects, and AppKit/UIKit interop, drawn from three official…

17 分鐘閱讀

What's New in SwiftUI for iOS 27

iOS 27 reworks SwiftUI lists, documents, toolbars, and errors: drag-to-reorder, a readable/writable document model, tool…

23 分鐘閱讀

The Design Engineer's Agent Stack

Design engineers need agent infrastructure that enforces visual consistency, typography discipline, color compliance, an…

14 分鐘閱讀