THE SWIFT PACKAGE MANAGER — X — FILE
Recently I’ve released a library on Github that helps iOS, macOS, tvOS and watchOS developer in using Bluetooth LE peripherals.
I’ve decide to distribute it by using two 2 dependency managers:
* Carthage
* Swift Package Manager (SPM)
In this article I’m going to explain you my experience in integrating SPM.
PACKAGE
A package is basically a collection of file with a manifest of instruction about how to use them. The package structure is written inside a manifest file, usually called Package.swift.
In the minimum useful version of a package file you can find defined products and targets.
Targets (modules) is something that you can build.
Products are build artifacts, there are 2 types of products: libraries and executables.
Another important part are dependencies, a package can rel on other packages they could be local or hosted on a specific URL, for the moment only GitHub can be used to host private or public packages.
To have a complete documentation about Swift Package Manager you can read official Apple documentation you can also read this amazing blog post by “The Swift Dev”.
HOW TO ENABLE YOUR CURRENT FRAMEWORK XCODE PROJECT TO SUPPORT SPM?
To make your current project to support SPM is not that hard. The most important thing is that if you want to do it fast you must change the structure of your folders and how files relies on them. If you don’t do that you can still keep your current file configuration but you will have to work with the Package manifest to point targets to the right directories.
To make it simple I’ve decided to move files into default expected folders.
Create two groups one called Sources and one called Tests.
In the Sources group move your target named group and do the same for the Tests group.
As an example my target is called LittleBlueTooth and this is the group structure of the project.
Add also a README.MD file, this file must be at the same level of Sources and Tests, pay attention that it should not be a part of any Xcode target of your project.
NB: you should probably also change the path of Info.plist files (target and tests) in the build settings.
The package file can be created manually or by using command line, we will use the terminal to create it.
Open the terminal go into the root path of your project and type:
swift package init --type library ,
library because in my case I’m building a library.
Press enter. If all paths are set correctly a Package.swift file will be created.
Now we need to regenerate our .xcodeproj
file.
In the terminal type swift package generate-xcodeproj
, type enter.
Now we can open the newly create project file and as you can see the Package.swift file is present and, in the project, you will also find two new xcode targets.
USE CASE: LITTLEBLUETOOTH LIBRARY
LittleBlueTooth is a library written in swift that wraps Apple CoreBluetooth framework to make integration in your applications super easy by using the power of Combine framework.
The first challange I had to face was about obtaining a good code coverage and using bluetooth is not easy. Bluetooth is not supported by simulator and event if it was, to simulate the variety of behaviors I must have programmed a device (iOS or embedded) to replicate them.
Fortunately I’ve came accross a library that mocks CoreBluetooth main components, this library is made by Nordic Semiconductor. Nordic is a company specialized in low power SoC and their chip are widely used in the bluetooth field.
CorebluetoothMock, at the moment of writing, can be integrated with Cocoapods, Carthage and SPM. I’ve decide to use SPM.
Unfortunately it wasn’t possible to test all the behaviors of my library most of the time because mocks are protocol based and they didn’t inherit all the peripheral functionalities. To make the project compile I had to make some conditional compiling.
In Xcode set up conditional compiling for unit tests is quite simple and you have many ways to do it, in my case I’ve decide to duplicate the debug configuration, rename it in Debug-test and in build settings under OTHER SWIFT FLAG I’ve added -DTEST to the Debug-test configuration. Then in the scheme of the target under test I’ve picked the configuration Debug-test.
The code I want to be executed during tests (or not be executed) is wrapped around #if TEST #else #endif
Press command + U and it will start building and launching your unit tests.
Of course I’ve needed to reflect this also in my package manifest by adding a dependency to CoreBlueToothMock.
When I’ve launched the command swift build
it compiled but when I’ve launched the command swift test
it failed.
What is happening here is that test lauched by using the swift command read everything from the package manifest, test are launched by using one of the 2 available configuration debug or release and the -DTEST
flag is missing.
SPM and Xcode settings are completely disconnected and it makes sense, SPM supports also Linux where XCode is not available and they could not depend on each other.
There is no way to make the SPM pick this Debug-test configuration from XCode so I had to find another way. The solution was to pass the flag during the swift test command.
swift test -Xswiftc "-DTEST"
Adding this option to the swift test command I was able to achieve the conditional compile in the framework during testing.
Later I’ve also decided to do not expose this dependency to the end user (remember that I use it only to launch unit tests), thinking in the Xcode way is pretty easy. LittleBlueTooth target can be built without the dependency and all the import statement of CoreBluetoohMock could be wrapped around conditional compiling, but I still need to make unit tests work.
This can be achieved by creating another target that uses the same files as the the LittleBlueTooth target, but that compiles with the -DTEST
flag. Let’s call it LittleBlueToothForTest.
On the test bundle modify the import statement in the unit test files and the dependency with the newly created target inside the test bundle.
Been there! done that!… everything works perfectly… but what about SPM?
Now that I have a new target, still I need to launch the test command along with the -DTEST flag?
Fortunately in the recent release of SPM in the manifest you can setup flags such as our “TEST”, so my idea was basically to replicate what I did in Xcode.
I’ve modified the manifest by adding a new product and a new target, I’ve set the .testTarget
with the new LittleBlueToothForTest dependency and set the flag. Here the new manifest:
tried to run and…
error: empty directory, file must be contained in the directory with the same name of the target
Ok, no worries, there is a path method and you can redirect the SPM to the same directory of the LittleBlueTooth source files, run again and…
error: overlapping resources
By looking for help in the swift forums I’ve found the reason:
SPM is made that way, you can’t use a file that has already been used for another target… D’OH!!!! this constraint was made to avoid namespaces collisions when using C or objC libraries. In swift you can use the module name as namespace, but the SPM doesn’t seems to care even if your project is written completely in Swift.
Thanks to 2 users I’ve also got some suggestions:
* Create another manifest with just one product called LittleBlueToothForTest and use that when launching tests, this would require a script to rename the file before launching swift test
* Use symlink
Note:
- Symlink are not the same thing as Finder aliases. Aliases won’t work
- Symlink must have a relative path to the repo, by default absolute path are created and they will work only on your machine
I’ve decided for the latter… and it worked
CONCLUSION
Do not confuse Xcode build configurations and settings with SPM they are not shared resources, they are two completely indipendent and decoupled system.
SPM is all about simplicity and in simplicity relies beauty .
SPM is the future, better get your hands dirty, before it’s too late.