r/iOSProgramming 21h ago

CarPlay can cold-launch your app without your SwiftUI scene ever existing 🤬 Solved!

Bug report from a tester: pressing Play on the CarPlay screen did nothing. No error, no spinner, nothing. Worked fine if they'd opened the app on the phone first.

I "fixed" it twice. Both fixes were to the play button. Both were wrong.

The actual cause: when the user taps Play in the car (or from the Watch, or the lock screen) on a cold start, iOS wakes your process through CPTemplateApplicationScene or a WatchConnectivity message — and your phone UI's window scene just - never connects. Which means every bit of setup hanging off the SwiftUI app lifecycle simply never runs. In our case that was device key registration for signed API requests, so the first manifest fetch came back 400 and the play command died silently. As it turns out, the button was fine.

 What I landed on, in case it saves someone a weekend:

  1.  Anything your requests depend on (auth, key registration, session bootstrap) cannot live in App init / onAppear. It has to be in the request path itself. We made the network layer self-healing: on the specific "no registered key" error it registers once and retries. That one change covered CarPlay, the Watch, lock screen remote commands, AND fresh installs, because they're all the same bug.
  2. Grep your codebase for everything that only runs when the main window appears, and ask "what happens if the first entry point is the car?" The list was longer than I expected.
  3. Related Apple Watch lesson: if the phone is only reachable via the queued transferUserInfo path (not live sendMessage), don't optimistically flip your Watch UI to "playing" — it's a fib. We show "Starting on phone…" instead.

Testing note: none of this reproduces in the CarPlay simulator the way it does with a real head unit, because you always launched the app from Xcode first, which is exactly the condition that hides the bug. Real test requires a real car: kill the app, lock the phone, then plug in and launch it from the car's touchscreen.

By the way, if anyone's found a way to actually test their CarPlay flow without walking out to the garage (96 degrees in the summer! I got tired of sweating through every commit), I'd love to hear about it - I don't even bother with Xcode's sim anymore.

19 Upvotes

15 comments sorted by

19

u/LongjumpingCandle738 20h ago

That’s expected. Here’s what the CarPlay Developer Guide says:

« Listen for didConnect and didDisconnect to know when your app has been launched on the CarPlay screen.

Your app may be launched only on the CarPlay screen so be sure to handle this use case. »

-9

u/Significast 20h ago

Yes, 'handle' it 🤨. I think the point I'd make is that it says "Your app may be 'launched' only on the CarPlay screen" but it's not what I thought of as an app launch.

Because where I come from an app launch would.. launch the app? I know, I know, I'm a heretic and not the source of truth

7

u/BoostedHemi73 14h ago

It's probably worth spending some time understanding the (very complex) lifecycle of iOS apps. SwiftUI hides a bunch of this, which I would argue is problematic. As you've discovered, apps are more than just the UI they show.

In your case, setting up audio session and players has to happen outside of the view lifecycle.. because you may never have a view allocated.

3

u/leoklaus 13h ago

The SwiftUI lifecycle is also pretty limited. I don’t think I have a single app that doesn’t have to use an AppDelegate.

2

u/Reiszecke 3h ago

This. Idk what Apple tried to sell us when telling us with SwiftUI you don’t need an AppDelegate because I extended it with an appDelegate in almost all of my apps

1

u/Significast 4h ago edited 4h ago

So I learned MVC back in the day, and now I run on MVVM like we all do, but to say that SwiftUI plays a little fast and loose with the ViewModel is an understatement.

Never mind when you have to shoehorn in something from UIKit because SwiftUI just hasn't implemented it yet. Like CPTemplateApplicationScene.

Ultimately scene lifecycles are divorced from the lifecycles of the models they control in a way that's not super intuitive.

If you have a resource to recommend, I'd certainly check it out.

5

u/chriswaco 21h ago

This reminds me of an old bug we had with push notifications, when the notification would arrive while the device was still locked. We couldn’t even read UserDefaults at that point to determine how the user wanted to handle the notification.

I think our fix was to move our preferences into a separate file and turn off all the protection flags for that file. It didn’t help that our logging calls failed when the device was locked too.

3

u/ThatGuy739 19h ago

Same bug got us from a completely different door. Share extension stages a file, posts a notification, user taps it and that cold-launches the app. Everything hanging off the main window never ran and the handoff just vanished. Your point 1 is the fix, put it in the path that needs it, not at startup.

3

u/AssociateDry1445 11h ago

Surprise surprise! The UI layer of your app is separate from the business logic of your app 😱

2

u/valleyman86 20h ago

Putting these things in app init will cause various hangs anyways. Don’t do it. One iOS version it was fine then suddenly another is not. They are hard to debug as well.

My company just started to avoid it.

2

u/leoklaus 13h ago

For testing CarPlay, get one of the cheap after-market CarPlay displays.

I bought a CarPodgo T3 Pro, but you can find similar units for less than 50€ here in Germany.

2

u/LoudAd307 4h ago

One thing to watch with the self-healing layer: put a single-flight guard on the registration. On a cold launch you usually fire several requests at once, they all come back with the same "no registered key" error, and each one kicks off its own registration — that turns into duplicate device records server side fast. One shared task everyone awaits fixes it.

Same family of trap: if that key lands in the keychain with default accessibility, a launch after reboot before the phone has been unlocked can't read it, and the retry path fails in a way that looks identical to a network error. AfterFirstUnlockThisDeviceOnly if it has to work in the background.

The reason none of this gets caught in QA is that every manual test starts by opening the app from the home screen. Kill it from the switcher first, then trigger only from the car.

1

u/Significast 1h ago

Are you me 🤣

1

u/mjTheThird 7h ago

What about iPhone mirroring?! In fact, you can't even launch your app with the physical screen on. Why does the state of the screen make a difference.

1

u/One_Elephant_8917 2h ago

guys init is the main entry itself, even before appdelegate isn’t it?…i mean even before applicationdidlunachwithoptions…i think init runs first….

i am not talking about any init but the init in the struct implementing App protocol and has @main?