If your Swift code contains XPC_TYPE_DICTIONARY — or any other XPC constant
— and you build it with Xcode 26 or 27, your app is terminated at launch on
iOS 15. The build produces no warning, and the crash report does not name your
code.
This package is twenty lines of C that avoid that. The code is the small part; the explanation below is the reason the repository exists.
.package(url: "https://github.com/owngoal-dev/libxpc-shim", from: "1.0.0")import XPC
import XPCShim
guard xpc_get_type(message) == XPCConstants.typeDictionary else { … }
xpc_array_set_value(array, XPCConstants.arrayAppend, entry)
if message === XPCConstants.errorConnectionInvalid { … }An app with IPHONEOS_DEPLOYMENT_TARGET = 15.0, built clean, installed on an
iPhone running iOS 15.0.2, closes the instant it opens. The report is a dyld
termination — six frames, all in dyld, nothing of yours:
"termination" : {
"namespace" : "DYLD",
"indicator" : "Library missing",
"reasons" : [
"Library not loaded: /usr/lib/swift/libswiftXPC.dylib",
"Referenced from: /…/YourApp.app/YourApp",
"Reason: tried: '/usr/lib/swift/libswiftXPC.dylib' (no such file), …"
]
}
Recent SDKs ship a Swift overlay for XPC — usr/lib/swift/XPC.swiftmodule
and libswiftXPC.tbd. Older SDKs had none: import XPC resolved to the plain
clang module over <xpc/xpc.h> and linked nothing, because every xpc_*
function lives in libSystem.
With the overlay present, three things line up:
import XPCnow loads a Swift module whose flags include-autolink-force-load, so the object file asks for-lswiftXPC.XPC_TYPE_DICTIONARY,XPC_ARRAY_APPEND,XPC_ERROR_CONNECTION_INVALIDand the rest are, in C, macros over libSystem globals (&_xpc_type_dictionary, present since iOS 8). In Swift they now resolve to accessor functions exported by the overlay dylib.libswiftXPC.tbdcarries no back-deployment metadata, and the overlay annotates those constants@available(iOS 8.0). So the linker has no reason to weak-link the library, and emits a required load command.
/usr/lib/swift/libswiftXPC.dylib first shipped in iOS 16. On iOS 15 dyld
cannot build the process, and kills it before main.
Nothing about this is visible while you work: the compiler accepts the code, the
linker accepts the library, xcodebuild is green, and the App Store or your APT
repository takes the build. The first person to find out is a user.
otool -L YourApp.app/YourApp | grep swiftXPC| output | meaning |
|---|---|
…/libswiftXPC.dylib (…, weak) |
fine — dyld tolerates its absence |
…/libswiftXPC.dylib (…) without weak |
your app cannot launch below iOS 16 |
| nothing | you never imported XPC |
And to see exactly which symbols pulled it in:
nm -mu YourApp.app/YourApp | grep libswiftXPC(undefined) external _$s3XPC0A16_TYPE_DICTIONARYs13OpaquePointerVvg (from libswiftXPC)
(undefined) external _$s3XPC0A13_ARRAY_APPENDSivg (from libswiftXPC)
(undefined) weak external __swift_FORCE_LOAD_$_swiftXPC (from libswiftXPC)
After the fix, only the last line remains — and that one is weak, so the library becomes optional again:
(undefined) weak external __swift_FORCE_LOAD_$_swiftXPC (from libswiftXPC)
That is the whole mechanism: with no overlay symbol in use, the linker weak-links the dylib by itself. No linker flag is involved, and none helps.
- Removing
import XPC. The import is harmless; the symbols are not. And a C header of your own that includes<xpc/xpc.h>still autolinks the overlay, because Swift loads the overlay for a clang module it imports transitively. Verified withswiftc -emit-ir:-lswiftXPCappears either way. - Weak-linking by hand. You can force it, but the five accessor symbols are then null and calling one is a crash at first use rather than at launch. The fix is to stop referencing them, not to make the failure later.
- Branching on
#available. Two paths mean the path the old OS takes is the one nobody ever tests. Use the C constants on every version; then iOS 26 and iOS 15 execute the same code. - Raising the deployment target to 16. That works, and it is a decision about your users rather than about XPC. Make it deliberately, not because a Swift overlay you never asked for shipped in an SDK.
CXPCShim exposes each constant as a static inline C function; XPCShim
wraps them in XPCConstants. Nothing is a wrapper around behaviour — each one
compiles to a load of the same global the C macro names. There is no library to
link, no runtime cost, and no behaviour of ours between you and XPC.
Copying Sources/CXPCShim/include/CXPCShim.h into your own project instead of
adding a dependency is a completely reasonable thing to do; that is why it is
one self-contained header.
xpc_type_t constants, XPC_BOOL_TRUE / XPC_BOOL_FALSE, XPC_ARRAY_APPEND,
the three connection errors, and the two dictionary keys we needed. Pull
requests adding a constant are welcome; pull requests adding behaviour are
not — the value of this package is that there is nothing in it.
The same trap has three other shapes, all of which raise your deployment floor without a warning:
- a weak-imported symbol newer than your floor is null at runtime:
nm -m <binary> | grep 'weak external', and each one has to be guarded; - an embedded framework built for a later OS:
vtool -show-build … | grep minos; - an SF Symbol newer than your floor:
UIImage(systemName:)returns nil and the control simply draws nothing. The availability table is on every Mac at/System/Library/CoreServices/CoreGlyphs.bundle/Contents/Resources/name_availability.plist.
platformize-app-ios carries scripts for all four checks.
Found while shipping Fila, a root file manager for jailbroken iOS, whose 0.1.6 release died at launch on two users' iOS 15 devices for exactly this reason.
MIT. See LICENSE.