For the past year, Swift Package Manager (SPM) has quickly gained popularity, with most big frameworks already adding support for it. In this article we will create a Swift Package by adding support for it to KNContactsPicker, one of my existing projects, which is already distributed via CocoaPods.
KNContactsPicker is a modern, highly customisable contact picker with multi-selection options that closely resembles the behaviour of the ContactsUI’s CNContactPickerViewController
.
So with that let’s have a look at what this article covers:
- Initialise the project as a Swift Package
- How to structure your project for Swift Package Manager
- Updating the Package.swift Manifest
- Build the Swift Package project
- Fixing the initial CocoaPods podspec
- More resources
Initialise the project as a Swift Package
Let’s navigate to the project’s directory using the terminal. To create a Swift Package, we can run the following command
$ swift package init Creating library package: KNContactsPicker Creating Package.swift Creating Sources/ Creating Sources/KNContactsPicker/KNContactsPicker.swift Creating Tests/ Creating Tests/LinuxMain.swift Creating Tests/KNContactsPickerTests/ Creating Tests/KNContactsPickerTests/KNContactsPickerTests.swift Creating Tests/KNContactsPickerTests/XCTestManifests.swift
Depending on whether the files exist already, this will generate the following files:
README.md
Package.swift
– which defines your package info, any dependencies, and specified which target and files to include in the package.Sources
– the folder where all the source code for your project should goTests
– the folder where your test files should go.gitignore
– to ensure you are not saving some compiled and unneeded files to version control
Since we are adding support for Swift Package Manager to an existing project, we won’t need some of the automatically generated files – so we can go ahead and remove the generated files inside the Sources and Tests folder.
In this case, KNContactsPicker
can only be run on iOS, so the LinuxMain.swift
file that was autogenerated won’t apply to my project. However, if your library supports more platforms, consider keeping it and whether there are any changes required to support Linux and other platforms.
We will come back to supported platforms later, when we address changes we need to make to the code for KNContactsPicker
.
How to structure your project for Swift Package Manager
As a convention, a Swift package follows a specific structure, separating the code in a Sources folder and a Tests folder. In this guide we’ll go ahead and restructure it to follow the Swift package pattern.
Note: Changing the structure of your project is optional. You can leave the structure unchanged by configuring your Package.swift to point it to the right folder/files corresponding to you source code and test, as well as exclude any files that may not be needed.
Let’s have a look at the KNContactsPicker
project file structure:
KNContactsPicker/ # Where source code is stored Extensions/ Array+Ext.swift CNContact+Ext ... Info.plist KNContactCell.swift KNContactCellModel.swift KNContactsPickerDelegate.swift ... KNContactsPickerTests/ # Where tests code is stored Info.plist KNContactsPickerTests.swift
We will need to reorganise the structure to this:
Sources/ KNContactsPicker/ Extensions/ Array+Ext.swift CNContact+Ext ... Info.plist KNContactCell.swift KNContactCellModel.swift KNContactsPickerDelegate.swift ... Tests/ KNContactsPickerTests/ KNContactsPickerTests.swift XCTestManifests.swift # Added after initialising the Swift Package
Note: Since the project is now a Swift Package, we can open it directly in XCode by opening Package.swift
. Optionally, we can remove the .xcodeproj
file, but if you decide to continue to use it, some settings under Build Settings / Packaging
may need to be updated for the both targets (your project and tests) to reflect where the Info.plist
resides now.
Great! We have now restructured the project. Let’s make sure the Package Manifest reflects that and that it will include the extra files under the Extensions folder.
Updating the Package.swift manifest
Let’s have a look at the Package.swift
file to understand what changes we need to make and adapt for our existing project. In this particular case I want to do three things:
- Specify iOS 12 and Swift 5 as the minimum requirements for using the project
- Explicitly specify the paths for
Sources
andTests
- Ensure the package can be built
Here’s how the Package.swift
file looks like right now:
// swift-tools-version:5.2 import PackageDescription let package = Package( name: "KNContactsPicker", products: [ .library( name: "KNContactsPicker", targets: ["KNContactsPicker"]), ], dependencies: [], targets: [ .target( name: "KNContactsPicker", dependencies: []), .testTarget( name: "KNContactsPickerTests", dependencies: ["KNContactsPicker"]) ] )
The name of the project looks alright while the products section defines the libraries produced by the project, with a mapping to the targets section to include in the package. This will make just the “KNContactsPicker” target visible to other packages. Theoretically we could have multiple separate targets made available, but let’s stick to this simple setting.
Since KNContactsPicker
was built to be dependency free, we’ll keep the dependencies section free. Finally the targets section defines a list of targets, the main project target and the test target.
Now that we had a look at the structure of the manifest file, let’s sort out the above requirements.
Specify the Swift Package Platform and Language Availability
As mentioned above, KNContactsPicker
only supports features from iOS 12 and above so let’s add that to supported platforms. This doesn’t change the way the project is built and compiled, it will help the potential package users by telling them if the package supports their platform. Add
// swift-tools-version:5.2 import PackageDescription let package = Package( name: "KNContactsPicker", platforms: [ .iOS(.v12) ], products: [ .library( name: "KNContactsPicker", targets: ["KNContactsPicker"]), ], dependencies: [], targets: [ .target( name: "KNContactsPicker", dependencies: []), .testTarget( name: "KNContactsPickerTests", dependencies: ["KNContactsPicker"]) ], swiftLanguageVersions: [.v5]) )
Specify the path for Sources and Tests explicitly
It’s worth mentioning that, according to the documentation, Swift Package does by default look into Sources
for the main target and into Tests
for test target, but let’s explicitly set the paths in our manifest.
let package = Package( name: "KNContactsPicker", platforms: [ .iOS(.v12) ], products: [ .library( name: "KNContactsPicker", targets: ["KNContactsPicker"]), ], dependencies: [], targets: [ .target( name: "KNContactsPicker", dependencies: [], path: "Sources" ), .testTarget( name: "KNContactsPickerTests", dependencies: ["KNContactsPicker"], path: "Tests" ) ], swiftLanguageVersions: [.v5] )
Let’s build the project
Technically we’re done with the setup, so we should be ready to build this package to make sure it compiles and it’s ready to be used by others. Let’s see what happens when we try to build it:
$ swift build ...KNContactsPicker/Sources/KNContactsPicker/Extensions/CNContact+Ext.swift:9:8: error: no such module 'UIKit'
There are a bunch of errors complaining certain libraries cannot be imported. In this case it’s UIKit. UIKit is a framework only available on iOS so let’s see how we can fix this.
Changes to your code depending on supported platform
Depending on which platforms you want to make your own package available for, some code changes may be required to ensure the package is supported across multiple devices and operating systems. For instance, on iOS most user interface elements and classes come from UIKit, whereas on macOS they will come from AppKit. Moreover, some functions that may be available in Foundation may have different equivalents on Linux.
So the error we saw above tells us that for a specific platform we tried to build, UIKit was not available. This is because Swift packages are platform-independent by nature, so we need to let the compiler know that certain parts of the code should only be available for iOS. For this project this makes sense and will be an easy fix, but in if your project works on multiple platforms, there might a bit more work involved to ensure feature parity and correct behaviour.
To fix this, we can wrap the classes that use UIKit in a conditional compilation block. We can do this at a high-level by OS, or more finely grained, per framework.
// Higher level, but more restrictive: #if os(iOS) // Code specific to iOS #elseif os(macOS) // Code specific to macOS #endif // Per framework, less restrictive: #if canImport(UIKit) // Code specific to platforms where UIKit is available #endif
Consider what make sense for your project. It’s up to you how restrictive or permissive you want to be. There are more compilation conditions available here.
The fix required changes to multiple files, and finally, once everything was fixed, the build command succeeded.
$ swift build [16/16] Merging module KNContactsPicker
Another good idea to see if the targets are set properly is to run the tests with swift test
.
Fixing the initial CocoaPods podspec
Note:
If you haven't changed your project's structure to follow the Swift Package convention, then you may not need to follow this step.
We’ve started with a CocoaPods project and we’ve added support for SPM. However in the process we’ve probably broken the podspec configuration, so let’s change that to ensure we continue to support both.
Just like in Package manifest we now have to let CocoaPods know where to find our project source files, so we need to change the spec.source_files
and add just the files under the Sources
folder.
Pod::Spec.new do |spec| spec.name = "KNContactsPicker" spec.version = "0.2.1" spec.summary = "KNContactsPicker is a modern, highly customisable contacts picker with search and multi-selection options." # ... spec.source_files = "Sources/KNContactsPicker/*.swift", "Sources/KNContactsPicker/Extensions/*.swift" # ... end
And with this the project should be ready to commit the changes to version control (Github in this case) and tag the release. Our users will now be able to import KNContactsPicker
as a Swift Package by specifying it as a dependency directly in their XCode projects or in their Package.swift
manifest.
More resources
If you’d like to consult more articles and learn more about how to create a Swift Package, here are some sources:
- https://developer.apple.com/documentation/xcode/creating_a_standalone_swift_package_with_xcode
- https://www.raywenderlich.com/1993018-an-introduction-to-swift-package-manager
- https://docs.swift.org/package-manager/PackageDescription/PackageDescription.html
This article was last revised 3 April 2021, adding notes about restructuring the project to follow the Swift Package folder structure and making it clear that the step is optional.
Thanks for this article. It helped me publish my existing CocoaPod library as a Swift package. It’s kind of implied in the article but just to reinforce it for anyone not sure: it’s not necessary to restructure your project to follow the ‘Sources’ and ‘Tests’ directory structure which Xcode creates by default for a new Swift package project. You can keep your existing directory structure and just communicate in the ‘Package.swift’ file where the sources and tests can be found. See the following ‘Package.swift’ file for an example: https://github.com/adil-hussain-84/quran-sdk/blob/master/Package.swift
Thank you for pointing that out Adil, I have just added two notes to the article to make it clearer 🙂