How do you make a custom modal screen work with VoiceOver: modal views, the escape gesture and the magic tap?
Predict first
What do the modal flag, escape and magic tap overrides report?
import UIKit
final class Sheet: UIView {
var closed = false, toggled = false
override func accessibilityPerformEscape() -> Bool { closed = true; return true }
override func accessibilityPerformMagicTap() -> Bool { toggled.toggle(); return true }
}
@MainActor func run() {
let sheet = Sheet()
print("modal by default:", sheet.accessibilityViewIsModal)
sheet.accessibilityViewIsModal = true
print("modal now:", sheet.accessibilityViewIsModal)
print("escape handled:", sheet.accessibilityPerformEscape(), "closed:", sheet.closed)
print("magic tap handled:", sheet.accessibilityPerformMagicTap(), "toggled:", sheet.toggled)
print("a plain view handles neither:", UIView().accessibilityPerformEscape(), UIView().accessibilityPerformMagicTap())
}
MainActor.assumeIsolated { run() }
- modal by default: false / modal now: true / escape handled: true closed: true / magic tap handled: true toggled: true / a plain view handles neither: false false
- modal by default: true …
- A plain view dismisses itself
Mental model. A modal is a lift door closing. Visual users see the doors shut behind them; VoiceOver users only know it if you tell the lift that the corridor is no longer part of the journey, and leave a way to press “open”.
A visual modal (a custom dialog, a popup menu, a bottom sheet you built yourself) dims what is behind it, but VoiceOver does not see dimming: it will happily swipe from the dialog into the controls behind it. Two things fix that. Set accessibilityViewIsModal = true on the container view of the modal content (it defaults to false, and the check reads both values) so VoiceOver ignores its siblings, and post a .screenChanged or .layoutChanged notification with the first element to read so focus moves into the dialog (Q467). In SwiftUI the same is expressed with the .isModal accessibility trait; system presentations (present(_:), sheet, alerts) already behave this way, so only hand-built overlays need the work. When the modal closes, return focus to the control that opened it.
VoiceOver also reserves two gestures that your views can answer. The escape gesture (a two-finger scrub, a “Z” shape) calls accessibilityPerformEscape() on the focused element and then up the view hierarchy, and your override should dismiss the modal or go back and return true when it handled it (the check shows the override firing and the plain view returning false). Standard navigation controllers and presented controllers already respond; a custom modal must implement it, or a VoiceOver user may have no way out when the close button is small or not obvious. The magic tap (a two-finger double tap) calls accessibilityPerformMagicTap() for the most important action of the current context, the way it answers a call or starts and pauses media; implement it on the screen where one action clearly dominates, and do not use it for a random shortcut. These methods are invoked by the system, so the check calls them directly; the gestures themselves and the VoiceOver focus movement need a device and were not run.
The modal flag is opt-in; the two gesture hooks do nothing unless a view overrides them.
Common traps
- Building an overlay without the modal flag. Focus escapes into the dimmed screen behind it.
- Using the magic tap for something unrelated to the screen’s main purpose. People rely on it to answer calls and control media, so keep its meaning consistent.
They might follow up with
- Does a SwiftUI
.sheetneed this? No, the system presentation is already modal for VoiceOver. - Where should focus go when the modal closes? Back to the element that triggered it, via a layout-changed notification.
Related questions
- 24.1.2 How do custom actions, adjustable controls, rotors and announcements make complex UI usable with VoiceOver?
- 24.1.3 How do Voice Control, Switch Control and keyboard access use your UI, and what do accessibility input labels…
- 24.6.2 How do you make a custom-drawn control (a canvas, a chart or a hand-built segmented control) accessible with…
- 7.3.3 How does modal presentation work, and how do the presentation styles and sheets differ?
← How do you make a custom-drawn control (a canvas, a chart or a… How do you control how VoiceOver pronounces text: spell-out… →