Creating Swift Package with binaryTarget that has dependencies

How can you distribute an XCFramework via Swift Package Manager when it has dependencies on other Swift packages? We accomplished this with CocoaPods in order to distribute our closed source SDK that has dependencies, but need to migrate to SPM. Note none of the types from the dependencies are used as part of our module’s public interface - usage is purely internal.

I’ve made a lot of progress following these steps for a simple example:

Create Framework Project

  1. Create a new iOS Framework project in Xcode and name it WallpaperKit
  2. In the project settings select the target and verify in Build Settings that Build Libraries for Distribution is Yes then set Skip Install to No
  3. Create a new UIViewController subclass, name it WallpaperPreviewViewController, make it public, and add some functionality to it to show a UIImageView
  4. Add a new Package Dependency in the project settings, for this example we’ll use https://github.com/onevcat/Kingfisher, and specify exact version 8.5.0
  5. Add internal import Kingfisher and use it in WallpaperPreviewViewController to download and show an image from the web
  6. Close the WallpaperKit project

Create Hosting App Project (this makes it easier to develop the framework functionality and test it in an app with Xcode building both in the same workspace)

  1. Create a new iOS app project and name it WallpaperApp
  2. Create a new workspace named WallpaperApp
  3. Close the WallpaperApp project
  4. Drag and drop WallpaperApp.xcodeproj into the workspace’s sidebar
  5. Drag and drop WallpaperKit.xcodeproj into the workspace’s sidebar
  6. Switch the scheme to WallpaperKit and build
  7. Select WallpaperApp project, then with WallpaperApp target selected, in the General tab under Frameworks, Libraries, and Embedded Content, click + and add WallpaperKit.framework
  8. In ViewController.swift, import WallpaperKit and add functionality to present an instance of WallpaperPreviewViewController
  9. Run the app and verify it works

Create XCFramework

  1. In Terminal, cd into WallpaperKit and run xcodebuild archive -scheme WallpaperKit -configuration Release -destination 'generic/platform=iOS' -archivePath './build/WallpaperKit.framework-iphoneos.xcarchive' SKIP_INSTALL=NO BUILD_LIBRARIES_FOR_DISTRIBUTION=YES DEFINES_MODULE=YES
  2. Run xcodebuild archive -scheme WallpaperKit -configuration Release -destination 'generic/platform=iOS Simulator' -archivePath './build/WallpaperKit.framework-iphonesimulator.xcarchive' SKIP_INSTALL=NO BUILD_LIBRARIES_FOR_DISTRIBUTION=YES DEFINES_MODULE=YES
  3. Run xcodebuild -create-xcframework -framework './build/WallpaperKit.framework-iphonesimulator.xcarchive/Products/Library/Frameworks/WallpaperKit.framework' -framework './build/WallpaperKit.framework-iphoneos.xcarchive/Products/Library/Frameworks/WallpaperKit.framework' -output './build/WallpaperKit.xcframework'
  4. Open the build folder and retrieve the XCFramework

Create Swift Package

  1. Create a new package in Xcode, select Library, and name it WallpaperKitDist
  2. Drag and drop WallpaperKit.xcframework into Sources
  3. Create a new directory in Sources called WallpaperKitDependencies
  4. Create a new Swift file in WallpaperKitDependencies called WallpaperKitDependencies (SPM requires a Swift file to recognize WallpaperKitDependencies as a valid target and fetch dependencies)
  5. Open Package.swift and change it to
import PackageDescription

let package = Package(
    name: "WallpaperKit",
    platforms: [
        .iOS(.v18)
    ],
    products: [
        .library(
            name: "WallpaperKit",
            targets: ["WallpaperKit", "WallpaperKitDependencies"]
        ),
    ],
    dependencies: [
        .package(
            url: "https://github.com/onevcat/Kingfisher.git",
            exact: "8.5.0"
        )
    ],
    targets: [
        .binaryTarget(
            name: "WallpaperKit",
            path: "./Sources/WallpaperKit.xcframework"
        ),
        .target(
            name: "WallpaperKitDependencies",
            dependencies: [
                "Kingfisher"
            ],
            path: "./Sources/WallpaperKitDependencies"
        )
    ]
)

Create Test App (to simulate a third-party app using the package)

  1. Create a new iOS app project and name it TestApp
  2. Add a new Local package selecting the WallpaperKitDist directory that contains Package.swift
  3. Import WallpaperKit and use it to present a WallpaperPreviewViewController

This works! Though the console logs objc[39953]: Class _TtC10KingfisherP33_6AA794C9C370CDB07604B4D8B99AEAA312BundleFinder is implemented in both /Users/Name/Library/Developer/Xcode/DerivedData/TestApp-capvhjiqxrdgdnbevpkajicnjpcs/Build/Products/Debug-iphonesimulator/WallpaperKit.framework/WallpaperKit (0x100e8bbf8) and /Users/Name/Library/Developer/CoreSimulator/Devices/E0AF13C2-874C-47B9-B864-72AF3E4D5D4B/data/Containers/Bundle/Application/AF32011A-92E7-4E26-9A97-9F0C25C07863/TestApp.app/TestApp.debug.dylib (0x101a543b0). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.

I thought using internal import Kingfisher (or @_implementationOnly import Kingfisher) would have resolved this, but seems to make no difference compared to just import Kingfisher. I suspect it might not be an issue as long as the Kingfisher version number specified in the distribution Package.swift matches the version used in the framework project (and the app does not add a different version as a dependency), but not positive.

Can these warnings be resolved, or is it not a concern in this setup? Is this the best solution to distribute an XCFramework via Swift Package Manager that has dependencies on other Swift packages for now or is there a better approach? Thanks!

Let's examine this log:

objc[39953]: Class _TtC10KingfisherP33_6AA794C9C370CDB07604B4D8B99AEAA312BundleFinder is implemented in both /Users/Name/Library/Developer/Xcode/DerivedData/TestApp-capvhjiqxrdgdnbevpkajicnjpcs/Build/Products/Debug-iphonesimulator/WallpaperKit.framework/WallpaperKit (0x100e8bbf8) and /Users/Name/Library/Developer/CoreSimulator/Devices/E0AF13C2-874C-47B9-B864-72AF3E4D5D4B/data/Containers/Bundle/Application/AF32011A-92E7-4E26-9A97-9F0C25C07863/TestApp.app/TestApp.debug.dylib (0x101a543b0). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.

You have mangled Swift class names there, and they exist in two places, so the system doesn't know which implementation to use. While the crux of your question is "Why are these symbols still given an _implementationOnly import", there's something subtle here — this log is being generated by the Objective-C runtime, for a Swift class — notice the system logging this is named objc at the front of that line.

Your package import statements (_implementationOnly or more recently, private import SomePackage via SE-0409) only manage the symbol names in the exported symbol list for the library — those symbols are still present in the binary, and required by the Objective-C runtime. Swift classes are also Objective-C classes at the runtime level, even if the classes do not inherit from NSObject or contain @objc modifiers — this is part of how you’re able to write Swift code that interoperates with Objective-C code. As a result, even if symbols are not exported out of the library, their full mangled name must be registered with the runtime when the code is loaded, and this error message about the collision is the result.

Since you're using a fairly well-known package for your example here, there are two paths that I'd look at, in ranked order here:

  • Reduce your dependencies as far as possible
  • Declare in your package manifest for your vended XCFramework a dependency on the popular package.

Reducing your dependencies as much as possible is the best solution, if your needs can be met by directly using Apple's SDK. I consider this to be the best answer because it eliminates all variations of this issue entirely, which I'll detail below so you can see them. I don't wish to pick on your example of a popular package here, as I know many developers derive value from it, as you also clearly do — any other popular package used by both your library and the app using your library would produce the same message. There is a question here that you can answer for yourself and your library — is the value offered by this dependency to my library substantial enough to continue using it, over having my own implementation of the same functionality directly using Apple's classes? There's no prescriptive answer there — sometimes the answer is yes, the value proposition is high and so you need to keep the dependency, and sometimes, it's more of a convenience than a necessity, and so you can let go of the dependency.

If you do want to keep the dependency for your XCFramework library, one thing you can sometimes do is set up a package to vend your XCFramework, and declare a dependency in that package so that consumers of the XCFramework also get a resolved version of the library dependency. The build of your framework would need to be configured so that this dependency doesn't wind up built in to your library binary. This way, the build of the client app provides a resolved version of the dependency.

However, this path has drawbacks when deployed. Let's say you depend on a library version 2.1. If the app, which also depends on the same library version 2.1, everything's good because the app will wind up with one copy of the dependent framework, let's say version 2.1.3, that both the app and your library share.

Let me introduce a variation however — you continue to depend on version 2.1 in my example, but the app now depends on 2.2. If your library can consume anything from 2.1, everything's still good, because your library and the app can both use version 2.2. But if your library requires you stay exactly on a version like 2.1.3, you now have competing requirements for dissimilar versions of the dependent library, which would then be a source of difficult to reason about bugs. Now imagine a major version difference like 3.0 instead of a minor version like 2.1 to 2.2 — the same problem here has many different constructions.

Let me add one other dimension to this — fourth party libraries. Perhaps an app using your library doesn't depend on the same library you do, so there's no conflict with the app, but you're in version conflict with this other library. That's the same scenario as above, except that the app may not even realize this. Or, let's say the app, your library, and some fourth-party library all depend on the same library, but each of you has entirely separate version requirements.

The take away I want you to have from those examples from simple version conflict through much more complex conflicts is that there are many different ways that a client of your XCFramework could wind up with these messages. That's why my first and preferred recommendation is to reduce your dependencies as much as you can first, because you eliminate all of the variations on this problem that I laid out.

To reiterate something I said above, the use of a popular dependency in wide deployment is what makes the problems I describe acute. For uncommon dependencies, the risk of a runtime collision like this is still always present, but much less likely to happen.

— Ed Ford,  DTS Engineer

Thanks Ed! I appreciate the goal of reducing dependencies as much as possible. In this simple example to outline the approach in detail, I chose Kingfisher. Our real SDK is much more complicated, it utilizes a couple dependencies that would be very difficult to build ourselves.

For our use case, it is acknowledged that if the app itself adds a dependency that our SDK relies on, it should be the same version number or an otherwise compatible version. Our dependencies are not super popular so that is fairly rare, though it does happen. And our SDK is not widely integrated in apps either, it happens to be quite niche. Note our SDK is already in use distributed via CocoaPods and we haven't had an issue thus far, we are now trying to understand how to distribute it via SPM instead.

I'm interested to understand why I see the duplicate class warnings with the example I put together, given the app itself did not add Kingfisher as a dependency, its only dependency is the sample WallpaperKitDist SDK. Perhaps because I didn't configure the framework or package correctly so Kingfisher unintentionally got included in the framework and the app? Which is a good segue to your next comment.

If you do want to keep the dependency for your XCFramework library, one thing you can sometimes do is set up a package to vend your XCFramework, and declare a dependency in that package so that consumers of the XCFramework also get a resolved version of the library dependency. The build of your framework would need to be configured so that this dependency doesn't wind up built in to your library binary. This way, the build of the client app provides a resolved version of the dependency.

This sounds perfect. Given the steps outlined in my post to create an example framework, what do I need to do differently? Thanks!

If you do want to keep the dependency for your XCFramework library, one thing you can sometimes do is set up a package to vend your XCFramework, and declare a dependency in that package so that consumers of the XCFramework also get a resolved version of the library dependency.

Given the steps outlined in my post to create an example framework, what do I need to do differently?

Ah, there's a level of nuance I had forgotten about here — my apologies for that. It's often the case where questions around libraries like yours wind up taking different paths because of exact specifics in any situation, and what I stated above works for some package dependencies, but not others.

For your configuration in a package, which is pulling in a source code-based package, this isn't possible. If you try to use a plain import statement within your framework (import SomeLibrary), you will get a complier warning that says:

module 'SomeLibrary' was not compiled with library evolution support; using it means binary compatibility for 'YourFramework' can't be guaranteed

That is what sends you down the path to using access modifiers on your import statement (private import SomeLibrary). Source code compiled through Swift Package target cannot have Library Evolution enabled, that is only possible with Xcode-based targets and use of the BUILD_LIBRARY_FOR_DISTRIBUTION build setting. The issue here is that if you export types from this imported library through your framework interface, then the binary interface or ABI (which is what Library Evolution controls) of your dependency must also be stable. That is on top of the semantics for package versioning collisions that I wrote about in my previous response.

The way you avoid this is to make the import an internal dependency only (private import SomeLibrary), but that brings us back around to the original questions you're exploring. So for this configuration, where you have a widely deployed dependency that is vended as a source code Swift package, this brings me back to my original recommendation — minimize your dependencies as much as possible, as that's the way to side-step avoid all of the complexities here.

Coming back to my prior statement:

If you do want to keep the dependency for your XCFramework library, one thing you can sometimes do is set up a package to vend your XCFramework, and declare a dependency in that package so that consumers of the XCFramework also get a resolved version of the library dependency.

The place where this is possible is if your external dependency is vended as an XCFramework, rather than as source code. Your Swift package that delivers your completed XCFramework declares a dependency to something else which is also an XCFramework.

To review why that works, it's back to the details of enabling Library Evolution. Building an XCFramework starts from an Xcode framework target, and enabling Library Evolution is also a required part of creating an XCFramework. With Library Evolution enabled, you then have an ABI-stable binary, which your framework can then link to. So long as the package-level versioning of this XCFramework aligns with the declarations used by the client app or its other frameworks (what I explained in my prior response), this is how you then share a dependency with the app. This shared dependency is a dynamic framework binary that lives independently inside the app's bundle for everyone to use, with the same API and a stable ABI that is compatible with all clients of the shared dependency inside the app.

— Ed Ford,  DTS Engineer

Creating Swift Package with binaryTarget that has dependencies
 
 
Q