Added accessibility support to in-app notifications on the ContentViewModel view model in the Sample app.
This commit is contained in:
@@ -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" : {
|
"view.recording.picker.locale.title" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
|
|||||||
@@ -47,3 +47,27 @@ extension AppNotification {
|
|||||||
case started
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import Accessibility
|
||||||
import Foundation
|
import Foundation
|
||||||
import Notifying
|
|
||||||
import Observation
|
import Observation
|
||||||
import OSLog
|
import OSLog
|
||||||
import Recording
|
import Recording
|
||||||
@@ -153,8 +153,8 @@ private extension ContentView.Model {
|
|||||||
await SpeechTranscriber.supportedLocale(equivalentTo: locale)
|
await SpeechTranscriber.supportedLocale(equivalentTo: locale)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Posts a notification for a download event of the given locale, scheduling its automatic dismissal a few seconds later —
|
/// Posts a notification for a download event of the given locale, announcing it to assistive technologies and scheduling its
|
||||||
/// the only way a notification is dismissed.
|
/// automatic dismissal a few seconds later — the only way a notification is dismissed.
|
||||||
///
|
///
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - event: The download event to notify.
|
/// - event: The download event to notify.
|
||||||
@@ -170,6 +170,10 @@ private extension ContentView.Model {
|
|||||||
|
|
||||||
notifications.append(notification)
|
notifications.append(notification)
|
||||||
|
|
||||||
|
AccessibilityNotification.Announcement(
|
||||||
|
String(localized: notification.textMessage)
|
||||||
|
).post()
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
try? await Task.sleep(for: Constant.Delay.dismissal)
|
try? await Task.sleep(for: Constant.Delay.dismissal)
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ struct ContentView: View {
|
|||||||
/// transcribed text of every processed recording — or a content unavailable message when the transcription is empty — closable
|
/// 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
|
/// 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
|
/// 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 {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
RecordingView(
|
RecordingView(
|
||||||
@@ -100,6 +101,7 @@ private extension ContentView {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
.disabled(model.locales.isEmpty)
|
.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
|
/// 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)
|
Text(notification.textMessage)
|
||||||
} icon: {
|
} icon: {
|
||||||
Image(systemName: notification.imageSymbol)
|
Image(systemName: notification.imageSymbol)
|
||||||
|
.accessibilityHidden(true)
|
||||||
}
|
}
|
||||||
.labelStyle(.notification(
|
.labelStyle(.notification(
|
||||||
event: notification.event
|
event: notification.event
|
||||||
))
|
))
|
||||||
|
.accessibilityElement(
|
||||||
|
children: .combine
|
||||||
|
)
|
||||||
.transition(.move(
|
.transition(.move(
|
||||||
edge: .leading
|
edge: .leading
|
||||||
).combined(
|
).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
|
// MARK: - Constants
|
||||||
|
|
||||||
/// The constant values used across the view.
|
/// The constant values used across the view.
|
||||||
|
|||||||
Reference in New Issue
Block a user