bielikb / xcframeworks

Demonstration of creating and integrating xcframeworks and their co-op with static libraries and Swift packages
MIT License
539 stars 46 forks source link
binary catalyst create fastlane-plugin macos module package stability swift swift-interface swift-packages xcframework xcode

xcframeworks repo

This is a demonstration of creating and integrating the xcframeworks and their co-op with static libraries and Swift packages within the same Xcode project.

Table of contents

Pre-requisities

Changelog

What's new Xcode version Swift version Description
Module stable binaries Xcode 11 Swift 5.1 Library evolution allows the library authors to distribute module stable Swift binaries
New version of .swiftinterface interface Xcode 11.4 Swift 5.2 New annotations added to Swift @_inheritsConvenienceInitializers, @_hasMissingDesignatedInitializers. ⚠️ Module interfaces aren't backwards compatible, clients using Swift 5.1.3 and below won't be able to compile Swift binaries compiled with Swift 5.2. More info here
Support for binary dependencies in SwiftPM Xcode 12.0 Swift 5.3 Swift Package Manager now supports declaring binary targets in Package.swift
Debug symbols Xcode 12.0 Swift 5.3 Debug symbols (dSYMs, BCSymbolMaps) can be included within the xcframework through new -debug-symbols <absolute path> flag

Introduction: New .xcframework format

Requirements

Motivation & consequences

Contents of xcframework

This format bundles module-stable frameworks (.swiftinterface) for the platforms of interest.

The Info.plist contains all available frameworks in a bundle specified by library identifiers. This information is used by Xcode during the linking time => xcodebuild picks the right framework for the platform we're building against. Since Xcode 12.0 the xcframework can contain also debug symbols (dSYMs, BCSymbolMaps).

The structure of xcframework looks as shown below

xcframework

Size of xcframework

During my tests I realized, the size of an xcframework was smaller than the size of an corresponding fat framework. I tested swift only & mixed frameworks. Generally the lipo commandline tool adds a bit of overhead for all contained architectures.

Platforms

xcframework supports all Apple platforms & their variants - iOS, maccatalyst, macOS, tvOS, watchOS, iPadOS, carPlayOS.

List of destinations

Platform Destination
iOS generic/platform=iOS
iOS Simulator generic/platform=iOS Simulator
maccatalyst generic/platform=macOS,variant=Mac Catalyst
iPadOS generic/platform=iPadOS
iPadOS Simulator generic/platform=iPadOS Simulator
macOS generic/platform=macOS
tvOS generic/platform=tvOS
watchOS generic/platform=watchOS
watchOS Simulator generic/platform=watchOS Simulator
carPlayOS generic/platform=carPlayOS
carPlayOS Simulator generic/platform=carPlayOS Simulator

How to create .xcframework that contain iOS + iOS Simulator platforms

This section describes the process of creating the xcframework by archiving & creating the final xcframeworks from 2 archives built for iOS & iOS Simulator.

However, if you're not interested in the details of the process of how the xcframework is created, head directly to section: Create xcframework using fastlane plugin.

1. Archive your scheme for desired platforms (destinations)

1.1 Pass SKIP_INSTALL=NO && BUILD_LIBRARY_FOR_DISTRIBUTION=YES to archive your scheme

xcodebuild archive \
-workspace MyWorkspace.xcworkspace \
-scheme MyScheme \
-destination "generic/platform=iOS" \
-archivePath "archives/MyScheme-iOS" \
SKIP_INSTALL=NO \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES

1.2 iOS Simulator - archive your scheme for iOS Simulator platform by specifying correct destination destination="generic/platform=iOS Simulator" & point archivePath to architecture specific path, e.g. archives/MyScheme-iOS-Simulator.

xcodebuild archive \
..
-destination "generic/platform=iOS Simulator" \
-archivePath "archives/MyScheme-iOS-Simulator" \
..

1.3 iOS - archive your scheme for iOS by specifying destination="generic/platform=iOS" & point archivePath to device specific path. The architecture specific path will ensure the archive from step 2. wont be overwritten, e.g. MyScheme-iOS

xcodebuild archive \
..
-destination "generic/platform=iOS" \
-archivePath "archives/MyScheme-iOS" \
..

Locations

Binaries in .xcarchive are located under:

2. Create .xcframework from built archives

xcodebuild allows you to create xcframework by specifying frameworks, libraries or even can add headers to the libraries. -create-xcframework

1. Specify all frameworks or libraries that you want to add into .xcframework
2. Specify the outpath path using -output argument. Don't forget to add .xcframework extension to your output path.
xcodebuild -create-xcframework \
           -framework My-iOS.framework \
           -debug-symbols <absolute path to dSYM or BCSymbolMaps folder in the xcarchive> # available from XCode 12.0+
           -framework My-iOS_Simulator.framework \
           -debug-symbols <absolute path to dSYM or BCSymbolMaps folder in the xcarchive> # available from XCode 12.0+
           -output My.xcframework

Module stability is gained with Xcode 11 + Swift 5.1, once your module declares .swiftinterface file, that describes the public interface of your framework along with linker flags, used toolchain and other info. Swift interface can be found under your framework's swiftmodule folder. .swiftinterface file is autogenerated when xcframework is created.

swift-interface

Create xcframework using fastlane plugin

This plugin allows you to generate the xcframework (including all dSYMs & BCSymbolMaps) by specifying the desired destinations. E.g. destination [iOS] will generate xcframework that contains slices for both iOS & iOS Simulator. ⚠️ Currently the plugin doesn't support static libraries.

  1. Add plugin to your project

    fastlane add_plugin create_xcframework
  2. Add lane to your Fastfile

    desc "Export xcframework"
    lane :export_xcframework do
    create_xcframework(
    workspace: 'path/to/your.xcworkspace',
    scheme: 'name of your scheme',
    destinations: ['iOS', 'maccatalyst'],
    xcframework_output_directory: 'Products/xcframeworks'
    )
    end

NOTE:

Version 1.1.0 of the fastlane plugin includes support for debug symbols.

You can try out the plugin in this project by calling following command:

bundle exec fastlane export_xcframework

Testing & Troubleshooting

Make sure to always build & run your generated xcframework before distributing it to your clients. Few of the problems will unveil just at the compile or run time, so don't rely solely on the success of the xcframework creation.

Here's the list of compiler errors I got across when integrating built xcframework into Xcode project.

Problem Severity Description Solution
Redundant conformance of x to NSObjectProtocol error - thrown at dynamic linking time Your class is already subclassed from NSObject, which conforms to NSObjectProtocol Check protocol conformances of your classes and remove redundant conformance to NSObjectProtocol
Use of unimplemented initializer 'init()' for class error - thrown at dynamic linking time Objective-C ABI public classes need to provide public init Provide public init override for your public class: override public init()
@objc' class method in extension of subclass of Class X requires iOS 13.0.0 error Rules for interoperability with Objective-C has changed since iOS 13.0.0. and currently doesn't support @objc interoperability in class extensions. There's open question on Swift forums Move/Remove @objc declaration from your Swift class extension
scoped imports are not yet supported in module interfaces warning Read more about Swift import declarations here: https://nshipster.com/import/ Import the module instead of specific declaration. For example: change import class MyModule.MyClass to import MyModule
Can’t use framework compiled with Swift 5.2 in Swift 5.1.3 project error - thrown at linking time The xcframework was generated using the Swift 5.2 and above. Module stable interfaces are not backwards-compatible. Update your Xcode to Xcode 11.4 and above or generate module stable binary using Xcode 11.3 and below
Incompatible module error - thrown at linking time The module built for iOS Simulator shares the same arch slice as the new M1. Exclude arm64 slice when building your xcframework by specifying following build setting: EXCLUDED_ARCHS[sdk=iphonesimulator*] = arm64

Distribution of xcframeworks

Distribution of xcframeworks using the native dependency manager Swift Package Manager or the 3rd party dependency managers does come with several reported integration issues. Manual integration seems sofar to be the safest/bullet-proof integration option.


How to integrate .xcframework in your project

  1. Drag & drop .xcframework manually into your project's target

Drag & drop xcframework

  1. Embed & sign .xcframework in your project's target

Embed & sign .xcframework


What's in XCFrameworks workspace

XCFrameworks workspace consists of:

swift-interface


Materials

Binary Frameworks in Swift

https://developer.apple.com/videos/play/wwdc2019/416/

Distribute binary frameworks as Swift packages

https://developer.apple.com/wwdc20/10147 https://developer.apple.com/documentation/swift_packages/distributing_binary_frameworks_as_swift_packages

ABI Stability & Module Stability - swift.org

https://swift.org/blog/abi-stability-and-more/

Library evolution in Swift - swift.org

https://swift.org/blog/library-evolution/

Library evolution for stable ABIs

https://github.com/apple/swift-evolution/blob/master/proposals/0260-library-evolution.md

Library evolution - Docs

https://github.com/apple/swift/blob/master/docs/LibraryEvolution.rst

Swift Unwrapped - Swift 5.1 with Doug Gregor (Library evolution, ...)

https://spec.fm/podcasts/swift-unwrapped/308610

Alexis Beingessner - How Swift Achieved Dynamic Linking Where Rust Couldn't

https://gankra.github.io/blah/swift-abi/

Alex Grebenyuk - XCFrameworks (case-study about Distribution of xcframeworks as Swift Packages for specific case)

https://kean.blog/post/xcframeworks-caveats