Skip to content

Commit 7ffe995

Browse files
fix: restore accessibilityViewIsModal after nested modal dismiss
Three issues caused the remaining modal's ContainerView to be left with accessibilityViewIsModal = false after a nested modal dismisses: 1. The flag assignment used allPresentations (includes exiting presentations) instead of presentations(includeExiting: false). 2. The flag assignment was gated behind an early-return guard that bails when the top layer hasn't changed — which is exactly the case when dismissing back to the same top modal. 3. remove(presentation:) never re-evaluated the flag after removing the exiting presentation from allPresentations. Fix: move the flag assignment above the guard so it runs unconditionally, use the filtered presentation list, and call updateAccessibilityViewIsModal() from remove(presentation:). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d0306d5 commit 7ffe995

1 file changed

Lines changed: 17 additions & 16 deletions

File tree

Modals/Sources/ModalPresentationViewController.swift

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -470,12 +470,24 @@ public final class ModalPresentationViewController: UIViewController {
470470
)
471471
}
472472

473+
/// Always update `accessibilityViewIsModal` — even when the top layer hasn't changed,
474+
/// an exiting presentation may have stolen the flag from the remaining one.
475+
476+
let views = presentations(includeExiting: false).map(\.containerView)
477+
478+
views.last?.accessibilityViewIsModal = true
479+
480+
for view in views.dropLast() {
481+
view.accessibilityViewIsModal = false
482+
}
483+
473484
/// Note: We can use ! here, because our base `content` is always included in the array; it can never be empty.
474485

475486
let oldTopLayer = oldLayers.last!
476487
let topLayer = accessibilityLayers.last!
477488

478489
/// If the layering didn't change, then nothing changed, we can bail early.
490+
/// (Focus restoration and a11y notifications only matter when the top layer changes.)
479491

480492
guard oldTopLayer != topLayer else {
481493
return
@@ -486,22 +498,6 @@ public final class ModalPresentationViewController: UIViewController {
486498
if accessibilityLayers.contains(oldTopLayer) {
487499
focusRestorationStorage.focusRestoration(for: oldTopLayer.viewController).recordFocusedAccessibilityElement()
488500
}
489-
490-
/// Note: We don't restore a11y here, it's done once the
491-
/// presentation or dismissal animation is completed after a short delay.
492-
///
493-
/// See `postAccessibilityScreenChangedNotification`.
494-
495-
/// Now we can update our `accessibilityViewIsModal` status.
496-
/// This ensures that VoiceOver users only see the top-most modal layer.
497-
498-
let views = allPresentations.map(\.containerView)
499-
500-
views.last?.accessibilityViewIsModal = true
501-
502-
for view in views.dropLast() {
503-
view.accessibilityViewIsModal = false
504-
}
505501
}
506502

507503
private func postAccessibilityScreenChangedNotification() {
@@ -762,6 +758,11 @@ public final class ModalPresentationViewController: UIViewController {
762758
presentation.decorationViews.forEach { $0.removeFromSuperview() }
763759

764760
allPresentations.removeAll { $0.viewController === presentation.viewController }
761+
762+
// Re-evaluate accessibilityViewIsModal after removal.
763+
// The removed presentation may have held the flag, leaving the
764+
// remaining top presentation with accessibilityViewIsModal = false.
765+
updateAccessibilityViewIsModal()
765766
}
766767

767768
private func setUpTransitionIn(presentation: Presentation) {

0 commit comments

Comments
 (0)