Skip to content

Create AppServices to decouple logic from AppDelegate#1468

Merged
GianniCarlo merged 2 commits intodevelopfrom
feat/phase0-swiftui-only
Mar 7, 2026
Merged

Create AppServices to decouple logic from AppDelegate#1468
GianniCarlo merged 2 commits intodevelopfrom
feat/phase0-swiftui-only

Conversation

@GianniCarlo
Copy link
Copy Markdown
Collaborator

Purpose

  • First step to eventually remove UIKit from the start sequence, and make BookPlayer a full SwiftUI app

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 AppServices to own CoreServices lifecycle, player UI state, and shared app actions previously held on AppDelegate.
  • Add WindowHelper and migrate window lookups away from AppDelegate/SceneDelegate references.
  • 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.

Comment on lines +17 to +25
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?
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Copilot uses AI. Check for mistakes.
Comment on lines 38 to 45
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 }
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread BookPlayer/Utils/WindowHelper.swift Outdated
Comment on lines +14 to +22
/// 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 }
}
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
/// 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 }

Copilot uses AI. Check for mistakes.

func didFinishLoadingSequence() {
let coreServices = AppDelegate.shared!.coreServices!
let coreServices = AppServices.shared.coreServices!
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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
}

Copilot uses AI. Check for mistakes.
Comment on lines 158 to 161
func loadLibraryItems(at relativePath: String?) -> [SimpleLibraryItem] {
guard
let libraryService = AppDelegate.shared?.coreServices?.libraryService
let libraryService = AppServices.shared.coreServices?.libraryService
else { return [] }
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@GianniCarlo GianniCarlo merged commit e602376 into develop Mar 7, 2026
5 checks passed
@GianniCarlo GianniCarlo deleted the feat/phase0-swiftui-only branch March 7, 2026 18:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants