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:
@@ -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
@@ -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])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user