import Recording import SwiftUI /// The root view of the Attendi sample app. /// /// The view hosts the `RecordingView` feature from the `Recording` target of the `Features` package, which drives the whole recording flow, /// inside a navigation stack, and presents the transcribed text of every processed recording in a modal sheet of its own navigation stack. /// A toolbar menu picks the locale of the spoken language to transcribe, and every pick kicks off the preinstallation of the locale's speech /// model assets, whose download events surface as transient in-app notifications overlaying the feature just below the navigation bar. /// /// The services attached to the feature, the transcription shown in the sheet, and the posted notifications live in the view's ``Model``; /// the view itself only renders it and forwards the feature's output, the picker's changes, and the sheet's dismissal. All user-facing text /// is localized through the app's string catalog. struct ContentView: View { // MARK: Properties /// The model that owns the attached services, the transcription presented in the modal sheet, and the posted in-app notifications. @State private var model = Model() // MARK: Body /// The content of the view: a navigation stack with the recording feature's view, attached to the model's services and expanded to fill /// the available space, with a toolbar picker for the locale of the spoken language to transcribe, and a modal sheet presenting the /// 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. Every posted notification is also announced to assistive technologies, since the /// banners themselves are transient. var body: some View { NavigationStack { RecordingView( capturer: model.capturer, transcriber: model.transcriber, locale: $model.locale, ) { transcription in model.received(transcription) } .frame( maxWidth: .infinity, maxHeight: .infinity ) .navigationTitle("view.recording.navigation.title") #if !os(macOS) .navigationBarTitleDisplayMode(.inline) #endif .toolbar { ToolbarItem(placement: .primaryAction) { menuLocale } } .overlay( alignment: .topLeading ) { stackNotifications } .task { await model.load() } .onChange( of: model.locale, initial: false ) { model.preinstall() } } .sheet(item: $model.transcription) { transcription in sheetTranscription(for: transcription) } } } // MARK: - Subviews private extension ContentView { // MARK: Computed /// The toolbar menu that picks the locale of the spoken language to transcribe from the supported locales, disabled until /// they are loaded. var menuLocale: some View { Menu { Picker( "view.recording.picker.locale.title", selection: $model.locale ) { ForEach( model.locales, id: \.self ) { Text(model.name(for: $0)) .tag($0) } } .pickerStyle(.inline) } label: { Label( "view.recording.picker.locale.title", systemImage: "globe" ) } .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 /// its scheduled dismissal arrives. var stackNotifications: some View { VStack( alignment: .leading, spacing: Constant.Spacing.stack ) { ForEach( model.notifications ) { notification in Label { Text(notification.textMessage) } icon: { Image(systemName: notification.imageSymbol) .accessibilityHidden(true) } .labelStyle(.notification( event: notification.event )) .accessibilityElement( children: .combine ) .transition(.move( edge: .leading ).combined( with: .opacity )) } } .padding(.horizontal) .animation( .spring, value: model.notifications ) } // MARK: Methods /// Returns the content of the modal sheet presenting the given transcription — or a content unavailable message when it is /// empty — inside a navigation stack of its own, closable through its toolbar button or a swipe. /// /// - Parameter transcription: The transcription to present. /// - Returns: The content of the modal sheet. func sheetTranscription( for transcription: Transcription ) -> some View { NavigationStack { Group { if transcription.isEmpty { ContentUnavailableView( "view.transcription.unavailable.title", systemImage: "text.page.slash", description: Text("view.transcription.unavailable.description") ) } else { ScrollView { Text(transcription.text) .font(.body) .fontWeight(.regular) .foregroundStyle(.primary) .frame( maxWidth: .infinity, alignment: .leading ) .padding() } } } .navigationTitle("view.transcription.navigation.title") #if !os(macOS) .navigationBarTitleDisplayMode(.inline) #endif .toolbar { Button(role: .close) { model.dismissed() } } } .presentationDetents([.medium, .large]) } } // MARK: - Constants /// The constant values used across the view. private enum Constant { /// The spacing constants. enum Spacing { /// The spacing between the elements of a stack. static let stack: CGFloat = 8 } } // MARK: - Previews #Preview( "Content view" ) { ContentView() }