Reorganized the Sample app target into folders and also, extracted the ContentViewModel view model from the ContentView view.
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import SwiftUI
|
||||
|
||||
/// The entry point of the Attendi sample app.
|
||||
///
|
||||
/// The app showcases the Recording feature of the `Features` package, attached to its real audio services: it records audio from the
|
||||
/// device's microphone, transcribes the recording into text on device, and presents the result in a modal sheet.
|
||||
@main
|
||||
struct AttendiApp: App {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"sourceLanguage" : "en",
|
||||
"strings" : {
|
||||
"view.recording.navigation.title" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Recording"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"view.transcription.navigation.title" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Transcription"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version" : "1.2"
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
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,
|
||||
/// and presents the transcribed text of every processed recording in a modal sheet.
|
||||
struct ContentView: View {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The transcription currently presented in the modal sheet, or `nil` when none is shown.
|
||||
@State private var transcription: Transcription?
|
||||
|
||||
// MARK: Body
|
||||
|
||||
/// The content of the view: the recording feature's view, attached to the microphone-backed recording service and the on-device speech
|
||||
/// transcribing service, with the transcribed text of every processed recording presented in a modal sheet.
|
||||
var body: some View {
|
||||
RecordingView(
|
||||
recorder: AudioRecordingService(),
|
||||
transcriber: AudioTranscribingService()
|
||||
) { text in
|
||||
transcription = .init(text: text)
|
||||
}
|
||||
.sheet(item: $transcription) { transcription in
|
||||
NavigationStack {
|
||||
ScrollView {
|
||||
Text(transcription.text)
|
||||
.frame(
|
||||
maxWidth: .infinity,
|
||||
alignment: .leading
|
||||
)
|
||||
.padding()
|
||||
}
|
||||
.navigationTitle("Transcription")
|
||||
#if !os(macOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
.toolbar {
|
||||
Button("Done") {
|
||||
self.transcription = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
.presentationDetents([.medium, .large])
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Models
|
||||
|
||||
/// A transcription of a processed recording to present in the modal sheet.
|
||||
private struct Transcription: Identifiable {
|
||||
/// The unique identifier of the transcription.
|
||||
let id = UUID()
|
||||
/// The transcribed text of the processed recording.
|
||||
let text: String
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview(
|
||||
"Content view"
|
||||
) {
|
||||
ContentView()
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import Foundation
|
||||
import Observation
|
||||
import Recording
|
||||
|
||||
extension ContentView {
|
||||
|
||||
/// The observable model that drives ``ContentView``.
|
||||
///
|
||||
/// The model owns the recording and transcribing services attached to the recording feature, and holds the transcription currently
|
||||
/// presented in the view's modal sheet: ``received(_:)`` wraps the transcribed text of a processed recording for presentation, and
|
||||
/// ``dismissed()`` clears it when the sheet closes.
|
||||
@MainActor
|
||||
@Observable
|
||||
final class Model {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The transcription currently presented in the modal sheet, or `nil` when none is shown.
|
||||
var transcription: Transcription?
|
||||
|
||||
/// The service that captures the audio from the device's microphone.
|
||||
@ObservationIgnored
|
||||
let recorder = AudioRecordingService()
|
||||
|
||||
/// The service that transcribes the recorded audio into text on device.
|
||||
@ObservationIgnored
|
||||
let transcriber = AudioTranscribingService()
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
/// Handles the transcribed text of a processed recording, presenting it in the modal sheet.
|
||||
///
|
||||
/// - Parameter text: The transcribed text of the processed recording.
|
||||
func received(
|
||||
_ text: String
|
||||
) {
|
||||
transcription = .init(text: text)
|
||||
}
|
||||
|
||||
/// Handles the dismissal of the modal sheet.
|
||||
func dismissed() {
|
||||
transcription = nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Models
|
||||
|
||||
extension ContentView.Model {
|
||||
/// A transcription of a processed recording to present in the modal sheet.
|
||||
struct Transcription: Identifiable {
|
||||
/// The unique identifier of the transcription.
|
||||
let id = UUID()
|
||||
/// The transcribed text of the processed recording.
|
||||
let text: String
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
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.
|
||||
///
|
||||
/// The services attached to the feature and the transcription shown in the sheet live in the view's ``Model``; the view itself only renders it and
|
||||
/// forwards the feature's output and the sheet's dismissal. The navigation titles of the stacks are localized through the app's string catalog.
|
||||
struct ContentView: View {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
/// The model that owns the attached services and the transcription presented in the modal sheet.
|
||||
@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 a modal sheet
|
||||
/// presenting the transcribed text of every processed recording, closable through its toolbar button or a swipe.
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
RecordingView(
|
||||
recorder: model.recorder,
|
||||
transcriber: model.transcriber
|
||||
) { text in
|
||||
model.received(text)
|
||||
}
|
||||
.navigationTitle("view.recording.navigation.title")
|
||||
#if !os(macOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
}
|
||||
.sheet(item: $model.transcription) { transcription in
|
||||
NavigationStack {
|
||||
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: - Previews
|
||||
|
||||
#Preview(
|
||||
"Content view"
|
||||
) {
|
||||
ContentView()
|
||||
}
|
||||
@@ -21,9 +21,11 @@
|
||||
02870A1F2FF7EB680079EA3A /* Exceptions for "Apps" folder in "Attendi" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Attendi/Assets.xcassets,
|
||||
Attendi/AttendiApp.swift,
|
||||
Attendi/ContentView.swift,
|
||||
Attendi/App/AttendiApp.swift,
|
||||
Attendi/Catalogs/Assets.xcassets,
|
||||
Attendi/Catalogs/Localizable.xcstrings,
|
||||
"Attendi/View Models/ContentViewModel.swift",
|
||||
Attendi/Views/ContentView.swift,
|
||||
);
|
||||
target = 02870A0C2FF7EB610079EA3A /* Attendi */;
|
||||
};
|
||||
@@ -164,6 +166,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
DEVELOPMENT_TEAM = 7FMNM89WKG;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -171,6 +174,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
DEVELOPMENT_TEAM = 7FMNM89WKG;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@@ -243,9 +247,9 @@
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Apps/Attendi/Attendi-Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Attendi (Sample)";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.medical";
|
||||
INFOPLIST_FILE = "Apps/Attendi/Resources/Attendi-Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Attendi;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Attendi uses the microphone to record the audio it transcribes into text.";
|
||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
|
||||
@@ -353,9 +357,9 @@
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Apps/Attendi/Attendi-Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Attendi (Sample)";
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.medical";
|
||||
INFOPLIST_FILE = "Apps/Attendi/Resources/Attendi-Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Attendi;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Attendi uses the microphone to record the audio it transcribes into text.";
|
||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
|
||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
|
||||
|
||||
Reference in New Issue
Block a user