Categories
Swift Swift Package

Running tests in Swift package with GitHub actions

Some time ago I published a tiny Swift package IndexedDataStore which tackles a problem of storing data blobs on disk. It could the image data or anything else. When working with Swift packages then it is extremely easy to build and run tests on macOS but when we want to build and run tests on iOS simulator then we need to drop using swift build and swift test commands. Fortunately xcodebuild can help here and we can build and run tests without generating a project file ourselves with swift package generate-xcodeproj. Something I would like to mention about the generate-xcodeproj command is that it generates a project which does not contain resource files. The aim of this blog post is to configure a GitHub action which runs unit-tests both on macOS and iOS.

GitHub actions need to be in the repostory’s .github/workflows folder. GitHub workflows are defined in YAML files which need to be in that folder. Let’s jump into it and start creating a worflow which runs tests on macOS and iOS. GitHub workflows must define a name, in our case, name: CI. Secondly we’ll need to define when the workflow should be triggered. Running the workflow whenever pull request is created of when something was pushed to main branch will suffice.

name: CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches:
      - '*'

The next step is creating a job and defining a number of steps. Jobs require a name, then which resource it needs to use. The first step is the repository checkout followed by running swiftlint which is already part of the enviornment. The linting step uses the default configuration file .swiftlint.yml at the repository root. At this point we are ready to build and run tests.

jobs:
  unit_tests:
    runs-on: macos-latest
    steps:
    - name: Repository checkout
      uses: actions/checkout@v2
    - name: Lint
      run: swiftlint

The simplest way to build and test Swift packages is to use swift build and swift test commands which builds and runs tests on macOS. Unfortunately there is not a way to use those commands and setting the deplyoment to iOS. Therefore we’ll need to use xcodebuild command instead. It will know how to deal with Swift packages and therefore we only need to define scheme and destination arguments. It also makes sense to separate building (build-for-testing) from running tests (test-without-building) as it makes it easier to see where a failure happened. We can make the xcodebuild output a little bit cleaner by using xcpretty. In addition it is also reasonable to use set -o pipefail which makes the pipeline to use an occured error code as the final code of the whole pipeline. Finally, we’ll disable any buffering by setting NSUnbufferedIO to YES.

jobs:
  unit_tests:
    runs-on: macos-latest
    steps:
    - name: Repository checkout
      uses: actions/checkout@v2
    - name: Lint
      run: swiftlint
    - name: Build for macOS
      run: swift build -v
    - name: Run macOS tests
      run: swift test -v
    - name: Build for iOS
      run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild build-for-testing -scheme IndexedDataStore -destination "platform=iOS Simulator,OS=latest,name=iPhone 12" | xcpretty
    - name: Run iOS tests
      run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild test-without-building -scheme IndexedDataStore -destination "platform=iOS Simulator,OS=latest,name=iPhone 12" | xcpretty

This completes our workflow and we can check in the YAML file which gets then picked up by GitHub. All the actions can be see under the Actions section on GitHub.

Summary

We looked into how to add a GitHub workflow for running tests both on macOS and iOS. Although swift build does not support building the package for iOS we could still do that with xcodebuild.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift

Code coverage for Swift Packages with Fastlane

Package.swift file in Swift packages describes the package contents including other dependencies, build targets and so on. Double-clicking a Package.swift file opens Xcode with that package and one can build and run tests. When adding continues integration to a Swift package then we would like to build, test, and also create a code coverage report. Fastlane is an excellent tool for such tasks and therefore let’s take a look on how to achieve that for packages with iOS target.

Code coverage support needs to be explicitly set and this can be done by using Swift Package Manager’s generate-xcodeproj command with having code coverage turned on. SPM then creates a Xcode project which has a slightly different scheme name with “-Package” suffix. The generated project file can then be used to run tests with Fastlane’s run_tests action. After running test we can use a tool like xcov for generating an HTML page with a code coverage report. One of the great features of the xcov is the ability to force a minimum code coverage percentage.

This approach works fine when we do not have any resource files in the package which are required for running tests. The problem is that the generate-xcodeproj command creates a project file without resource files. In those cases we would need to either use a separate standalone project or workspace file which has code coverage turned on.

Down below, we can see an example Fastfile which has a lane called “test_report” which generates a project file, runs tests and uses xcov for generating code coverage report. Additionally, it will delete the generated project file and any related folders.

require 'fileutils'
default_platform(:ios)
platform :ios do
desc "Run tests and create a unit-test report"
lane :test_report do
spm(
command: 'generate-xcodeproj',
enable_code_coverage: true
)
run_tests(
project: 'SPMPackage.xcodeproj',
scheme: 'SPMPackage-Package',
device: 'iPhone 12',
output_directory: './fastlane/UnitTests',
clean: true
)
xcov(
project: 'SPMPackage.xcodeproj',
scheme: 'SPMPackage-Package',
output_directory: './fastlane/CodeCoverage',
include_targets: 'SPMPackage.framework',
minimum_coverage_percentage: 80.0
)
if ENV['CI'] != 'true'
sh 'open ./UnitTests/report.html'
sh 'open ./CodeCoverage/index.html'
end
end
before_all do
FileUtils.remove_dir './CodeCoverage', true
FileUtils.remove_dir './UnitTests', true
end
after_all do
FileUtils.remove_dir './../SPMPackage.xcodeproj', true
end
end
view raw Fastfile.rb hosted with ❤ by GitHub

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS macOS Swift

Publishing API documentation in GitHub with Jazzy

Jazzy is an excellent tool for generating API documentation for Swift and Objective-C projects. Let’s take a look how to generate and publish API documentation in GitHub with Jazzy.

Installing Jazzy

Installation: sudo gem install jazzy

Jazzy config file for a Swift package

Configuration options can be passed into Jazzy directly with the command or adding them to the a file, by default it is .jazzy.yaml. We’ll use the default path as then we can run Jazzy without any command line arguments: the configuration is read from the configuration file. For seeing all the available configuration options run jazzy config -h. Just note that the configuration file expects snakecase (build-tool-arguments becomes build_tool_arguments). Let’s take a look on Swift package IndexedDataStore which can be built both for macOS and iOS. It has some additional functions for iOS and therefore it is preferred to build iOS target when running Jazzy. Otherwise API documentation would not contain those public functions meant for iOS. Typically Swift package is built using swift build command. The current state is that there is no easy way for just specifying the target OS to the swift build command. What we can do instead is using xcodebuild command which knows how to build Swift packages as well. We’ll just need to specify the scheme, sdk, and destination arguments. If we now run jazzy command without any arguments, it will read the configuration file, and generate API documentation which includes functions which require UIKit.

author: Toomas Vahter
author_url: https://www.augmentedcode.io
github_url: https://github.com/laevandus/IndexedDataStore
output: Docs
swift_build_tool: xcodebuild
build_tool_arguments:
-scheme
IndexedDataStore
-sdk
iphoneos
-destination,platform=iOS Simulator,OS=latest,name=iPhone 11 Pro
view raw .jazzy.yaml hosted with ❤ by GitHub
Configuration file for Jazzy which builds documentation for iOS.

GitHub action for publish API documentation

Thankfully there is a GitHub action available for publishing API documentation with Jazzy. We can set up a GitHub action with a name PublishDocumentation and store it in the repository’s .github/workflows folder.

name: PublishDocumentation
on:
workflow_dispatch:
release:
types: [ published ]
jobs:
publish:
runs-on: macos-latest
steps:
– uses: actions/checkout@v2
– name: Publish Jazzy Docs
uses: steven0351/publish-jazzy-docs@v1
with:
personal_access_token: ${{ secrets.ACCESS_TOKEN }}
config: .jazzy.yaml
GitHub action which publishes API documentation to GitHub pages.

The GitHub action is triggered either manually or when publishing a release. Manual trigger is called workflow_dispatch and when it is set, GitHub webpage will display a “Run workflow” button. This is very handy when testing GitHub actions. Another thing to note is that publish-jazzy-docs requires repository access because it needs to write the documentation files to the gh-pages branch. For giving repository access we’ll need to set up personal access token with repo scope. Secondly, we’ll need to paste the created token to the repository’s secrets. In this example, we have added a secret named ACCESS_TOKEN and the value is the personal access token. Now, if we have committed and pushed the GitHub action then we can open the repository on GitHub.com, navigate to actions, selecting PublishDocumentation, and using Run workspace button for triggering the wokrflow. If everything goes well, then the workspace creates a gh-pages branch which in turn creates a new GitHub page. In this case the URL to the new GitHub page looks like: https://laevandus.github.io/IndexedDataStore/ (link). This is what we wanted to achieve, API documentation publiched on GitHub.

Summary

We set up Jazzy for a Swift package and used it to generate API documentation. Generated API documentation was published to a GitHub page.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift Swift Package Xcode

Creating and publishing a Swift package

In the previous post we looked into how to separate code with local Swift packages within a project. This time let’s create a Swift package, publish it on GitHub, and add it to a separate project. We’ll create a package which extends UIImage and enables calculating color contrast ratios. Color contrast is important factor for keeping text readable in apps.

Creating a Swift package

Open Xcode and select “Swift Package” item from the File > New menu. We’ll set the name of the package to “ColorContrastRatio”.

Selecting a new Swift Package.
Saving a new Swift package.

Xcode’s template of new Swift packages is configured to have a basic hello world example with an unit-test. Before we change the package’s implementation, we’ll add minimum platform versions to the Package.swift file, in other words, minimum deployment target.

// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "ColorContrastRatio",
platforms: [
.iOS(.v13), .macOS(.v10_15)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "ColorContrastRatio",
targets: ["ColorContrastRatio"]),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "ColorContrastRatio",
dependencies: []),
.testTarget(
name: "ColorContrastRatioTests",
dependencies: ["ColorContrastRatio"]),
]
)
view raw Package.swift hosted with ❤ by GitHub

Next step is to implement functionality of the package. We’ll keep the package simple and make it UIKit only which can be built for iOS and macCatalyst apps. Package is adding an UIColor extension and providing functionality for calculating relative luminance and contrast ratio. Functionality is set, next step is to publish it.

import UIKit
/*
Contrast ratio is calculated using the proceedure here:
https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-procedure
*/
public extension UIColor {
/// Relative luminance of the color.
var relativeLuminance: CGFloat {
var red: CGFloat = 0
var green: CGFloat = 0
var blue: CGFloat = 0
guard getRed(&red, green: &green, blue: &blue, alpha: nil) else { return 1.0 }
let convert: (CGFloat) -> CGFloat = { component in
guard component > 0.03928 else { return component / 12.92 }
return pow(((component + 0.055) / 1.055), 2.4)
}
return 0.2126 * convert(red) + 0.7152 * convert(green) + 0.0722 * convert(blue)
}
/// Returns contrast ratio with other color.
/// – Parameter otherColor: UIColor in RGB color space.
/// – Returns: Contrast ratio of two colors.
func contrastRatio(_ otherColor: UIColor) -> CGFloat {
let luminance1 = relativeLuminance
let luminance2 = otherColor.relativeLuminance
return (min(luminance1, luminance2) + 0.05) / (max(luminance1, luminance2) + 0.05)
}
}

Publishing a Swift package

First step is to go to GitHub and adding a new repository. I chose to include an automatically created license file, cloned the repository on my mac and then moved the code we added in the previous step to that checkout, then committed and pushed. Alternative is to add a remote to your local git repository. Either way is fine as long as our project ends up on GitHub. In addition, will add a tag which will mark the first release 0.1.0. Tags can be added by running those commands in Terminal:

git tag -a 0.1.0 -m “0.1 release of the package”

git push origin 0.1.0

Another option is creating a tag in the GitHub’s web interface: releasing projects on GitHub.

Adding the published Swift package to another project

We published our package on GitHub and it requires only a few steps for adding it to an existing project. In an existing project, open the target settings and click on the plus button in the “Frameworks, Libraries, and Embedded Content”. In the opened view, click on the “Add Other” and select “Add Package Dependency”. Then we can paste the package’s GitHub url (https://github.com/laevandus/ColorContrastRatio) to the search field and complete the flow. From then on, it is just a matter of importing the new package and using it in the main project. Described steps are shown below:

Selecting the target where to the package is added.
Selecting “Add Package Dependency” which allows adding Swift package.
Searching for the Swift package by url.
Setting the package update rule.
Choosing products from the package.
Finished example application using the Swift package’s color contrast ratio function.
import ColorContrastRatio
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Text("Luminance for red color").padding()
Rectangle()
.frame(width: 50, height: 50)
.fixedSize()
.foregroundColor(.red)
Text("\(UIColor.red.relativeLuminance)")
}
}
}

Summary

We created a Swift package and a repository on GitHub. Then we proceed with making the first release by adding a git tag. After that, we went ahead and added the package to another example project.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Resources

ColorContrastRatio (GitHub, Swift Package)

Swift Packages (Apple)

Categories
iOS macOS Swift Swift Package SwiftUI Xcode

Separating code with Swift packages in Xcode

Xcode 12 comes with Swift toolchain 5.3 which brings resource and localisation support to Swift packages. Nice thing is that Swift package support only depends on the toolchain’s version and does not have additional OS requirements. At the same time, let’s keep in mind that OS requirements come from the code we actually add to the package. This means that it is a good time to start using Swift packages for separating code into separate libraries and stop using separate framework projects in a workspace. Additional benefit is that, if needed, it is pretty easy to move the package out of the workspace and creating a sharable package what can be publish and reused in other projects. But for now, let’s take a look on how to set up a new workspace with an app project and a Swift package which represents a design library with custom button style.

File structure of the workspace

The app project will have a name “ButtonGallery” and the Swift package will have a name “ButtonKit”. But first, let’s create a folder named “SwiftPackageAppWorkspace” which is the root folder of the project. The app project and the Swift package will go to that folder in separate folders.

Adding the workspace, the app project, and the Swift package

New workspace can be created by selecting the Workspace menu item in File>New menu in the Xcode. Save the workspace in the “SwiftPackageAppWorkspace” folder what we created just before. Xcode opens the created workspace after clicking on save and then the next step is to add a new Swift package. Easiest is to use the plus button at the bottom of the left corner, selecting “New Swift Package”, and saving the package in the “SwiftPackageAppWorkspace” folder. Uncheck the option on the save panel for creating a git repository because the git repository should be added in the “SwiftPackageAppWorkspace” instead (we skipped this step). Third step is to add the app project by using File>New menu. Xcode also offers an option to add the new project to the workspace. Therefore make sure “Add to” and “Group” have the workspace selected on the save panel. Described steps are shown below.

Selecting a new workspace in the main menu.
Using the plus button in the workspace for creating a new package.
Saving Swift package in the root folder.
Adding a new Xcode project.
Selecting template for the project.
Adding a name to the app project.
Saving a new app project and adding to an existing workspace.
Workspace with a Swift package and an app project.

Linking the Swift package in the app project

Swift package needs to be added to the app target: select “ButtonGallery” in the project navigator, click on the iOS target, General, and then on the plus button in the “Frameworks, Libraries, and Embedded Content” section, select the “ButtonKit” library.

Navigating to iOS target’s general settings.
Linking with the ButtonKit.

Now the workspace is configured but there is not any useful code in the “ButtonKit”. Let’s fix that next and add a FunkyButtonStyle.swift to the package and set minimum platforms in the package manifest because we’ll use SwiftUI in the implementation and it has minimum platform requirements. Because FunkyButtonStyle is in a separate module and by default access control is set to internal, then we’ll need to make it public before it can be imported to the app target.

// swift-tools-version:5.3
import PackageDescription
let package = Package(
name: "ButtonKit",
platforms: [
.iOS(.v14), .macOS(.v10_15)
],
products: [
.library(
name: "ButtonKit",
targets: ["ButtonKit"]),
],
targets: [
.target(
name: "ButtonKit", dependencies: []),
.testTarget(name: "ButtonKitTests", dependencies: ["ButtonKit"]),
]
)
view raw Package.swift hosted with ❤ by GitHub
import SwiftUI
public struct FunkyButtonStyle: ButtonStyle {
public init() {}
public func makeBody(configuration: Self.Configuration) -> some View {
configuration.label.padding()
.background(Color.red)
.cornerRadius(16)
.foregroundColor(.white)
}
}
import ButtonKit
import SwiftUI
struct ContentView: View {
var body: some View {
Button("Title", action: tap).buttonStyle(FunkyButtonStyle())
}
func tap() {
print("Tapped")
}
}
ContentView in the app target which imports ButtonKit and uses its FunkyButtonStyle.

Summary

We created a new workspace what contains a Swift package and an app project. We looked into how to provide functionality in the package and making it available for the main app target.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Project

SwiftPackageAppWorkspace (Xcode 12b1)