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
|
import SwiftUI
|
||||||
|
|
||||||
/// The entry point of the Attendi sample app.
|
/// 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
|
@main
|
||||||
struct AttendiApp: App {
|
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 */ = {
|
02870A1F2FF7EB680079EA3A /* Exceptions for "Apps" folder in "Attendi" target */ = {
|
||||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||||
membershipExceptions = (
|
membershipExceptions = (
|
||||||
Attendi/Assets.xcassets,
|
Attendi/App/AttendiApp.swift,
|
||||||
Attendi/AttendiApp.swift,
|
Attendi/Catalogs/Assets.xcassets,
|
||||||
Attendi/ContentView.swift,
|
Attendi/Catalogs/Localizable.xcstrings,
|
||||||
|
"Attendi/View Models/ContentViewModel.swift",
|
||||||
|
Attendi/Views/ContentView.swift,
|
||||||
);
|
);
|
||||||
target = 02870A0C2FF7EB610079EA3A /* Attendi */;
|
target = 02870A0C2FF7EB610079EA3A /* Attendi */;
|
||||||
};
|
};
|
||||||
@@ -164,6 +166,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
DEVELOPMENT_TEAM = 7FMNM89WKG;
|
DEVELOPMENT_TEAM = 7FMNM89WKG;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
@@ -171,6 +174,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
DEVELOPMENT_TEAM = 7FMNM89WKG;
|
DEVELOPMENT_TEAM = 7FMNM89WKG;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
@@ -243,9 +247,9 @@
|
|||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = "Apps/Attendi/Attendi-Info.plist";
|
INFOPLIST_FILE = "Apps/Attendi/Resources/Attendi-Info.plist";
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Attendi (Sample)";
|
INFOPLIST_KEY_CFBundleDisplayName = Attendi;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.medical";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Attendi uses the microphone to record the audio it transcribes into text.";
|
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=iphoneos*]" = YES;
|
||||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
|
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
|
||||||
@@ -353,9 +357,9 @@
|
|||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = "Apps/Attendi/Attendi-Info.plist";
|
INFOPLIST_FILE = "Apps/Attendi/Resources/Attendi-Info.plist";
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Attendi (Sample)";
|
INFOPLIST_KEY_CFBundleDisplayName = Attendi;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.medical";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Attendi uses the microphone to record the audio it transcribes into text.";
|
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=iphoneos*]" = YES;
|
||||||
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
|
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
|
||||||
|
|||||||
Reference in New Issue
Block a user