Distributing iOS Libraries as Swift Packages for Development
As a developer, you know the importance of sharing your libraries, code, and dependencies with other apps and development teams. XCFramework makes it easier to do just that by bundling everything into one package. The XCFramework includes compiled binaries and header files that you can use to integrate your code into other projects.
Building Your Library in Release or Debug Mode
When building your library, you can choose between Release and Debug modes. The Release mode is for performance optimization. The compiler removes debug information and minimizes the binary size, making the code faster and more efficient. This is the version that you typically distribute to end-users or customers for production use.
The Debug mode, on the other hand, is for debugging purposes. It includes additional information such as symbols and line numbers, making it easier for debugging tools to provide better error messages and help you quickly diagnose and fix issues. This version of the library is only used by developers during the development process and is slower and larger than the Release version.
Flutter and XCFramework Optimization
When creating XCFrameworks from Flutter, an extra optimization step takes place. The Dart VM architecture changes based on the mode, with the Debug version only running in the Simulator or during a debugging session in Xcode with a device plugged in, and the Release version only working on the device that is not connected to a debugging session.
Using XCFramework in Xcode and Swift Package
Using XCFramework in an Xcode project is simple. You can set up the project to use the Release or Debug version of the XCFramework depending on the target. However, when using the XCFramework as a binary dependency in a Swift Package, things can get complicated.
The recommended Flutter setup in the add-to-app docs is for developers to choose which binary to use depending on the use case. This allows them to use the Debug version during development and switch to the Release version for distribution. However, this setup won't work for library distribution as both versions need to be bundled for the developer to switch between Release and Debug.
Different Approaches to Solve the Challenge
Our team tried two different approaches to solve this challenge.
Our first attempt was to rename the Debug versions of the Flutter and App XCFrameworks to Flutter_debug
and App_debug
, and load both as different binary targets. However, this approach failed because even though the files had different names, the targets bundled with them had the same name, causing a naming collision in the app binary.
Our second attempt was to use different targets for the Release and Debug versions, including only the ones we wanted to use in each scenario. We created two library targets: TikiSdk
and TikiSdkDebug
, and three target dependencies: TikiSdkSource
, with the Swift source files, TikiSdkReleaseBin
with the Release binaries, and TikiSdkDebugBin
with the Debug binaries. Both libraries depended on TikiSdkSource
, and each one depended on the corresponding binaries.
While this setup worked for SPM compilation, it caused issues in a project since the imports had to follow the library names. This was inconvenient for library users as they would have to rename all imports in their project depending on the version of the library.
The Solution: Different Dependency Sources
To overcome this challenge, we came up with an idea: what if the library user just changes the dependencies when releasing the app? With this idea, we can keep two different git origins, one for development and another for release. The developer would use the development origin all the time and switch to the release origin just when releasing the app.
The drawback with this idea is that we would end up with two repositories that should be always in sync. To overcome this, our pipeline replaces the debug version of the frameworks with the release version and creates a tag and a release. This way, developers simply need to switch from the main version to the tag version before releasing.
Now we have two versions of the TIKI SDK iOS that you can use. The version in the main branch is always the development version, which you can use during development. And the version in the latest tag, identified as a Release in Github, is the release version used to compile the code for distribution.
No code changes, no hacky fixes. Just set up the main branch dependency in Debug mode and use the latest tag in Release mode.