Fix launch crash: drop CloudKit init, lazy Wallet, revert plist

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 <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-05-27 10:21:47 -05:00
parent d444a5caac
commit f40b02f68d
4 changed files with 57 additions and 27 deletions
+14 -4
View File
@@ -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",
+15 -14
View File
@@ -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])
}
}
+23 -9
View File
@@ -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<String> = []
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 }
+5
View File
@@ -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.