How to create a Swift Package from a CocoaPods project

create a swift package from a cocoapods project

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

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 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 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:

  1. Specify iOS 12 and Swift 5 as the minimum requirements for using the project
  2. Explicitly specify the paths for Sources and Tests
  3. 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:

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.