In this article we will explain how we use Swift Package Manager to distribute the Fleksy keyboard SDK, as well as touch on the difficulties we found in doing so.
You will learn:
- How to distribute a binary framework using Swift Package Manager.
- A workaround to have binary frameworks depend on other Swift Packages.
- How to use a Swift Package consisting of a binary securely stored behind basic authentication.
But before we begin.
At Fleksy, we are proud of our Keyboard SDK because it allows anyone to build a fully featured and highly customizable keyboard. Perhaps the most valued feature of our SDK is that it provides invaluable Data about the user’s input to B2B clients, which has already proven to be life-changing in the healthcare industry.
Indeed, the Fleksy Keyboard SDK is very easy to integrate into a project and we are constantly evolving, and improving functionalities and features.
Are you curious to know how we distribute the Fleksy iOS SDK to our clients?
Why Swift Package Manager?
Our Fleksy SDK for iOS consists of a number of binaries included in XCFramework bundles. The integration engineer simply needs to link an App against the XCFrameworks in order to start using the SDK.
Whereas linking our XCFrameworks manually can be an option, it clearly has the following downsides:
- The whole process is manual, which is in itself undesirable: downloading the XCFrameworks and linking them to the target(s).
- The XCFramework files need to be added to the repository, which means more space used and slower Git checkouts.
- The integration engineer must be aware of the versions of the XCFrameworks that they are linking against.
In our case, this has the added difficulty of making sure that the multiple XCFrameworks have compatible versions.
The above points highlight why using a dependency manager is a must – should we need to include external dependencies in our codebase. Focusing on the Apple developer ecosystem, for a long time we have had to “make do” with third-party dependency managers: CocoaPods and Carthage. These solutions provide a great way to manage the dependencies in our Xcode projects. However, there have always been some downsides to them:
- Not integrated with Xcode.
- Cocoapods relies on a bunch of hacks, seen as magic by many developers. It ends up being very intrusive and leaving its traces all over the Xcode project. It also sometimes generates errors that are not obvious to fix.
- Carthage setup process is too manual and not widely supported.
When the Swift Package Manager was announced back in 2018 at the Apple WWDC, it lacked basic features that made it cumbersome for many developers. But after several years, Apple and the Swift Community have continuously improved, making the Swift Package Manager the most reliable, integrated and easy-to-use dependency manager for the Apple Platforms. Only now, after a few years since its release, we can confidently say that the Swift Package Manager is mature enough to be a viable alternative.
I can hear many say that the Swift Package Manager is far from perfect, and it still has some limitations. For example, the binary targets cannot declare dependencies in Swift packages…
Nevertheless, it is the only first-class option in the Apple developer ecosystem. It is also widely supported by most libraries. For example, the omnipresent Firebase SDK for iOS recently added Swift Package Manager support.
This is one of the reasons we decided to support Swift Package Manager distribution in our Fleksy SDK for iOS.
Distributing the Fleksy SDK as a binary Swift Package
Binary frameworks can be distributed as Swift Packages. To do that, we simply need to define a binaryTarget and expose it as a product in the Swift Package. This would be our Swift Package manifest file Package.swift:
// swift-tools-version:5.5 import PackageDescription let package = Package( name: "FleksyKeyboardSDK", platforms: [ .iOS(.v10) ], products: [ .library( name: "FleksyKeyboardSDK", targets: ["FleksyKeyboardSDK"]), ], dependencies: [ ], targets: [ .binaryTarget( name: "FleksyKeyboardSDK", url: “https://url/to/remote/FleksyKeyboardSDK.xcframework.zip", checksum: "954f92b2894168cd2e6f04010caa41aa894f337a9a01a7b1f7720a73845e274e" ) ] )
For the above, we need to have the XCFramework of our FleksyKeyboardSDK compressed as a zip file. We then need to have it stored in a remote host, accessible to our clients. Also, note the checksum field of the binaryTarget, which contains a hash of the XCFramework. When our clients add the package to their project, Xcode uses the checksum to validate the XCFramework it downloads.
In addition, our FleksyKeyboardSDK is built on top of a core codebase that serves as the “brain” for our keyboard technology. We call this the “Core Engine”. It basically consists of shared code between our iOS and Android SDK.
In the case of iOS, our clients also need to link their app against the Engine XCFramework because, in fact, our FleksyKeyboardSDK depends on it. It goes without saying that we also want to distribute the Engine via Swift Package Manager.
Actually, this is the first issue we faced when adding a Swift Package Manager support to the Fleksy Keyboard SDK. At the time of writing this article, we have the following limitations:
- Nested frameworks are not supported on iOS. In our case, this means that we cannot distribute our FleksyKeyboardSDK in a way that it also embeds the Engine XCFramework.
- Binary frameworks cannot depend on Swift Packages. Therefore, the binary in our FleksyKeyboardSDK Swift Package cannot depend on the Engine Swift Package.
So, how can we work around this limitation you might ask?
In short, since we cannot make the FleksyKeyboardSDK Swift Package depend on the Engine Swift Package, we need our clients to link their apps against the two packages.
A first approach could have been to have clients add the two packages. However, we immediately discarded that option because:
- We don’t want our clients adding the Keyboard SDK package and forget adding the Engine dependency because the app will crash at run time due to missing symbols used by the SDK.
- We don’t want our clients to manage the versions with two dependencies.
In other words, the Fleksy SDK needs to be easily integrated by any client.
The last thing you want is a client adding a version of the dependencies that is not compatible with each other! Avoiding these kinds of issues is why we decided to use Swift Package Manager in the first place!
Workaround to have sub-dependencies in binary targets
The following second approach is the one we finally implemented.
Binary targets cannot define Swift Package dependencies. But regular targets can. So the solution is to have a “dummy” empty target that declares both the Fleksy Keyboard SDK and the Engine as dependencies.
// swift-tools-version:5.5 import PackageDescription let package = Package( name: "FleksySDK", platforms: [ .iOS(.v10) ], products: [ .library( name: "FleksySDK", targets: ["FleksySDK"]), ], dependencies: [ .package(name: "FleksyEngine", url: “git@url/to/FleksyEngineGitRepository.git”, .exact(“3.5.4")), .package(name: "FleksyKeyboardSDK", url: “git@url/to/FleksyKeyboardSDK.git”, .exact(“4.0.0”)) ], targets: [ .target( name: "FleksySDK", dependencies: ["FleksyEngine", "FleksyKeyboardSDK"] ), ] )
For this workaround to work, we need to commit at least one source file on the dummy package repository. It is enough to have an empty source file as the image below illustrates:
You can notice how this solution allows us to specify the exact version of the dependencies. In the example above, we require version 3.5.4 of the Engine. This is the required version by the version 4.0.0 of the FleksyKeyboardSDK. As a consequence, any developer that uses the FleksySDK Swift Package does not have to worry about the version compatibility between dependencies in our SDK.
Thanks to this approach, our clients only have to add the FleksySDK Swift Package to their project. Even if being a workaround, it did meet the minimum requirements we wanted to distribute our SDK.
Bonus: limit access to a binary Swift Package
In order to protect Fleksy’s intellectual property, we needed to provide limited access to our iOS SDK binaries, in such a way that only our clients are able to use our SDK. Here is why we accomplished this with the Swift Package Manager.
Perhaps the most straightforward approach would be to commit the binaries of the Swift Packages in the same git repository as the Package itself. Then, by restricting the access to that git repository to our clients only, we would achieve our goal. However, this presents a couple of issues:
- The Git repository would get overloaded, which creates bigger and slower checkouts.
- Managing access to a git repository for a big amount of users is not very scalable for us. The binaries of the Swift Packages don’t need to be stored locally. They also don’t need to be included in the package’s Git repository. That can be hosted on a separate server.
Swift Package Manager takes care of downloading and validating the zip archive of the binary, as we mentioned above. In fact, the binaries hosted on a server do not need to be publicly available. Swift Package Manager supports basic authentication for packages whose binaries are stored in a private server.
Assuming that the server that hosts the ZIP of the binary supports basic authentication (username + password), then, in order to import a binary Swift Package of this sort:
- If it does not exist already, create a .netrc file in the home directory of the computer where the dependencies will be downloaded (
- Add the login credentials to .netrc file for the host that contains the binaries.
The .netrc file should look something similar to this:
machine remote_host_name.com login username password password_for_remote_host
Sometimes, Swift Package Manager cannot resolve such a dependency right after adding the .netrc file. This is usually due to caching and it often gets resolved by applying Xcode 101’s “close Xcode and delete the DerivedData folder”. If not, CS 101’s “turning the computer off and on again” does the trick.
If the provided credentials are valid, the Swift Package should get resolved successfully.
Obviously, the Swift Package Manager still has limitations. Nevertheless, as we have seen above, some workarounds are possible. Moreover, it also offers some advanced features such as supporting basic authentication for binaries hosted remotely.
CocoaPods and Carthage have filled the dependency management gap and the Apple developer Community should be grateful of them. In fact, both solutions have served as models for the development of the Swift Package Manager. Over time, the official solution provided by Apple has grown greatly, which certainly makes its alternatives become less and less relevant.
Here at Fleksy, we aim to deliver the best possible service solutions for our clients. In terms of distributing an iOS library, we think the best, most straightforward and official solution is the Swift Package Manager.
As you read in this article, thanks to the Swift Package Manager, the Fleksy SDK for iOS can be very easily integrated into any project and help you create unimaginable products.
Enjoyed this article? We’d love to receive your feedback and we can’t wait to see what you build on top of the Fleksy iOS SDK!
Did you find this article valuable?
Support Fleksy's Engineering Blog by becoming a sponsor. Any amount is appreciated!