Swift Binary Frameworks
Introduction
Binary frameworks represent a powerful way to distribute compiled code in the Swift ecosystem. Unlike source code packages, binary frameworks allow you to share functionality with other developers without exposing your source code, making them ideal for proprietary libraries, third-party SDKs, or any situation where you need to protect intellectual property while still providing functionality.
In this guide, we'll explore how to create, distribute, and use binary frameworks with Swift Package Manager (SPM). We'll cover XCFrameworks, which Apple introduced to solve many of the challenges of distributing binary frameworks across multiple platforms and architectures.
What Are Binary Frameworks?
A binary framework is a precompiled bundle of code that can be imported into projects. Unlike source packages where the consumer compiles the source code as part of their project, binary frameworks are already compiled and only their public interfaces are exposed.
Key advantages of binary frameworks include:
- Intellectual Property Protection: Source code remains private
- Faster Build Times: No need to recompile the framework's source code
- Version Stability: Exact same binary is used across all consuming projects
- Reduced Dependency Complexity: Implementation details are hidden
XCFrameworks: The Modern Approach
Apple introduced XCFrameworks at WWDC 2019 to solve the challenges of distributing binary frameworks across multiple platforms and architectures. An XCFramework is a bundle that can contain multiple variants of a framework or library for different platforms (iOS, macOS, tvOS, watchOS) and architectures (arm64, x86_64, etc.).
Creating a Binary Framework Step by Step
Step 1: Create a Framework Project
First, let's create a simple framework project in Xcode:
- Open Xcode and select File > New > Project
- Choose Framework as the template
- Name your framework (e.g., "MyAwesomeKit") and click Next
- Choose a location to save your project and click Create
Step 2: Write Your Framework Code
Let's create a simple utility class in our framework:
import Foundation
public class StringUtility {
public init() {}
public func reverseString(_ input: String) -> String {
return String(input.reversed())
}
public func countCharacters(_ input: String) -> Int {
return input.count
}
}
Notice the public
access modifier - this is crucial for exposing functionality to consumers of your framework.
Step 3: Build the XCFramework
To create an XCFramework that supports multiple platforms, we'll use a shell script. Create a new file named build-xcframework.sh
in your project directory:
#!/bin/bash
# Define framework name
FRAMEWORK_NAME="MyAwesomeKit"
# Define output directory
OUTPUT_DIR="./build"
# Create output directory if it doesn't exist
mkdir -p $OUTPUT_DIR
# Clean previous builds
rm -rf $OUTPUT_DIR/$FRAMEWORK_NAME.xcframework
# Build for iOS devices
xcodebuild archive \
-scheme $FRAMEWORK_NAME \
-destination "generic/platform=iOS" \
-archivePath $OUTPUT_DIR/ios.xcarchive \
-sdk iphoneos \
SKIP_INSTALL=NO \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
ENABLE_BITCODE=YES
# Build for iOS Simulator
xcodebuild archive \
-scheme $FRAMEWORK_NAME \
-destination "generic/platform=iOS Simulator" \
-archivePath $OUTPUT_DIR/ios-simulator.xcarchive \
-sdk iphonesimulator \
SKIP_INSTALL=NO \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
ENABLE_BITCODE=YES
# Build for macOS
xcodebuild archive \
-scheme $FRAMEWORK_NAME \
-destination "generic/platform=macOS" \
-archivePath $OUTPUT_DIR/macos.xcarchive \
-sdk macosx \
SKIP_INSTALL=NO \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES
# Create the XCFramework
xcodebuild -create-xcframework \
-framework $OUTPUT_DIR/ios.xcarchive/Products/Library/Frameworks/$FRAMEWORK_NAME.framework \
-framework $OUTPUT_DIR/ios-simulator.xcarchive/Products/Library/Frameworks/$FRAMEWORK_NAME.framework \
-framework $OUTPUT_DIR/macos.xcarchive/Products/Library/Frameworks/$FRAMEWORK_NAME.framework \
-output $OUTPUT_DIR/$FRAMEWORK_NAME.xcframework
echo "✅ XCFramework successfully built at $OUTPUT_DIR/$FRAMEWORK_NAME.xcframework"
Make the script executable:
chmod +x build-xcframework.sh
Run the script:
./build-xcframework.sh
This will create an XCFramework in the build
directory that supports iOS devices, iOS simulators, and macOS.
Distributing the Binary Framework via SPM
Step 1: Create a Package Repository
Create a new Git repository to host your package.
Step 2: Create a Package.swift File
Create a Package.swift
file with the following contents:
// swift-tools-version:5.3
import PackageDescription
let package = Package(
name: "MyAwesomeKit",
products: [
.library(
name: "MyAwesomeKit",
targets: ["MyAwesomeKit"]),
],
dependencies: [],
targets: [
.binaryTarget(
name: "MyAwesomeKit",
path: "MyAwesomeKit.xcframework"
)
]
)
Step 3: Copy Your XCFramework
Copy the XCFramework from your build directory to the root of your package repository.
Step 4: Commit and Push
Commit all files and push to your repository:
git add .
git commit -m "Initial release of MyAwesomeKit binary framework"
git tag 1.0.0
git push
git push --tags
Step 5: Alternatively, Use a URL Instead of Local Path
For remote distribution, you can also provide a URL instead of a local path:
.binaryTarget(
name: "MyAwesomeKit",
url: "https://example.com/archives/MyAwesomeKit-1.0.0.xcframework.zip",
checksum: "a7cb048dbb3a9a4171cc64a7e82a8a710fbf467829f362ee7db3486097f6c088"
)
To generate the checksum for your XCFramework, you can use the following command:
swift package compute-checksum path/to/MyAwesomeKit.xcframework.zip
Consuming a Binary Framework in Another Project
Via Swift Package Manager in Xcode
- In Xcode, select File > Swift Packages > Add Package Dependency
- Enter the URL of the Git repository hosting your package
- Choose the version requirements (e.g., "Up to Next Major" with "1.0.0")
- Select the target where you want to add the package and click Finish
Via Package.swift in Another Package
If you're creating another Swift package that depends on your binary framework:
// swift-tools-version:5.3
import PackageDescription
let package = Package(
name: "MyApp",
products: [
.library(
name: "MyApp",
targets: ["MyApp"]),
],
dependencies: [
.package(url: "https://github.com/username/MyAwesomeKit.git", from: "1.0.0")
],
targets: [
.target(
name: "MyApp",
dependencies: ["MyAwesomeKit"]),
]
)
Using the Framework in Code
Once imported, you can use your framework like any other Swift module:
import MyAwesomeKit
let utility = StringUtility()
let reversed = utility.reverseString("Hello, World!")
print(reversed) // Output: !dlroW ,olleH
let count = utility.countCharacters("Swift is awesome")
print(count) // Output: 16
Versioning and Updating Binary Frameworks
When you need to update your binary framework:
- Make changes to your source code
- Rebuild the XCFramework using your build script
- Update the binary in your package repository
- Commit changes and create a new tag for the new version
- Push the changes and tag to the repository
Consumers can update to the new version by updating their package dependencies.
Real-World Example: Creating an Analytics Framework
Let's look at a more practical example - creating a simple analytics framework that tracks screen views in an app.
Framework Code
import Foundation
public class AnalyticsManager {
public static let shared = AnalyticsManager()
private init() {}
public func trackScreen(name: String, properties: [String: Any]? = nil) {
var logMessage = "Screen viewed: \(name)"
if let properties = properties, !properties.isEmpty {
logMessage += " with properties: \(properties)"
}
// In a real implementation, you would send this to a server
print(logMessage)
#if DEBUG
print("[Debug] Analytics event fired at \(Date())")
#endif
}
public func setUserProperty(key: String, value: Any) {
print("User property set: \(key) = \(value)")
}
}
Consumer Usage
import AnalyticsFramework
class HomeViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Track screen view
AnalyticsManager.shared.trackScreen(
name: "Home Screen",
properties: ["source": "app_launch", "user_type": "returning"]
)
}
}
Output
Screen viewed: Home Screen with properties: ["source": "app_launch", "user_type": "returning"]
Best Practices for Binary Frameworks
-
Provide Comprehensive Documentation: Since consumers can't see your source code, thorough documentation is essential.
-
Semantic Versioning: Follow semantic versioning (MAJOR.MINOR.PATCH) to indicate compatibility-breaking changes.
-
Binary Compatibility: Be careful about making changes that break binary compatibility.
-
Include Swift Interface Files: These help provide better documentation for Swift binary frameworks.
-
Test Across All Supported Platforms: Ensure your framework works correctly on all platforms and device types it claims to support.
-
Consider Privacy & Security: Avoid collecting unnecessary data that might violate privacy regulations.
-
Size Optimization: Keep your binary as small as possible to minimize impact on app download size.
Challenges and Limitations
Binary frameworks come with some challenges:
-
Debugging Difficulty: Consumers can't step through your source code when debugging.
-
Swift Version Compatibility: Binary frameworks built with one Swift version might not work with different Swift versions.
-
Dependency Management: If your framework has dependencies, you need to handle them carefully.
-
App Store Review: Ensure your binary framework complies with App Store guidelines.
Summary
Binary frameworks provide a powerful way to distribute compiled Swift code while protecting your intellectual property. The introduction of XCFrameworks has made it much easier to support multiple platforms and architectures in a single bundle.
In this guide, we covered:
- What binary frameworks are and their advantages
- Creating an XCFramework step by step
- Distributing binary frameworks via Swift Package Manager
- Consuming binary frameworks in other projects
- Versioning and updating binary frameworks
- Real-world examples and best practices
By leveraging binary frameworks, you can create modular, reusable components for your Swift projects while maintaining control over your source code.
Additional Resources
- Apple Documentation: Distributing Binary Frameworks as Swift Packages
- WWDC 2019: Binary Frameworks in Swift
- Swift Package Manager Documentation
Exercises
- Create a simple utility XCFramework that contains string manipulation functions and distribute it using SPM.
- Create an XCFramework that works on both iOS and macOS, demonstrating platform-specific code paths.
- Create a binary framework that wraps a C library and distribute it via Swift Package Manager.
- Update an existing binary framework with a new feature and practice versioning and distribution.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)