Addressing SwiftData Weakly Linking Issues on iOS 16
Since iOS 17.0, Apple has introduced SwiftData, providing a much easier way to store, retrieve, and update persistent data in a database compared to the previous Core Data framework.
I had sticked to Core Data for a long time, since all my apps have a minimum version of iOS 16. But recently, I started using SwiftData in my PhotonCam app to help build a feature that requires iOS 17 or above. Thanks to SwiftData, I no longer have to manipulate the .xcdatamodeld
file in Xcode or write so much boilerplate code to implement this feature.
Code, build, run, archive, and release to TestFlight—all of those steps worked well, until one of the tester reported that the app crashes on an iPhone running iOS 16.
The following content will discuss how this crash happened and how to address the issue in various cases.
How does the crash look
First of all, it’s my bad to not check the app running on iOS 16.0 emulator. It crashes as the app launches:
And you can find some message in the console:
Library not loaded: /System/Library/Frameworks/SwiftData.framework/SwiftData
The thing is: even my code is wrapped with Swift’s Availability checks, the system will still attempt to link against SwiftData at launch. How you address this issue depends on several scenarios:
The SwiftData usage code are compiled to the main executable.
The SwiftData usage code resides in a statically linked Swift package.
The SwiftData usage code resides in a dynamically linked Swift Package.
The SwiftData usage code are compiled to the main executable
Here’s a minimum sample project to reproduce this issue:
An iOS project with a minimum deployment version of iOS 16.0.
The target to run contains some code that uses SwiftData, for example, defining a model using the
@Model
Marco. To make it compile, it’s marked with@available(iOS 17.0, *)
.
That’s it. You don’t even have to reference the SwiftData code at runtime, such as by creating a ModelContainer
.
To address the linking issue in this case, simply mark SwiftData
to be optionally linked in the Xcode settings.
Go to the target settings in Xcode.
Navigate to Build Phrases and Link Binary With Libraries.
Add
SwiftData.framework
and set it asOptional
.
You may need to restart and rebuild the project to make work.
Since you already check for availability before referencing any SwiftData symbols (otherwise, the code wouldn’t compile), no code changes are required.
The SwiftData usage code resides in a statically linked Swift package
A minimum sample project to reproduce the issue in this case:
An iOS project with a minimum deployment version of iOS 16.0.
The target has a dependency on a Swift Package.
That Swift Package contains some code that uses SwiftData.
That Swift Package is statically linked, with its product type explicitly set to
static
or left to empty.
In this case, the solution to the linking issue is the same as before: simply go to Link Binary With Libraries and add SwiftData.framework
, setting it as Optional
.
The SwiftData usage code resides in a dynamically linked Swift package
A minimum sample project to reproduce the issue in this case:
An iOS project with a minimum deployment version of iOS 16.0.
The target has a dependency on a Swift Package.
That Swift Package contains some code that use SwiftData.
That Swift Package is dynamically linked, with its product type explicitly set to
dynamic
.
That Swift Package sets its platform to iOS 16 or later.
The Package.swift
file should look like this:
let package = Package(
name: "MyLibrary",
platforms: [.iOS(.v16)],
products: [
.library(name: "MyLibrary", type: .dynamic, targets: ["MyLibrary"]),
],
targets: [.target(name: "MyLibrary")],
)
Note that I specifically mentioned that a custom deployment platform must be set to iOS 16 or later. Normally, if a framework references symbols that only exist in the newer version than the declared one, the framework should automatically be set to be linked weakly. I have tried this with the Combine
framework, which was introduced back in iOS 13 and it works as expected.
However, for some unknown reasons, SwiftData
doesn’t follow the rule when the Swift Package has a minimum deployment version of iOS 16, as if it’s first introduced in iOS 16 instead of iOS 17.
If we change it to iOS 15 or below, like [.iOS(.v15)]
, or just simply omit the platforms
parameter (which will default to iOS 12), the SwiftData will be weakly linked, and the crash caused by dyld trying to load the SwiftData framework won’t occur.
Why “strongly link” is used for iOS 16?
After some digging, I found that even though SwiftData is marked as available starting with iOS 17, there is a struct named PersistentIdentifier
that is available as of iOS 16:
My guess is that whether a system framework is weakly linked (meaning all symbols from that framework are weakly linked) is determined by the minimum OS version supported by that framework. In other words, because there is a struct marked as available on iOS 16, the Swift Package build process decides to strongly link SwiftData.
If you know the exact process for how a framework is implicitly linked, please leave a comment—thank you!
How can we examine how a framework is linked before running the app? There is a tool on macOS to help you examine a binary or a executable.
You can use otool -L
to check the shared libraries that a binary or executable is linked against:
otool -L path_to_your_binary
When setting the dynamically linked Swift Package with platform of .iOS(.v16)
, the otool -L
output would be:
@rpath/MyLibrary.framework/MyLibrary (compatibility version 0.0.0, current version 0.0.0)
/System/Library/Frameworks/SwiftData.framework/SwiftData (compatibility version 0.0.0, current version 0.0.0)
// ...
while setting the custom platform to .iOS(.v15)
will have the following output:
@rpath/MyLibrary.framework/MyLibrary (compatibility version 0.0.0, current version 0.0.0)
/System/Library/Frameworks/SwiftData.framework/SwiftData (compatibility version 0.0.0, current version 0.0.0, weak)
// ...
By comparing those outputs, the main difference is that the latter has the word weak
at the end of the SwiftData.framework
line, indicating that this framework is linked against weakly.
Here is a one solution you can use to address this issue:
Use
.iOS(.v15)
or lower version as minimum supported platform, if applicable.
It’s still like a workaround. And more luckily, we have another robust option to use: linkerSettings.
You can specify unsafeFlags
in the Swift Package’s linkerSettings
to explicitly mark SwiftData as weakly linked.
The syntax of this flag follows this archived documentation: The -weak_framework
option tells the linker to weakly link all symbols in the named framework.
targets: [
.target(
name: "MyLibrary",
linkerSettings: [.unsafeFlags(["-weak_framework", "SwiftData"])]
),
]
With this linkerSettings
set, when you examine how SwiftData is linked using the otool -L
command, it will now be weakly linked as expected.
@rpath/MyLibrary.framework/MyLibrary (compatibility version 0.0.0, current version 0.0.0)
/System/Library/Frameworks/SwiftData.framework/SwiftData (compatibility version 0.0.0, current version 0.0.0, weak)
// ...
A sample project
I created a sample project to help you experiment with the settings mentioned above. Please feel free to check it out:
https://github.com/JuniperPhoton/SwiftDataWeaklyLinkIssue
Links
Here are some links I referred to while addressing this issue and writing this blog.
Why Swift Package Manager does not support weak linking (-weak_framework SwiftUI)