Swift packages offer an easy manageable way to share code and functionality. If you wish to create your own package, it’s important to make sure your code works where it's supposed to and the quality is good. Most importantly, continuous integration is a great way to ensure that. If you are interested in learning why continuous integration is important, check out this article on avoiding merge hell. Therefore, in this article I am going to show you how to setup Swift package continuous integration.
Specifically, I am going to break down the following:
What's important to realize is that Swift packages are reusable pieces of code which encapsulate a set of functionality. In the end, there are two ways to create a Swift package: via Xcode or command line.
Getting Started
Under the Xcode menu, go to File > New > Swift Package and follow the corresponding dialog. Alternatively, you can use the swift command line tool to create your package. First, create an empty directory and run the create package subcommand:
>mkdir New-Package
> swift package init
In the end, you should have:
A Source directory for your main source code
A Tests directory for your testing code
A Package.swift containing the configuration in Swift of your code
Specifying Platforms and Devices
Before distributing your Swift package, it’s important to note which operating systems or platforms your code can run on. In the past I’ve reasonably assumed a Swift package would work on watchOS or Linux for instance. Only after adding the dependency and attempting to compile, did I realize the incompatibility issue. If you use an API that is limited to certain platforms, you have two options.
Specify via Package
Firstly, you can specify an operating system and version in the Package.swift under the platforms property:
On the other hand, you may want to offer some functionality on some devices. With this in mind, you can use the attributes and API availability checks. In other words the available attribute allows you to mark certain functionality of your Swift package unavailable on certain operating systems and versions.
In addition, by doing API availability checks you can supply the correct implementation based on operating system and version. Furthermore, whenever you may need to import certain frameworks which may be required but may not be available, you can use canImport, as well.
Here's an example of extending CGFloat for all operating systems, but importing CoreGraphics, when possible:
Be that as it may, if you are planning on supporting all operating systems which support Swift then there is no need to do this. Regardless, it may be worth spending some time verifying and updating this property accordingly.
Why Continuous Integration?
Having properly setup your package and provided correct compatibility, it’s important to make sure your package works. Additionally, you may want to make sure each change to your code works correctly. Luckily, this is where Continuous Integration comes in.
Continuous integration (CI) is the software practice where small code changes are frequently tested with the rest of the code to ensure nothing breaks as it changes. In other words, with CI we can make sure each change works correctly by integrating a service with our code.
Choosing a service for Swift Package Continuous Integration
What’s important to realize is that by using a cloud service for your Swift package, you can make sure your package works everywhere. Often times, we may be have some source, artifact, or tool required to build our code. However with a cloud service, you can ensure that if your Swift package builds and tests in the cloud it will work for anyone else.
Luckily, there are some great services out there to help your Swift package especially if it’s a public GitHub repository. At the present time, unfortunately, some services only support Continuous Integration for iOS Applications rather than Swift packages such as Circle CI and Bitrise. Be that as it may, Travis-CI as well as the most recent GitHub Actions allow for Swift Package Continuous Integration.
Travis-CI
Travis-CI is one of the older Continuous Integration services out there. Most importantly for public repositories, Travis-CI is completely free. In other words, you can configure your Swift package on Travis-CI. In order to configure your repository, signup with Travis-CI by linking your GitHub account or organization. Next you need to setup your .travis.yml file.
Notably, there are three key components for setting up a Swift package in Travis-CI:
Specifying the correct operating system and Xcode version
Building and testing your code
For example, here is a simple .travis.yml file:
language:objective-cos:osxosx_image:xcode11.3script:-swift build-swift test
The first three properties setup the virtual machine which Travis-CI uses. Next, the lines under script specify the commands Travis-CI needs to run to verify the Swift package is working. In this case, since we are not building from an Xcode project or workspace, we need to specify our own custom commands. In other words, rather than using xcodebuild, the Swift command line tool will build and test our package.
While Travis-CI is the most experienced cloud CI service out there, recently GitHub has also offered its own service with GitHub Actions.
Github Actions
With the fairly recent release of GitHub Actions, you can setup Swift package continuous integration right from your repo. Most importantly GitHub Actions is completely free for public repositories. Therefore, let’s breakdown the functionality of GitHub Actions.
GitHub Actions Components - Workflows, Actions, Jobs and more
In order to setup our Swift package continuous integration in GitHub Actions, we need to understand the components. Similar to Travis-CI, GitHub uses YAML for its configuration. Specifically these YAML files are known as workflow files. For example, here is a sample with the name macOS:
name:macOSon:[push]jobs:build:runs-on:macos-lateststeps:-uses:actions/checkout@v2-name:Buildrun:swift build-name:Run testsrun:swift test
In contrast to Travis-CI, you can setup multiple of these files. Therefore as long as you store them in your repository under the .github/workflow folder, GitHub Actions will execute each of these workflow files on trigger.
Speaking of trigger, you can specify many triggers. However in the case of our Swift package, we will only be using the push trigger to activate this workflow.
Below the on trigger, we have setup one job under jobs called build. With in the build job, we have specified the runner type (i.e virtual environment) of macOS-latest. In other words, it will be using the latest version of macOS to run the series of steps.
...steps:-uses:actions/checkout@v2-name:Buildrun:swift build-name:Run testsrun:swift test
GitHub Actions - Step By Step
Below the runner type property are the series of steps specified. In this case, the first step with the single uses property checks out the code from the repository. While the following steps have a name label and a run command to execute: first building the Swift package (swift build) then running tests provided (swift test).
What’s important to note is that each needs a custom command (via run) with a name or to use a prebuilt action from the GitHub repo such as actions/checkout@v2 to pull the source code from the repository. Unfortunately many of the GitHub Actions provided on GitHub may not be compatible with macOS due to various requirements.
Luckily you should have Continuous Integration setup for your Swift package in both Travis-CI and GitHub Actions. However if you wish to support Linux with your Swift package, it is import to configure the Swift package continuous integration for a Linux environment as well.
Linux Support for Swift Package Continuous Integration
If you wish for your Swift package to be supported everywhere, it might be a good idea to support Linux. Therefore, it’s important to have your Swift package Continuous integration to extend to Linux. Thankfully both Travis-CI and GitHub Action support Linux (i.e. Ubuntu) as well as the configuration of more than one environment per repository. First, let’s understand how build matrices work in Travis-CI.
Configuring Travis-CI for Linux
With Travis-CI, we can use a build matrix to include more than one environment. In other words, Travis-CI will run each environment in the build matrix in parallel. Therefore, let’s swap the top section specifying only macOS from above with a build matrix including both macOS and Linux (i.e. Ubuntu 18.04-bionic):
Now, it will run each phase in parallel on Ubuntu as well as macOS. However we'll need to make changes to both of the these sections in order to support Linux. Most importantly, we will need to install Swift.
Therefore, we will move or create the script and before_install phases into separate bash scripts and update .travis.yml accordingly:
Next let’s create a script for before_install named before_install.sh under the Scripts folder with the following code:
#!/bin/bashif[[$TRAVIS_OS_NAME='osx']];then# install macOS prerequisteselif[[$TRAVIS_OS_NAME='linux']];then# download swift
wget https://swift.org/builds/swift-5.1.3-release/ubuntu1804/swift-5.1.3-RELEASE/swift-5.1.3-RELEASE-ubuntu18.04.tar.gz
# extract the archivetar xzf swift-5.1.3-RELEASE-ubuntu18.04.tar.gz
# include the swift command in the PATHexport PATH="${PWD}/swift-5.1.3-RELEASE-ubuntu18.04/usr/bin:$PATH"fi
Within the bash script, we check the environment variable TRAVIS_OS_NAME setup by Travis-CI. With the variable, we check whether it is osx for a macOS environment or linux for the Ubuntu environment.
Since Xcode is not available on Ubuntu, we need to manually within the bash script, download and extract Swift. Lastly we need to include the directory path of the Swift command tool within the environment PATH variable.
Correspondingly we need to make sure the PATH is updated in the Scripts/script.sh file:
#!/bin/bashif[[$TRAVIS_OS_NAME='osx']];then# What to do in macOSelif[[$TRAVIS_OS_NAME='linux']];then# What to do in Ubunutuexport PATH="${PWD}/swift-5.1.3-RELEASE-ubuntu18.04/usr/bin:$PATH"fi
swift build
swift test
Therefore the only major change within the script section of the Travis-CI configuration is including the path to the Swift command line tool.
As a result, each time a push is made to the repository, Travis-CI will run both a macOS and Ubuntu process. In effect, we have setup Swift package continuous integration for both operating systems in Travis-CI. Accordingly, let’s do the same thing for GitHub Actions.
Configuring GitHub Actions for Linux
While Travis-CI requires us to check the operating system within the build script, GitHub Actions allows us to use separate workflow files for each operating system.
name:ubuntuon:[push]jobs:build:runs-on:ubuntu-18.04steps:-uses:actions/checkout@v2-name:Download Swift 5.1.3run:wget https://swift.org/builds/swift-5.1.3-release/ubuntu1804/swift-5.1.3-RELEASE/swift-5.1.3-RELEASE-ubuntu18.04.tar.gz-name:Extract Swift 5.1.3run:tar xzf swift-5.1.3-RELEASE-ubuntu18.04.tar.gz-name:Add Pathrun:echo "::add-path::$GITHUB_WORKSPACE/swift-5.1.3-RELEASE-ubuntu18.04/usr/bin"-name:Buildrun:swift build-name:Run testsrun:swift test
In this case, each command is stored within an individual step. As can be seen, the one custom part is the method we use to add the path using GitHub Actions logging commands and variables. With echo "::add-path::$GITHUB_WORKSPACE/swift-5.1.3-RELEASE-ubuntu18.04/usr/bin", we update the PATH variable.
As a result, your Swift package continuous integration should support Linux via Travis-CI and GitHub Action. However, there are a few steps in order to support Linux when it comes to testing.
Testing, CI, and Linux
When it comes to verifying whether your package works correctly, having the right unit tests are a major component of that. Luckily, if you are using the XCTest framework and setup the CI scaffold previously described, you should be good to go. However with Linux, XCTest will not automatically provide all the correct tests in your application without some modifications. There are a few ways of doing this:
Manually update your XCTestManifests.swift and LinuxMain.swift :(
Run swift test --generate-linuxmainbefore commit on your macOS machine to let the swift command line tool update your LinuxMain.swift.
Update your CI for Linux to run swift test --enable-test-discovery which will automatically discover and run your tests.
Now that we have our unit tests working correctly, let's look into keeping tracking of code coverage.
Integrating Code Coverage
Code coverage is a good way to keep track of how well your unit tests are handling. Luckily there are some great services out there to keep track of your code coverage as you build your Swift package.
CodeCov
CodeCov.io is one such service which we can plugin into our Swift Package Continuous Integration setup. However, once you have signed up and added your public repository, it will take some configuration to make it work completely.
For instance, we must make sure that the tests, build a report, convert the report to a compatible format, then lastly upload the the report to CodeCov.io. Building the report when you run the unit tests is fairly easy. In this case, we simply need to add the flag --enable-code-coverage to our swift test for both Travis-CI and GitHub Actions; macOS and Linux:
swift test--enable-code-coverage
Next, we need to prepare the report for upload using llvm-cov. With this in mind, on macOS you can use xcrun to run llvm-cov:
First, go to your project in CodeCov.io and copy the token from the project overview.
Go to your GitHub repository settings and under Secrets add the token under the name CODECOV_TOKEN.
Under both the macOS and ubuntu workflow files, add the CODECOV_TOKEN as the environment variable under the upload step.
For example, here’s the Ubuntu workflow file which uses an environment variable for the Swift package name and maps the CODECOV_TOKEN secret to the uploader command environment variable:
Now that we have completed setting up a code coverage report with CodeCov.io, Let’s talk about ensuring good code quality.
Code Quality and CI
There are some great automated ways we can keep up code quality inside our Swift package continuous integration. However, nothing replaces a good code review. If you can, have some sort of code review process by someone else. If it’s difficult to find someone else, I suggest reviewing a Swift package’s code after some down time to ensure you can have a fresh look at your code.
Regardless, let’s look at some of the ways we can automate code quality checks.
Formatting, Beautification, and Linting
There are two command line tools I use to maintain quality code while keeping formatting consistent: SwiftFormat and SwiftLint. Fortunately, each application can be installed via Homebrew. Therefore, we can use Homebrew bundle to automate the installation of both applications inside our Swift package continuous integration. With this in mind, add a file called Brewfile to the root of repository with the following text:
brew "swiftformat"
brew "swiftlint"
Next we need to add the installation these applications in the earliest part of our CI setup. However these applications will only work on macOS and we need to verify that as well. Therefore we modify our before_install.sh script for Travis-CI with:
Lastly we need to add the following command to verify code quality after the installation:
swiftformat --lint.&& swiftlint
In short, this command will run both applications in the CI process. Specifically we run swiftformat in linter mode, since our expectation is that the developer has already formatted the code correctly before commit. Correspondingly, with swiftlint, you can use the autocorrect subcommand to fix some linter errors in your code before commit.
Again, for Travis-CI, verify the $TRAVIS_OS_NAME is osx, so the CI doesn’t run the tool in Linux.
Beside installation and execution of the tools within the Swift package continuous integration, you should probably add configuration files for both tools. In brief, these are .swiftformat and .swiftlint.yml. Futhermore, check out both the SwiftFormat and SwiftLint projects for details on how setup these configurations.
Besides SwiftFormat and SwiftLint, there are a few cloud based code quality tools which easily integrate with your public GitHub repository. Therefore, you may be interested in:
After having setup these various tools, it’s worth taking a look at some various issues which may crop up.
Common Code Quality Issues
Now that we have setup our Swift package continuous integration and plugged in several services, there are few issues you could resolve which will show up:
Cyclomatic Complexity
Cyclomatic Complexity refers to the complexity of the code specifically when it comes to testability. In the end, if your code contains several logical paths, it is best to break these up. For instance, using Protocol Oriented Programming, you can have a protocol to test the case. Then, for each case use individual implementations and store them in an array. For instance, rather than:
As a result, testing and maintaining this code becomes much more simple.
Cognitive Complexity
While Cyclomatic Complexity determines how difficult your code will be to test, Cognitive Complexity is how difficult it is to read and understand. Therefore much of this involves breaks in logic, and unnecessary loop statements. However by making your code flow clearly from top to bottom this can be avoided.
File and Function Too Long
One of the easiest metrics of complexity is the length of your files and functions. In the end, simply refactoring should resolve the issue. To put it differently, break down large functions, classes, and structs into smaller components. At the same time, you may find a class which is difficult to split up such as a UITableViewController. In this case, Swift extensions are a great way to resolve this.
Assignment Branch Condition
Assignment Branch Condition typically is calculated based on the number of assignments, branches, and conditions as opposed to simply length. Therefore, this can be resolved by separating your functions into smaller pieces. In other words, reduce these complex elements from one function into several.
Many of these code quality issues should be resolved, however it is good to not be overly concerned with some of these metrics. Similar to code coverage, it is much more a case-by-case need for resolution.
Additionally there can easily be instances of false positives such as identical code issues. In the end, it’s important to have a measured but determined approach to code quality.
Documenting Your Swift Package
If you intend to distribute your Swift package to the public then it’s important to document your code and make that documentation easily available. In the first place, take a look at Apple’s guidance on documenting Swift code as well as:
Once you’ve properly documented your Swift package API, let’s add code documentation to our Swift package continuous integration.
Primarily, you have the option of using either Jazzy by Realm or SourceDocs. Each has their advantage depending on whether you wish to build a complete static HTML site. In that case, I would recommend Jazzy. Especially considering the maturity of Jazzy, it can be an excellent tool. On the other hand, if you wish to simply output Markdown for your package documentation, I’d recommend SourceDocs.
Using SourceDocs
To build documentation using sourcedocs, first build your Swift package using swift build. Once your package is built, you run sourcedocs using:
sourcedocs generate --spm-module PACKAGE_NAME
Additionally, you can use a different output directory besides Documentation, such as docs. In order to do that, you can use the flag --output-folder like so:
A point often overlooked, is that this will fail if there is an already existing push committed while this CI takes place. Therefore, in those cases, the CI will fail. If you are concerned, I would recommend looking at using a pull request rather than a direct push to the branch.
After having built documentation for our swift package, it is important to make our package discoverable to others.
Making Your Swift Package Available
Now that we have setup:
CI based on the supported platforms such as Linux
linting and code quality checks
code documentation
… let’s discuss how we can properly distribute the package to others.
SwiftPM
While Swift packages can easily be added simply as a few lines in a Package.swift, discoverability can be difficult. Luckily thanks to the work of Dave Verwer, the community has a great site with swiftpm.co.
However there are some checks we should add to make sure your Swift package is valid. Additionally we can add these checks to the Swift package continuous integration.
Using jq to Verify Products
With this intention, we need to make sure that our swift package has at least one valid product. For instance, under the product section of my Package.swift for my package AssetLib, it has a library product listed:
...products:[// Products define the executables and libraries produced by a package, and make them visible to other packages..library(name:"AssetLib",targets:["AssetLib"])],...
Fortunately, we can automate this by dumping the JSON of the package and verifying the products listed using jq. jq is a fantastic command-line tool for processing JSON data similar to sed. Therefore by piping the JSON from the swift subcommand package dump-package to jq, we can check the number products:
swift package dump-package | jq -e".products | length > 0"
As can be seen, we are using the filter string to check the length property of the property products to see if there is more than one product.
Lastly, we can submit our Swift package for review by:
Making sure packages.json is sorted via jq by running:
echo "$(jq 'sort_by(ascii_downcase)' packages.json)" > packages.json
Run validate.sh
Create a pull request in GitHub
Then wait for acceptance to see your package listed at swiftpm.co.
Cocoapods Compatibility
While Swift packages are the future of dependency management, there are several reasons why Cocoapods is still in use. For this reason, it’s a good idea to add support for Cocoapods to your library. Luckily, there are only a few simple steps to add for making our Swift package is compatible.
Once you have Cocoapods installed, create the Cocoapods spec by running:
pod spec create $(git remote get-url origin)
At the present time, you should have a .podspec file named after your library. Inside the .podspec. file, add or set:
Long description under spec.description
Short summary under spec.summary
Create License File and set the set license type and file name under spec.license
The spec.source to the repository url and git tag of the target version
spec.source_files to Sources/**/*.swift
The spec.swift_versions
Lastly we can verify our package is valid in our existing continuous integration by including pod lib lint within the macOS sections of our script.
Setting Up An Example Project
Additionally I would encourage creating an example project using your library with targets in each operating system. Moreover, we can also verify our example project builds. After creating the example project in a directory called Example, create a Podfile using pod init. Then update the Podfile by adding a reference to your pod in the parent directory:
...target'iOS Example'do# Comment the next line if you don't want to use dynamic frameworksuse_frameworks!pod'PackageName',:path=>'../'# Pods for iOS Exampleend...
Next to verify the build in our macOS continuous integration, include the following lines:
Install the Cocoapod and create the workspace file
Build each application target for each operating system
Therefore, we have created a .podspec and added continuous integration for the pod as well as the example project. Lastly, we need to push the repository to GitHub and tag it with the proper version. Once that’s done, make sure you have a Cocoapods account setup and run: pod trunk push NAME.podspec to push our repo.
Examples and Integration
In the end, it's important to make sure your package maintains good quality, works on its designated platforms, is easily available, and is fully tested. If you are interested, you can check out packages which I have implemented using these practices:
Command Line Tool for Starting Your Swift Packages with Continuous Integration
EggSeed is a command-line tool for creating swift pacakges with continous integration support. While swift package init, creates simple packages, there is no guarantee that your package will work on everyone else's device. That's where continuous integration goes in.
By using eggseed, you can create a package with full integration into CI services such as: GitHub Actions, Travis-CI, BitRise, CircleCI and more. Not only that but EggSeed also sets up code documentation, linting, and more...
Check out the roadmap below for more details on future integrations.