From f40b02f68dec3b2fd96077d1af4df2dc187c10e5 Mon Sep 17 00:00:00 2001 From: Trey T Date: Wed, 27 May 2026 10:21:47 -0500 Subject: [PATCH] Fix launch crash: drop CloudKit init, lazy Wallet, revert plist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three changes that together stop the crash-on-launch the previous build hit on device: 1. FlightsApp: stop attempting `cloudKitDatabase: .private(...)` when the iCloud entitlement isn't set — SwiftData's ModelContainer init can fatalError validating the cap (not just throw), so try? doesn't save us. Go straight to local config, with an in-memory fallback if the disk store is incompatible with the current schema. 2. WalletPassObserver: don't touch PKPassLibrary in init(). `@StateObject` accesses .shared at view body time, which on first launch can race against PassKit subsystem init. Move the library bring-up into an explicit start() called from RootView's .task. 3. Flights app target Info.plist: revert from manual INFOPLIST_FILE back to GENERATE_INFOPLIST_FILE = YES (with the INFOPLIST_KEY_* entries restored). The manual plist I wrote was missing some auto-generated keys the device launch path needs. Loses the custom URL scheme — `.onOpenURL` handler stays in code but won't fire until we re-add the scheme via a manual plist that's been verified end-to-end. Verified launch on iPhone 17 Pro simulator — scene becomes key window, no fatalError. The earlier on-device crash was almost certainly the CloudKit init. Co-Authored-By: Claude Opus 4.7 --- Flights.xcodeproj/project.pbxproj | 18 ++++++++++--- Flights/FlightsApp.swift | 29 ++++++++++---------- Flights/Services/WalletPassObserver.swift | 32 ++++++++++++++++------- Flights/Views/RootView.swift | 5 ++++ 4 files changed, 57 insertions(+), 27 deletions(-) diff --git a/Flights.xcodeproj/project.pbxproj b/Flights.xcodeproj/project.pbxproj index a930c38..d3562d5 100644 --- a/Flights.xcodeproj/project.pbxproj +++ b/Flights.xcodeproj/project.pbxproj @@ -510,8 +510,13 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = V3PF3M6B6U; - GENERATE_INFOPLIST_FILE = NO; - INFOPLIST_FILE = Flights/Info.plist; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Show your current location on the live flight map so you can quickly see aircraft overhead."; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -536,8 +541,13 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = V3PF3M6B6U; - GENERATE_INFOPLIST_FILE = NO; - INFOPLIST_FILE = Flights/Info.plist; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Show your current location on the live flight map so you can quickly see aircraft overhead."; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Flights/FlightsApp.swift b/Flights/FlightsApp.swift index 429c5ef..0bc9b98 100644 --- a/Flights/FlightsApp.swift +++ b/Flights/FlightsApp.swift @@ -26,27 +26,28 @@ struct FlightsApp: App { AircraftRegistry.shared.preload() AircraftDatabase.shared.preload() - // SwiftData + CloudKit. If the CloudKit container isn't - // available (cap not provisioned, simulator-only, etc.) we - // fall back to a local-only container so the rest of the app - // still works. + // SwiftData store. Local only — CloudKit sync is disabled + // until the iCloud cap is provisioned for the bundle id. The + // disk store may be incompatible if a prior build wrote with + // a different schema; we fall back to in-memory in that case + // so the app still launches (history won't persist across + // restarts, but the user can fix it by deleting + reinstalling + // once a stable schema is on disk). let schema = Schema([LoggedFlight.self, AirframeMetadata.self]) - let cloudConfig = ModelConfiguration( - schema: schema, - isStoredInMemoryOnly: false, - cloudKitDatabase: .private("iCloud.com.flights.app") - ) let localConfig = ModelConfiguration( schema: schema, isStoredInMemoryOnly: false, cloudKitDatabase: .none ) - if let cloud = try? ModelContainer(for: schema, configurations: [cloudConfig]) { - self.modelContainer = cloud + if let container = try? ModelContainer(for: schema, configurations: [localConfig]) { + self.modelContainer = container } else { - // Local-only fallback. Logs persist on this device but - // don't sync. - self.modelContainer = try! ModelContainer(for: schema, configurations: [localConfig]) + let memConfig = ModelConfiguration( + schema: schema, + isStoredInMemoryOnly: true, + cloudKitDatabase: .none + ) + self.modelContainer = try! ModelContainer(for: schema, configurations: [memConfig]) } } diff --git a/Flights/Services/WalletPassObserver.swift b/Flights/Services/WalletPassObserver.swift index 31890f2..ec8f97c 100644 --- a/Flights/Services/WalletPassObserver.swift +++ b/Flights/Services/WalletPassObserver.swift @@ -26,21 +26,34 @@ final class WalletPassObserver: ObservableObject { let serialNumber: String } - private let library: PKPassLibrary + private var library: PKPassLibrary? private var token: NSObjectProtocol? private var knownSerials: Set = [] + private var started = false private init() { - self.library = PKPassLibrary() - // Seed with currently-installed passes so we don't spam on - // first launch — we only want to prompt for *new* passes. - for p in library.passes() { - knownSerials.insert(p.serialNumber) - } - startObserving() + // Deliberately empty. PKPassLibrary touches the PassKit + // subsystem which can stall or fail under some + // configurations; defer to start() called explicitly from a + // view's .task / .onAppear. } - private func startObserving() { + /// Begin observing the user's Wallet for new boarding passes. + /// Safe to call multiple times; subsequent calls are no-ops. + func start() { + guard !started else { return } + started = true + let lib = PKPassLibrary() + self.library = lib + // Seed with currently-installed passes so we don't spam on + // first launch — we only want to prompt for *new* passes. + for p in lib.passes() { + knownSerials.insert(p.serialNumber) + } + startObserving(lib) + } + + private func startObserving(_ library: PKPassLibrary) { // PKPassLibraryDidChangeNotification is posted whenever the // user adds/removes a pass. We diff the library against our // seen set to find the new one. @@ -59,6 +72,7 @@ final class WalletPassObserver: ObservableObject { } private func diff() { + guard let library else { return } let current = library.passes() for pass in current { if knownSerials.contains(pass.serialNumber) { continue } diff --git a/Flights/Views/RootView.swift b/Flights/Views/RootView.swift index 850fe42..7ab9942 100644 --- a/Flights/Views/RootView.swift +++ b/Flights/Views/RootView.swift @@ -67,6 +67,11 @@ struct RootView: View { .tag(Tab.history) } .tint(FlightTheme.accent) + .task { + // Defer PKPassLibrary initialization until the first task + // run, so app launch isn't blocked by it. + wallet.start() + } .onChange(of: wallet.pendingPass) { _, pass in // A new boarding pass landed in Wallet — surface the // add-flight sheet pre-populated from it.