An iOS dependency can arrive as source code, a prebuilt binary, or a package that wraps either one. These concepts overlap, but they are not interchangeable:
For most reusable code, start with a source-based Swift package. Use an XCFramework when you need to ship a prebuilt binary, usually for closed-source SDKs, build-time reduction, or code produced by another build system.
| Format | Contains | Best fit |
|---|---|---|
| Swift package | Manifest, source targets, resources, or binary targets | Default distribution method for reusable source code |
| XCFramework | Platform and variant-specific frameworks or libraries | Distributing a binary across Apple platforms and Simulator builds |
| Framework | One platform-specific static or dynamic framework bundle | Build product inside an XCFramework or direct project dependency |
Static library (.a) |
Archive of object files | C-family binaries, usually packaged inside an XCFramework |
Dynamic library (.dylib) |
Runtime-loaded shared library | Primarily macOS; use framework bundles for dynamic linking on iOS |
A Swift package is defined by a Package.swift manifest. It organizes code into targets and exposes products to clients. A package can include:
A target defines a module. A library product exposes one or more target modules to clients.
// swift-tools-version:6.0
import PackageDescription
let package = Package(
name: "AnalyticsKit",
platforms: [.iOS(.v17), .macOS(.v14)],
products: [
.library(name: "AnalyticsKit", targets: ["AnalyticsKit"])
],
targets: [
.target(
name: "AnalyticsKit",
resources: [.process("Resources")]
),
.testTarget(
name: "AnalyticsKitTests",
dependencies: ["AnalyticsKit"]
)
]
)Use a source package when consumers can compile the code themselves. This is usually the simplest option:
A SwiftPM library product can leave the linkage type unspecified or explicitly request .static or .dynamic:
products: [
.library(name: "AnalyticsKit", targets: ["AnalyticsKit"]),
.library(name: "AnalyticsKitStatic", type: .static, targets: ["AnalyticsKit"]),
.library(name: "AnalyticsKitDynamic", type: .dynamic, targets: ["AnalyticsKit"])
]Do not force a linkage type unless you have a concrete reason. Letting the build system choose gives clients more flexibility.
SwiftPM can wrap a local or remote XCFramework in a binary target. Remote archives require an HTTPS URL and checksum:
// swift-tools-version:6.0
import PackageDescription
let package = Package(
name: "AnalyticsKit",
products: [
.library(name: "AnalyticsKit", targets: ["AnalyticsKit"])
],
targets: [
.binaryTarget(
name: "AnalyticsKit",
url: "https://example.com/AnalyticsKit.xcframework.zip",
checksum: "<sha256>"
)
]
)Generate the checksum after creating the ZIP archive:
swift package compute-checksum AnalyticsKit.xcframework.zipThe XCFramework must be at the root of the ZIP archive. For a local artifact, use .binaryTarget(name:path:) and point directly to the .xcframework.
Binary targets are only available on Apple platforms. A package can combine source and binary targets, which is useful when a small source wrapper exposes a friendlier API around a closed-source SDK.
An XCFramework (.xcframework) is a binary distribution bundle created by Xcode. It groups compatible variants without pretending that device and Simulator binaries are interchangeable.
One XCFramework can contain variants for:
Each variant can contain a static or dynamic framework. An XCFramework can also contain static libraries and headers.
A universal, or fat, binary combines architecture slices for one platform variant. For example, a Simulator static library can contain both arm64 and x86_64.
Do not use lipo to merge an iOS device binary and an iOS Simulator binary. They target different platform variants even when both use arm64. XCFrameworks keep those variants separate and let Xcode select the correct one.
For a Swift framework distributed as a binary:
BUILD_LIBRARY_FOR_DISTRIBUTION) to YES.SKIP_INSTALL) to NO.xcodebuild -create-xcframework.xcodebuild archive \
-project AnalyticsKit.xcodeproj \
-scheme AnalyticsKit \
-destination "generic/platform=iOS" \
-archivePath "archives/AnalyticsKit-iOS"
xcodebuild archive \
-project AnalyticsKit.xcodeproj \
-scheme AnalyticsKit \
-destination "generic/platform=iOS Simulator" \
-archivePath "archives/AnalyticsKit-Simulator"
xcodebuild -create-xcframework \
-archive "archives/AnalyticsKit-iOS.xcarchive" \
-framework "AnalyticsKit.framework" \
-archive "archives/AnalyticsKit-Simulator.xcarchive" \
-framework "AnalyticsKit.framework" \
-output "AnalyticsKit.xcframework"For a static C-family library, use -library and provide headers:
xcodebuild -create-xcframework \
-library "products/ios/libParser.a" \
-headers "products/ios/include" \
-library "products/simulator/libParser.a" \
-headers "products/simulator/include" \
-output "Parser.xcframework"Rebuild binary SDKs as platforms adopt new architectures. Clients can only build destinations covered by the slices included in the artifact.
Sign an XCFramework before distribution:
codesign --timestamp -s "<identity>" AnalyticsKit.xcframeworkXcode can inspect the signing identity of an XCFramework and fails the build if the signature is removed, becomes invalid, or changes unexpectedly. Treat third-party binary SDK updates as supply-chain changes: verify the signer and review release provenance.
A third-party SDK can include a PrivacyInfo.xcprivacy file that describes data collection, tracking domains, and use of required-reason APIs. Xcode combines dependency manifests into a privacy report when preparing an app for distribution.
Include a privacy manifest when your SDK uses required-reason APIs, collects data, enables an app to collect data, or contacts tracking domains. Apple also requires manifests for SDKs on its commonly used third-party SDK list. Listed SDKs distributed as binary dependencies require signatures as well.
Add PrivacyInfo.xcprivacy to the SDK target’s resources so Xcode places it correctly in framework and XCFramework products.
A framework (.framework) is a bundle for one platform-specific build product. It can contain:
Info.plistFrameworks can be dynamic or static.
A dynamic framework remains a separate binary at runtime. The app links against it and embeds it in the app bundle. This can help share code between an app and its extensions, but it adds launch-time and packaging considerations.
For iOS, iPadOS, tvOS, visionOS, and watchOS, dynamic linking requires framework bundles. Do not distribute bare .dylib files for those platforms.
A static framework contains a static archive as its main binary. The linker copies the needed code into the client executable. Starting with Xcode 15, Apple supports static frameworks that bundle resources with a static library.
For distribution, archive each supported platform variant and package the variants into an XCFramework. Do not manually place a .a file inside a .framework directory and call it a static framework.
A static library (.a) is an archive of object files. At link time, the linker copies required code into the client executable.
Static libraries are common for C and C++ dependencies. They do not bundle resources by themselves. When distributing a static library across Apple platforms, put each platform variant and its headers into an XCFramework.
Parser.xcframework/
ios-arm64/
libParser.a
Headers/
ios-arm64_x86_64-simulator/
libParser.a
Headers/Expose C-family APIs to Swift with public headers and a module map. A SwiftPM C-family target can use publicHeadersPath; a system-library target can describe an installed library with module.modulemap and pkg-config.
A dynamic library (.dylib) remains separate from the executable and is loaded at runtime. Apple operating systems ship system dynamic libraries, which allow apps to use OS-provided implementations without embedding copies.
For third-party code on iOS-family platforms, distribute dynamically linked code in a framework bundle, usually wrapped in an XCFramework. Bare third-party .dylib distribution is mainly relevant to macOS.
Source packages are compiled with the client’s toolchain. Binary Swift frameworks need additional compatibility planning.
Set BUILD_LIBRARY_FOR_DISTRIBUTION=YES when creating a Swift binary framework for distribution. Xcode generates a textual .swiftinterface and enables library evolution support so clients can import the framework from compatible Swift compiler versions.
This setting has tradeoffs:
The correct integration settings depend on linkage:
| Dependency | Link | Embed in app bundle |
|---|---|---|
| Static library | Yes | No |
| Static framework | Yes | Let Xcode handle its resource bundle behavior |
| Dynamic framework | Yes | Yes, normally Embed & Sign |
| XCFramework | Depends on the selected contained variant | Let Xcode select and integrate the matching variant |
| Source Swift package | Managed by SwiftPM and Xcode | Depends on the resolved product linkage |
Avoid embedding the same dynamic framework independently in multiple nested bundles. If an app and extension both use a dynamic framework, verify the final archive layout and signing rather than relying on Debug builds alone.
| Situation | Recommendation |
|---|---|
| Reusable app feature or open-source library | Source-based Swift package |
| Internal source code shared across apps | Private Swift package |
| Closed-source Swift or Objective-C SDK | Signed XCFramework distributed through a SwiftPM binary target |
| Cross-platform C or C++ library | Static libraries and headers packaged as an XCFramework |
| App-specific modularization | Swift package or Xcode framework target, based on build and ownership needs |
| Existing fat framework script | Replace it with per-destination archives and an XCFramework |
Bare .dylib intended for iOS |
Package dynamic code as a framework, then distribute an XCFramework |
Before publishing a binary SDK:
BUILD_LIBRARY_FOR_DISTRIBUTION for Swift binary frameworks.xcodebuild -create-xcframework.PrivacyInfo.xcprivacy when your SDK needs a privacy manifest.