From 6bcb88c944c29406fd428f5fe11cafecc9ae90db Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Sun, 5 Jul 2026 13:06:43 +0200 Subject: [PATCH] Added accessibility support to in-app notifications on the ContentViewModel view model in the Sample app. --- Apps/Attendi/Catalogs/Localizable.xcstrings | 12 ++++++ Apps/Attendi/Models/AppNotification.swift | 24 ++++++++++++ .../View Models/ContentViewModel.swift | 10 +++-- Apps/Attendi/Views/ContentView.swift | 37 ++++--------------- 4 files changed, 50 insertions(+), 33 deletions(-) diff --git a/Apps/Attendi/Catalogs/Localizable.xcstrings b/Apps/Attendi/Catalogs/Localizable.xcstrings index 4fe5c23..b168172 100644 --- a/Apps/Attendi/Catalogs/Localizable.xcstrings +++ b/Apps/Attendi/Catalogs/Localizable.xcstrings @@ -47,6 +47,18 @@ } } }, + "view.recording.picker.locale.hint" : { + "comment" : "The accessibility hint of the toolbar menu that picks the locale to transcribe.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Picks the language of the spoken audio to transcribe." + } + } + } + }, "view.recording.picker.locale.title" : { "localizations" : { "en" : { diff --git a/Apps/Attendi/Models/AppNotification.swift b/Apps/Attendi/Models/AppNotification.swift index 4c767bb..60186b0 100644 --- a/Apps/Attendi/Models/AppNotification.swift +++ b/Apps/Attendi/Models/AppNotification.swift @@ -47,3 +47,27 @@ extension AppNotification { case started } } + +// MARK: - Properties + +extension AppNotification { + + /// The system symbol of the icon matching the notification's event. + var imageSymbol: String { + switch event { + case .cancelled: "xmark.circle" + case .failed: "exclamationmark.triangle" + case .started: "arrow.down.circle" + } + } + + /// The localized message of the notification, naming the locale of the speech model, from the app's string catalog. + var textMessage: LocalizedStringResource { + switch event { + case .cancelled: .viewRecordingNotificationDownloadCancelled(localeName) + case .failed: .viewRecordingNotificationDownloadFailed(localeName) + case .started: .viewRecordingNotificationDownloadStarted(localeName) + } + } + +} diff --git a/Apps/Attendi/View Models/ContentViewModel.swift b/Apps/Attendi/View Models/ContentViewModel.swift index a282b25..d12a966 100644 --- a/Apps/Attendi/View Models/ContentViewModel.swift +++ b/Apps/Attendi/View Models/ContentViewModel.swift @@ -1,5 +1,5 @@ +import Accessibility import Foundation -import Notifying import Observation import OSLog import Recording @@ -153,8 +153,8 @@ private extension ContentView.Model { await SpeechTranscriber.supportedLocale(equivalentTo: locale) } - /// Posts a notification for a download event of the given locale, scheduling its automatic dismissal a few seconds later — - /// the only way a notification is dismissed. + /// Posts a notification for a download event of the given locale, announcing it to assistive technologies and scheduling its + /// automatic dismissal a few seconds later — the only way a notification is dismissed. /// /// - Parameters: /// - event: The download event to notify. @@ -170,6 +170,10 @@ private extension ContentView.Model { notifications.append(notification) + AccessibilityNotification.Announcement( + String(localized: notification.textMessage) + ).post() + Task { try? await Task.sleep(for: Constant.Delay.dismissal) diff --git a/Apps/Attendi/Views/ContentView.swift b/Apps/Attendi/Views/ContentView.swift index 80a248d..54599cd 100644 --- a/Apps/Attendi/Views/ContentView.swift +++ b/Apps/Attendi/Views/ContentView.swift @@ -25,7 +25,8 @@ struct ContentView: View { /// transcribed text of every processed recording — or a content unavailable message when the transcription is empty — closable /// through its toolbar button or a swipe. The model's in-app notifications overlay the feature, stacking downward just below the /// navigation bar; each one slides in from the leading edge when posted and out again when its scheduled dismissal arrives, without - /// displacing the recording controls underneath. + /// displacing the recording controls underneath. Every posted notification is also announced to assistive technologies, since the + /// banners themselves are transient. var body: some View { NavigationStack { RecordingView( @@ -100,6 +101,7 @@ private extension ContentView { ) } .disabled(model.locales.isEmpty) + .accessibilityHint(Text("view.recording.picker.locale.hint")) } /// The stack of the model's in-app notifications, each one sliding in from the leading edge when posted and out again when @@ -116,10 +118,14 @@ private extension ContentView { Text(notification.textMessage) } icon: { Image(systemName: notification.imageSymbol) + .accessibilityHidden(true) } .labelStyle(.notification( event: notification.event )) + .accessibilityElement( + children: .combine + ) .transition(.move( edge: .leading ).combined( @@ -181,35 +187,6 @@ private extension ContentView { } -// MARK: - AppNotification+Extensions - -private extension AppNotification { - - // MARK: Properties - - /// The system symbol of the icon matching the notification's event. - var imageSymbol: String { - switch event { - case .cancelled: "xmark.circle" - case .failed: "exclamationmark.triangle" - case .started: "arrow.down.circle" - } - } - - // MARK: Methods - - /// The localized message of the notification, naming the locale of the speech model, from the app's string catalog. - var textMessage: LocalizedStringResource { - switch event { - case .cancelled: .viewRecordingNotificationDownloadCancelled(localeName) - case .failed: .viewRecordingNotificationDownloadFailed(localeName) - case .started: .viewRecordingNotificationDownloadStarted(localeName) - } - } - -} - - // MARK: - Constants /// The constant values used across the view.