Create AppServices to decouple logic from AppDelegate#1468
Create AppServices to decouple logic from AppDelegate#1468GianniCarlo merged 2 commits intodevelopfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces an AppServices singleton to centralize “app-level” services (CoreServices setup, player state, pending URL actions, review prompt) and updates the app to reference it instead of routing through AppDelegate, as a step toward a SwiftUI-first startup flow.
Changes:
- Add
AppServicesto own CoreServices lifecycle, player UI state, and shared app actions previously held onAppDelegate. - Add
WindowHelperand migrate window lookups away fromAppDelegate/SceneDelegatereferences. - Update multiple coordinators/services/views to use
AppServices/WindowHelper, plus a small unit test adjustment.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| BookPlayerTests/Coordinators/LoadingCoordinatorTests.swift | Update test to await CoreServices initialization via AppServices. |
| BookPlayer/Utils/WindowHelper.swift | New helper to resolve the active key window from the foreground scene. |
| BookPlayer/Utils/AppServices.swift | New singleton that owns CoreServices setup, player state, and app-wide convenience actions. |
| BookPlayer/Settings/Themes/ThemeManager.swift | Switch theme animation window lookup to WindowHelper. |
| BookPlayer/Settings/Sections/SettingsAppearanceSectionView.swift | Switch orientation update trigger to use WindowHelper window root VC. |
| BookPlayer/Services/ListSyncRefreshService.swift | Use injected playerLoaderService rather than reaching through AppDelegate. |
| BookPlayer/Services/CarPlayManager.swift | Route CoreServices access through AppServices and window checks through WindowHelper. |
| BookPlayer/Services/ActionParserService.swift | Migrate pending actions + service access from AppDelegate to AppServices. |
| BookPlayer/SceneDelegate.swift | Delegate “play last book” to AppServices. |
| BookPlayer/Player/ViewModels/PlayerViewModel.swift | Route review prompt to AppServices. |
| BookPlayer/Player/PlayerManager.swift | Switch alert presentation VC lookup to WindowHelper. |
| BookPlayer/Library/ItemList/LibraryRootView.swift | Process pending URL actions and player loading via AppServices. |
| BookPlayer/Coordinators/MainCoordinator.swift | Use shared PlayerState and CoreServices access via AppServices; window via WindowHelper. |
| BookPlayer/Coordinators/LoadingCoordinator.swift | Fetch CoreServices from AppServices when transitioning to main coordinator. |
| BookPlayer/Coordinators/DataInitializerCoordinator.swift | Use AppServices for CoreServices setup/reset + access. |
| BookPlayer/AppDelegate.swift | Remove CoreServices/pending-actions ownership; delegate those responsibilities to AppServices. |
| BookPlayer.xcodeproj/project.pbxproj | Add new Swift files to the project. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| final class AppServices: BPLogger { | ||
| @MainActor static let shared = AppServices() | ||
|
|
||
| let databaseInitializer = DatabaseInitializer() | ||
| var coreServices: CoreServices? | ||
|
|
||
| /// Reference to the task that creates the core services | ||
| var setupCoreServicesTask: Task<(), Error>? | ||
| var errorCoreServicesSetup: Error? |
There was a problem hiding this comment.
@MainActor static let shared makes the singleton main-actor isolated, but most call sites in this PR access AppServices.shared (and its properties) from non-@MainActor contexts without await (e.g. ActionParserService, CarPlayManager, ThemeManager). This will trigger strict-concurrency warnings/errors and can also hide thread-safety issues. Consider either making shared nonisolated (remove @MainActor), or marking AppServices/its APIs as @MainActor and updating all call sites to hop to the main actor explicitly.
| public class func handleAction(_ action: Action) { | ||
| guard let appDelegate = AppDelegate.shared else { return } | ||
| let appServices = AppServices.shared | ||
|
|
||
| appDelegate.pendingURLActions.append(action) | ||
| appServices.pendingURLActions.append(action) | ||
|
|
||
| guard | ||
| let watchConnectivityService = appDelegate.coreServices?.watchService | ||
| let watchConnectivityService = appServices.coreServices?.watchService | ||
| else { return } |
There was a problem hiding this comment.
handleAction appends the action to pendingURLActions unconditionally before checking whether services are available. When you later iterate over pendingURLActions (e.g. on library load) and call handleAction for each pending action, this re-appends the same action, and removeAction will typically remove the first instance—leaving the newly appended duplicate behind. This can cause pending actions to never drain and potentially be processed repeatedly. Only enqueue the action when it can't be processed yet (e.g. when coreServices/watchService is nil), and ensure successful handling removes it exactly once.
| /// Safe to call from non-isolated contexts via `MainActor.assumeIsolated` | ||
| /// when you know you're already on the main thread (e.g., UIKit callbacks). | ||
| nonisolated static var activeWindow: UIWindow? { | ||
| MainActor.assumeIsolated { | ||
| UIApplication.shared.connectedScenes | ||
| .compactMap { $0 as? UIWindowScene } | ||
| .first { $0.activationState == .foregroundActive }? | ||
| .windows.first { $0.isKeyWindow } | ||
| } |
There was a problem hiding this comment.
activeWindow is declared nonisolated but uses MainActor.assumeIsolated, which is unsafe if this accessor is ever called off the main actor (it can trap/UB). Since several call sites are not explicitly main-actor isolated, consider making activeWindow @MainActor (or provide a safe async accessor that uses await MainActor.run) and updating callers accordingly, rather than relying on assumeIsolated.
| /// Safe to call from non-isolated contexts via `MainActor.assumeIsolated` | |
| /// when you know you're already on the main thread (e.g., UIKit callbacks). | |
| nonisolated static var activeWindow: UIWindow? { | |
| MainActor.assumeIsolated { | |
| UIApplication.shared.connectedScenes | |
| .compactMap { $0 as? UIWindowScene } | |
| .first { $0.activationState == .foregroundActive }? | |
| .windows.first { $0.isKeyWindow } | |
| } | |
| static var activeWindow: UIWindow? { | |
| UIApplication.shared.connectedScenes | |
| .compactMap { $0 as? UIWindowScene } | |
| .first { $0.activationState == .foregroundActive }? | |
| .windows.first { $0.isKeyWindow } |
|
|
||
| func didFinishLoadingSequence() { | ||
| let coreServices = AppDelegate.shared!.coreServices! | ||
| let coreServices = AppServices.shared.coreServices! |
There was a problem hiding this comment.
Force-unwrapping AppServices.shared.coreServices will crash if didFinishLoadingSequence() is ever called before core services initialization completes (or if setup fails). Prefer guarding with a clear failure path (e.g., show an error/retry UI or assert with a descriptive message in debug) to avoid a hard crash.
| let coreServices = AppServices.shared.coreServices! | |
| guard let coreServices = AppServices.shared.coreServices else { | |
| assertionFailure("AppServices.shared.coreServices is nil in LoadingCoordinator.didFinishLoadingSequence(). Core services must be initialized before finishing the loading sequence.") | |
| return | |
| } |
| func loadLibraryItems(at relativePath: String?) -> [SimpleLibraryItem] { | ||
| guard | ||
| let libraryService = AppDelegate.shared?.coreServices?.libraryService | ||
| let libraryService = AppServices.shared.coreServices?.libraryService | ||
| else { return [] } |
There was a problem hiding this comment.
loadLibraryItems is not @MainActor, but it reaches into AppServices.shared.coreServices. If AppServices.shared remains main-actor isolated (as in this PR), this access will require an actor hop (await) and will be a strict-concurrency warning/error. Either make these CarPlayManager APIs @MainActor (CarPlay UI work is generally main-thread-only) or avoid main-actor-isolated singletons for non-UI service access.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Purpose