Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions App/AppLauncher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import SwiftUI

/// Entry point: unit tests get an empty host app, so launch work (database, sync, menu bar) never runs under XCTest.
@main
enum AppLauncher {
static func main() {
if isRunningUnitTests {
UnitTestHostApp.main()
} else {
StudyAppApp.main()
}
}

private static var isRunningUnitTests: Bool {
let environment = ProcessInfo.processInfo.environment
return environment["XCTestConfigurationFilePath"] != nil
|| environment["XCTestBundlePath"] != nil
|| environment["XCTestSessionIdentifier"] != nil
}
}

/// Host app for the unit tests; the tests build what they need themselves.
private struct UnitTestHostApp: App {
var body: some Scene {
WindowGroup { EmptyView() }
}
}
1 change: 0 additions & 1 deletion App/StudyAppApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import AppIntents
import CoreSpotlight
import SwiftUI

@main
struct StudyAppApp: App {
private let container: AppContainer
private let autopilot: LectureAutopilot
Expand Down
6 changes: 4 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ StudyApp follows a pragmatic Clean Architecture (ports and adapters). The goal i

```text
┌──────────────────────────────────────────────────────────────┐
│ App StudyAppApp (@main) · AppContainer · AppUpdater │ wires everything together
│ App AppLauncher (@main) · StudyAppApp · AppContainer │ wires everything together
├──────────────────────────────────────────────────────────────┤
│ Presentation SwiftUI views · @Observable view models │
├──────────────────────────────────────────────────────────────┤
Expand All @@ -26,14 +26,16 @@ Dependencies point **inward**: Presentation and Data both depend on Domain, Doma
| Presentation | `Presentation/` | Domain, SwiftUI, AppKit, WebKit | One folder per feature with views and view models; `DesignSystem/` for shared components and `Theme` |
| Shared | `Shared/` | Foundation | Data contract between app and widget extension (`WidgetSnapshot`, `StudyAppLink`) |
| Widgets | `Widgets/` | Shared, WidgetKit, SwiftUI | Widget extension; reads the snapshot only |
| App | `App/` | everything | `AppContainer` (composition root), `StudyAppApp`, `AppUpdater` |
| App | `App/` | everything | `AppContainer` (composition root), `AppLauncher`, `StudyAppApp`, `AppUpdater` |

## Composition root

`App/AppContainer.swift` is the only place that knows concrete implementations. It creates the SwiftData container, every repository and adapter, and offers factory methods such as `makeModulesViewModel()` or `makeCourseFilesViewModel(module:)`. Views receive view models or factory closures (`SlidesViewModelFactory`, `CourseFilesViewModelFactory`) instead of creating dependencies themselves.

`AppContainer(inMemory: true)` swaps in in-memory persistence and preferences for previews and tests.

`AppLauncher` is the entry point: under XCTest it starts an empty host app instead of `StudyAppApp`, so launch work (database, sync, menu bar item) never runs during unit tests. On CI runners the menu bar scene kept the test host from finishing its launch.

## Data flow example: calendar subscription → module

1. `StudyCalendarViewModel.subscribe(to:)` normalises the address (`CalendarFeedURL`) and calls `SyncCalendarSubscriptionUseCase`.
Expand Down
Loading