Skip to main content

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:

  1. Open Xcode and select File > New > Project
  2. Choose Framework as the template
  3. Name your framework (e.g., "MyAwesomeKit") and click Next
  4. 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:

swift
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:

bash
#!/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:

bash
chmod +x build-xcframework.sh

Run the script:

bash
./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
// 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:

bash
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:

swift
.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:

bash
swift package compute-checksum path/to/MyAwesomeKit.xcframework.zip

Consuming a Binary Framework in Another Project

Via Swift Package Manager in Xcode

  1. In Xcode, select File > Swift Packages > Add Package Dependency
  2. Enter the URL of the Git repository hosting your package
  3. Choose the version requirements (e.g., "Up to Next Major" with "1.0.0")
  4. 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
// 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:

swift
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:

  1. Make changes to your source code
  2. Rebuild the XCFramework using your build script
  3. Update the binary in your package repository
  4. Commit changes and create a new tag for the new version
  5. 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

swift
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

swift
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

  1. Provide Comprehensive Documentation: Since consumers can't see your source code, thorough documentation is essential.

  2. Semantic Versioning: Follow semantic versioning (MAJOR.MINOR.PATCH) to indicate compatibility-breaking changes.

  3. Binary Compatibility: Be careful about making changes that break binary compatibility.

  4. Include Swift Interface Files: These help provide better documentation for Swift binary frameworks.

  5. Test Across All Supported Platforms: Ensure your framework works correctly on all platforms and device types it claims to support.

  6. Consider Privacy & Security: Avoid collecting unnecessary data that might violate privacy regulations.

  7. 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:

  1. Debugging Difficulty: Consumers can't step through your source code when debugging.

  2. Swift Version Compatibility: Binary frameworks built with one Swift version might not work with different Swift versions.

  3. Dependency Management: If your framework has dependencies, you need to handle them carefully.

  4. 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

Exercises

  1. Create a simple utility XCFramework that contains string manipulation functions and distribute it using SPM.
  2. Create an XCFramework that works on both iOS and macOS, demonstrating platform-specific code paths.
  3. Create a binary framework that wraps a C library and distribute it via Swift Package Manager.
  4. 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! :)