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
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:
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 go
Tests– 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
How to structure your project for Swift Package Manager
By convention, to create a Swift package a specific structure is needed: a Sources folder and a Tests folder. If your project currently has a different structure, then you may want to move the files around a bit.
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
- 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.
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
Fixing the initial CocoaPods podspec
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
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
If you’d like to consult more articles and learn more about how to create a Swift Package, here are some sources:
This article was last revised 30 May 2020.