DIY Swift Package Manager Dependencies

 · 5 min
Rowan Heuvel on Unsplash

The way we manage iOS dependencies has evolved a lot over time. And the latest and most integrated offering is the Swift Package Manager (SPM).

But what to do if your favorite dependency is not SPM-compatible (yet)? We do it ourselves!


A brief history

In the beginning, there was nothing.

As with all software projects, relying on dependencies and sharing code is a crucial key for a better developer ecosystem.

But sharing code meant we need either to copy the source directly into your project or linking it with a framework manually.

About three years after the original iPhone SDK was released, the first dependency manager was released, too.

But not by Apple.

CocoaPods

CocoaPods is an application level dependency management system inspired by RubyGems.

Installable with already available tools in macOS, creating a single extra file called Podfile with all your dependencies, and running pod install. That’s all that’s needed to get our favorite 3rd party frameworks automagically integrated into our Xcode project by creating an Xcode workspace, and adding them as standalone projects linked to our original project.

CocoaPods is source-based, so all our pods are compiled with our project. This means longer compile times, and possible breakage on SDK updates. Also, by re-working your project structure to a workspace it’s tightly integrated now and not easily removable.

But it was the status-quo of dependency management for iOS projects, so we learned to love it, and it worked very well, and grew fast.

Carthage

The tight integration of CocoaPods is also a caveat. A more loosely coupled solution emerged: Carthage.

No touching of our project files. No additional projects. No extra compilation every time we compile your project. No centralized repository.

Instead of integrating the dependencies as source code into your project, Carthage downloads the source, compiles it only once, and we have to add the framework manually. Yes, it’s more work as with CocoaPods. But it’s also more flexible and not as tightly coupled.

Swift Package Manager (SPM)

It’s a shame Apple didn’t provide a solution for dependency management in Xcode for 11 years. Finally, Xcode 11 got a native integration of the Swift Package Manager that was released 3 years earlier.

We can add our dependencies directly within Xcode, and the Swift Package Mangager will automatically download, compile, and link all the dependencies for us.

Just like Carthage, it doesn’t need a central repository. Any Git repository can be used, even local ones.

The biggest problem is that not all dependencies are compatible yet. But we can change that ourselves.


Non-SPM to SPM

In my current iOS project, I’m trying not to mix different dependency managers. We could use all of them together, but this means we have to deal with their edge cases and problems at once.

Instead of using CocoaPods, the project was started with Carthage. With the release of Xcode 11 I’ve migrated all but one dependency to SPM.

A few days ago I wanted to add another dependency for easier “empty state” handling of UICollectionViews. But the 2 libraries I wanted to try out were both not SPM-compatible.

Of course, I could have added them to a Cartfile and use them with Carthage, but what’s the fun in that?

Instead, I decided to make them compatible with SPM myself.

Package.swift

The core of every SPM dependency is a file called Package.swift. It tells SPM how to consume the dependency by specifying source folders, targets, version requirements, etc.

With the simple addition of a Package.swift file we’ve created an SPM-compatible dependency.

Here’s an example:

swift
// swift-tools-version:5.1
// This comment is necessary, and every Package.swift file
// must start with it.
// It tells SPM which version to use.
// It doesn't have to be the same version as your code,
// but it should be ABI compatible.

import PackageDescription

let package = Package(    name: "MyCoolPackage",

    // Which platforms and minimum deployment targets are supported
    // See: https://docs.swift.org/package-manager/PackageDescription/PackageDescription.html#supportedplatform
    platforms: [
        .iOS(.v9)
    ],

    // The externaly visible build artifacts
    // See: https://docs.swift.org/package-manager/PackageDescription/PackageDescription.html#product
    products: [
        // The library that you can actually import        
        .library(
            name: "MyCoolPackage",
            targets: [ "MyCoolPackage" ]
        )
    ],

    // Your package might need other packages.
    // Due to being decentralized you have to tell SPM where to look.
    dependencies: [
        .package(
            url: "https://github.com/another/great-dependency.git",
            from: "2.0.0"
        )
    ],

    // The basic building blocks.
    // See: https://docs.swift.org/package-manager/PackageDescription/PackageDescription.html#target
    targets: [
        // Our package contains two targets, one for our library
        // code, and one for our tests:
        .target(
            name: "MyCoolPackage",
            path: "src/Classes"
        ),

        .testTarget(
            name: "MyCoolPackageTests",
            dependencies: [ "MyCoolPackage" ]
        )
    ]
)

Of course, there are many other options. But for simple pods or Carthage-based dependencies, this might be just enough to get it up and running.

Here is all the information about the SPM and how to use it if you’re interested in the rest of the options:

Hosting the dependency

The simplest way to host our modified dependency is a GitHub fork.

  1. Fork the original dependency
  2. Checkout your fork
  3. Add Package.swift as needed
  4. Add, Commit, Push.
  5. Done!

Now we can use the dependency as any other dependency. With the additional advantage of creating a pull request with our changes to the original dependency, so others might enjoy an SPM-compatible dependency. Or we merge changes back into the fork if the original author is not interested in adding SPM for free.

As mentioned before, we could also use a local project by specifying the git repository with a file:///Users/ben/code/path/to/repository/

Caveat

At the time of writing the article, the SPM doesn’t support anything else but code. No resources or bundles.

So if your dependency needs any assets, you’re out of luck at the moment.

For the dependencies I needed, this wasn’t a problem, but your requirements may differ.


Conclusion

Using Swift Package Manager is great, and simple dependencies can easily be converted by ourselves.

In my opinion, you should try to use SPM if possible because it’s the native and kind of official way to go.

If you need other dependencies that can’t be converted, you should rely on Carthage. But I’m really looking forward to only needing a single dependency manager in the future.