We recently noticed a CarPlay crash in our app piling up that started appearing after we released app version 3.2.0.
There were no compiler errors or warnings associated with the troublesome code, and we didn’t touch the implementation either. Let’s look at the issue together.
TL;DR: If you implement CarPlay’s CPPointOfInterestTemplateDelegate and have Swift’s Dynamic Actor Isolation (or Swift 6 language mode) enabled, make sure the delegate implementations are declared nonisolated.
The exception
The stack traces associated with the crash look very similar, reporting BUG IN CLIENT OF LIBDISPATCH: Assertion failed: Block was expected to execute on queue [com.apple.main-thread (0x204048280)]. The issue was reported on a wide range of devices, starting with iOS 18.6 up to iOS 26.1.
Crashed: com.apple.NSXPCConnection.user.endpoint
EXC_BREAKPOINT 0x00000001d4718540
0 libdispatch.dylib _dispatch_assert_queue_fail + 120
1 libdispatch.dylib dispatch_assert_queue$V2.cold.2 + 114
3 libdispatch.dylib dispatch_assert_queue$V2 + 108
2 libdispatch.dylib dispatch_assert_queue + 108
4 libswift_Concurrency.dylib <redacted> + 48
5 libswift_Concurrency.dylib <redacted> + 356
6 Flagship @objc ChargePointMap.pointOfInterestTemplate(_:didChangeMapRegion:) + 48900
7 CarPlay -[CPPointOfInterestTemplate handleMapRegionDidChange:] + 84
...
You can see the call from the XPC connection, calling the delegate directly on its own queue instead of the main queue.
The problem seems pretty obvious. Since the app crashes before it reaches our code and since we cannot influence CarPlay code, how do we fix this?
Analyzing the exception
Under the hood, Swift Concurreny relies on libdispatch.
The crash is an assertion in libdispatch which is complaining that the call to pointOfInterestTemplate(_:didChangeMapRegion:) is not on the main queue. This is correct since CPPointOfInterestTemplateDelegate is annotated to be isolated to the @MainActor.
I could verify this by creating an isolated project and running in Swift 5 language mode. The delegate method is called on the concurrent queue but could reach my code. This is just a broken assumption without any enforcement. If this causes issues in an app depends on the specific implementation.

Turning on Dynamic Actor Isolation, or the full Swift 6 mode, reliably produces the error. Since the runtime behavior is the same, only Swift’s enforcement of the isolation domain is what causes the issue to appear.

I don’t have any experience with interfacing Objective-C to Swift Concurrency. However, from reading the Swift Evolution proposal, I understand the Objective-C code is responsible for handling the isolation domain manually, and failing to do so correctly.
Testing other delegates
I looked around for other CPTemplates that have delegates.
In all of those implementations, I could see the delegate call being dispatched to the main queue from the XPC connection. I tested this for CPSearchTemplateDelegate and CPTabBarTemplateDelegate.
Conclusion
It seems to me there is an oversight in the implementation of CPPointOfInterestTemplate which leads to its delegate being called off the MainActor.
With its @MainActor annotation and Swift’s isolation checking, this programming error has now surfaced.
To work around it, you can declare the delegate implementation as nonisolated. This way, it is not expected to be isolated to the main queue. Dispatching to the MainActor is then the job of your client code, if needed.
Make sure to do this for both pointOfInterestTemplate(_:didChangeMapRegion) and pointOfInterestTemplate(_:didSelectPointOfInterest:).
The Swift compiler isn’t showing me any additional warnings doing it this way, so I would assume it is safe to implement this for the time beeing.
I reported this issue as FB21202147. Hopefully, we’ll see a fix in an upcoming release of iOS.
