Reorganized the Sample app target into folders and also, extracted the ContentViewModel view model from the ContentView view.

This commit is contained in:
2026-07-04 10:32:59 +02:00
parent 2b62de5154
commit d7dc32d7f7
10 changed files with 172 additions and 77 deletions
@@ -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"
}
-68
View File
@@ -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
}
}
+70
View File
@@ -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()
}
+13 -9
View File
@@ -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;