Home / Blog / Shipping a closed-source SDK through SPM binary targets

Shipping a closed-source SDK through SPM binary targets

A client needed a closed-source SDK. I used to deliver these as a Cocoapods vendored framework; since moving to SPM, binaryTarget gives me a much cleaner flow. Here are the setup steps and the pitfalls I hit. Producing an XCFramework A binary target expects a .xcframework. I build it in two steps: xcodebuild archive -scheme […]

A client needed a closed-source SDK. I used to deliver these as a Cocoapods vendored framework; since moving to SPM, binaryTarget gives me a much cleaner flow. Here are the setup steps and the pitfalls I hit.

Producing an XCFramework

A binary target expects a .xcframework. I build it in two steps:

xcodebuild archive 
  -scheme MySDK 
  -destination "generic/platform=iOS" 
  -archivePath build/MySDK-iOS 
  SKIP_INSTALL=NO 
  BUILD_LIBRARY_FOR_DISTRIBUTION=YES

xcodebuild archive 
  -scheme MySDK 
  -destination "generic/platform=iOS Simulator" 
  -archivePath build/MySDK-iOS-Simulator 
  SKIP_INSTALL=NO 
  BUILD_LIBRARY_FOR_DISTRIBUTION=YES

xcodebuild -create-xcframework 
  -framework build/MySDK-iOS.xcarchive/Products/Library/Frameworks/MySDK.framework 
  -framework build/MySDK-iOS-Simulator.xcarchive/Products/Library/Frameworks/MySDK.framework 
  -output build/MySDK.xcframework

BUILD_LIBRARY_FOR_DISTRIBUTION=YES is the non-negotiable one. Skip it and you lose module stability, which crashes consumers built with a different Swift version.

Package.swift: remote binary target

let package = Package(
    name: "MySDK",
    platforms: [.iOS(.v16)],
    products: [
        .library(name: "MySDK", targets: ["MySDK"])
    ],
    targets: [
        .binaryTarget(
            name: "MySDK",
            url: "https://sdk.example.com/releases/1.2.0/MySDK.xcframework.zip",
            checksum: "a3b2c1d4..."
        )
    ]
)

Get the checksum with swift package compute-checksum MySDK.xcframework.zip. A wrong checksum fails at resolve time.

The archive format matters

First time around I tried to ship a tar.gz. SPM rejected it. It only accepts .zip. The xcframework also has to be at the root of the zip, not nested in a folder:

zip -r MySDK.xcframework.zip MySDK.xcframework

Forget the -r flag and the zip only contains an empty folder, and the error message is misleading.

Local testing with a path-based binary target

Before a release I let the consumer test the SDK from a local file:

.binaryTarget(
    name: "MySDK",
    path: "./artifacts/MySDK.xcframework"
)

I drop the SDK into the consumer project and point Package.swift at the local path. No checksum required.

Versioning: the git tag is mandatory

The consumer pulls the SDK with .package(url:, from:), which needs a semver tag. On every release:

git tag 1.2.0
git push origin 1.2.0

Then I update the URL and checksum in Package.swift to match the new release. Miss the tag and the consumer pulls old code with the new binary, and you have an inconsistency.

Bitcode and strip

Bitcode is deprecated in iOS 17. Even so, SDKs built with older Xcode still ship with bitcode and fail App Store upload. Fix: add ENABLE_BITCODE=NO during archive. For symbol stripping, set STRIP_INSTALLED_PRODUCT=NO or you’ll lose crash symbolication.

Debugging on the consumer side

You can’t set breakpoints inside a framework from a binary target. For source-level debugging the consumer needs the .dSYM too. In my releases I zip them alongside and keep both in an internal S3:

MySDK.xcframework.zip
MySDK.xcframework.dSYM.zip

Privacy Manifest

Since iOS 17, a PrivacyInfo.xcprivacy needs to live inside the SDK. Bundled inside the binary target, consumer apps can aggregate it into their own privacy manifest. Leave it out and the App Store submission warns you about it.

When I skip binary targets

For an open-source library, sharing source is better: consumers can build against whichever Swift version they’re on. Binary targets earn their keep only for closed source or license-restricted cases. For very large SDKs, Carthage or a vendored framework flow is still viable, but SPM causes less pain in CI.

Final advice

Before you publish the SDK, spin up a separate test project and write a small app that consumes it through SPM. Most integration problems only surface from the consumer’s perspective.

Have a project on this topic?

Leave a brief summary — I’ll get back to you within 24 hours.

Get in touch