Apple rejected the iOS build reporting the app crashed on launch. No crash log was provided and the crash did not reproduce locally (Debug builds in the simulator / on a dev device).
Two iOS-only structural hazards in the launch path, either of which crashes the app in ways that are invisible on JVM targets:
initKoin() ran inside the ComposeUIViewController content lambda
(MainViewController.kt). Android calls startKoin once in
Application.onCreate and desktop in main(), but on iOS any second
invocation of MainViewController() — SwiftUI recreating the
UIViewControllerRepresentable after a scene reconnect (memory pressure,
backgrounding on the review device farm) — called startKoin again,
which throws KoinApplicationAlreadyStartedException and kills the app
immediately. To a reviewer that is “crashed on launch.”
The unqualified single<CoroutineScope> in sharedModule (the scope
ClientNodeManager.init launches startup work into) had no
CoroutineExceptionHandler. On Kotlin/Native an unhandled coroutine
exception terminates the whole process; on JVM targets it only logs.
Any exception in the startup coroutine (serialization, storage,
node-bookkeeping) was therefore a silent log line locally and a hard
crash on iOS.
Additionally, no setUnhandledExceptionHook was installed, so a fatal
Kotlin exception left nothing diagnosable — Apple crash reports cannot
symbolicate Kotlin/Native frames, which is why the rejection came with no
usable log.
MainViewController.kt: moved initKoin() out of composition (runs once
when the view controller factory is called) and made it idempotent via a
process-level flag; installed setUnhandledExceptionHook that NSLogs the
full Kotlin stack trace then calls terminateWithUnhandledException so a
crash report is still produced.shared/.../di/SharedModule.kt: added a logging
CoroutineExceptionHandler to the app-wide CoroutineScope, mirroring
the IO_SCOPE handler each platformModule already has.composeApp/.../App.kt: wrapped the post-init client dispatch in
runCatching and always set ready — a failure there used to leave the
app on the startup spinner forever.shared/src/jvmTest/.../di/SharedModuleScopeTest.kt
asserts the scope carries a CoroutineExceptionHandler and survives a
failing child coroutine.startKoin (or any once-per-process init) inside a composable
lambda; on iOS the Compose content lambda is re-executed whenever the
hosting view controller is recreated. Process-level init belongs at the
entry-point function, guarded idempotent.CoroutineScope registered in DI must carry a
CoroutineExceptionHandler — on Kotlin/Native a missing handler turns
any background exception into process death. SharedModuleScopeTest
guards the shared one.setUnhandledExceptionHook installed on iOS so the next
launch-crash report comes with a Kotlin stack trace in the device
console (Console.app / sysdiagnose) even when Apple provides nothing.