iOS Dependency Managers (SwiftPM, CocoaPods, Carthage)

Jun 02, 2022iosswiftobjcxcode

Dependency managers simplify and standardize the process of fetching third-party code and incorporating it into your project. Without this tool, you’d do this by manually copying source code files, dropping in pre-compiled binaries or using a mechanism like Git submodules.

Swift Package Manager (SwiftPM) officially supported by Apple since Swift 3.0 with limited features. Since the Swift 5 and Xcode 11, SwiftPM is compatible with the iOS, macOS and tvOS build systems for creating apps.

CocoaPods has been the most popular iOS dependency manager for years, widely used in the iOS community, has best discoverability experience as package registry service.

Carthage is another simpler, decentralized but less popular solution.

Dependency Management

Modern development is accelerated by the use of external dependencies (for better and worse). This is great for allowing you to get more done in less time, but adding dependencies to a project has an associated coordination cost.

In addition to downloading and building the source code for a dependency, that dependency’s own dependencies must be downloaded and built as well, and so on, until the entire dependency graph is satisfied. To complicate matters further, a dependency may specify version requirements, which may have to be reconciled with the version requirements of other modules with the same dependency.

The role of the package manager is to automate the process of downloading and building all of the dependencies for a project, and minimize the coordination costs associated with code reuse.

Dependency Hell

Dependency hell is the colloquialism for a situation where the graph of dependencies required by a project cannot be met. Common scenarios are:

  • Inappropriate versioning - A package may specify an inappropriate version for a release.
  • Incompatible major version requirements - A package may have dependencies with incompatible version requirements for the same package.
  • Incompatible minor or update version requirements - A package may have dependencies that are specified too strictly, such that version requirements are incompatible for different minor or update versions.
  • Namespace collision - A package may have two or more dependencies that have the same name.
  • Broken software - A package may have a dependency with an outstanding bug that is impacting usability, security, or performance.
  • Global state conflict - A package may have two or more dependencies that presume to have exclusive access to the same global state.
  • Package becomes unavailable - A package may have a dependency on a package that becomes unavailable.

A good package manager should be designed from the start to minimize the risk of dependency hell, and where this is not possible, to mitigate it and provide tooling so that the user can solve the scenario with a minimum of trouble.

Semantic Versioning

A simple set of rules and requirements that dictate how version numbers are assigned and incremented.

Consider a version format of X.Y.Z (Major.Minor.Patch). Bug fixes not affecting the API increment the patch version, backwards compatible API additions/changes increment the minor version, and backwards incompatible API changes increment the major version.

Under this scheme, version numbers and the way they change convey meaning about the underlying code and what has been modified from one version to the next.

Binary Dependencies

Carefully consider whether you want to add a binary dependency, because doing so comes with drawbacks. For example, a binary dependency is less portable because it can only support platforms that its included binaries support, and binary dependencies are only available for Apple platforms.

If you have a choice between a source-based dependency and a binary dependency, use the source-based dependency if it provides the same functionality.

Git Submodules

Git submodules allows you to have a folder in your repository be populated from another Git repository. You can either track a specific commit or a branch in the given submodule.

Before you add a repository as a submodule, first check to see if you have a better alternative available, modern languages like Swift have friendly, Git-aware dependency management systems built-in from the start.

Git submodules work well enough for simple cases, but these days there are often better tools available for managing dependencies than what Git submodules can offer.

  • Git doesn’t download submodule contents by default.
  • Submodules require you to carefully balance consistency and convenience. The setup used here strongly prefers consistency, at the cost of a little convenience.
  • Collaborators won’t automatically see updates to submodules—if you update a submodule, you may need to remind your colleagues to run git submodule update or they will likely see odd behavior.
  • Managing dynamic, rapidly evolving or heavily co-dependent repositories with submodules can quickly become frustrating.

Swift Package Manager (SwiftPM)

SwiftPM is a tool built by Apple as part of the Swift project for integrating libraries and frameworks into your Swift apps. It launched in 2015 and gained integration with Xcode 11 in 2019.

The tool directly addresses the challenges of compiling and linking Swift packages, managing dependencies, versioning, and supporting flexible distribution and collaboration models.

Swift Packages

A package consists of Swift source files, including the Package.swift manifest file. The manifest file, or package manifest, defines the package’s name and its contents using the PackageDescription module. A package has one or more targets. Each target specifies a product and may declare one or more dependencies.

// swift-tools-version:4.0
import PackageDescription

let package = Package(
  name: "DeckOfPlayingCards",
  products: [
    .library(name: "DeckOfPlayingCards", targets: ["DeckOfPlayingCards"])
  ],
  dependencies: [
    .package(url: "https://github.com/apple/example-package-fisheryates.git", from: "2.0.0"),
    .package(url: "https://github.com/apple/example-package-playingcard.git", from: "3.0.0"),
  ],
  targets: [
    .target(
      name: "DeckOfPlayingCards",
      dependencies: ["FisherYates", "PlayingCard"]),
    .testTarget(
      name: "DeckOfPlayingCardsTests",
      dependencies: ["DeckOfPlayingCards"]),
  ]
)

A package author can publish their Swift package to either public or private repositories. Xcode supports both private and publicly available packages.

Swift Modules

Swift organizes code into modules. Each module specifies a namespace and enforces access controls on which parts of that code can be used outside of that module.

A program may have all of its code in a single module, or it may import other modules as dependencies. Aside from the handful of system-provided modules, such as Darwin on OS X or GLibc on Linux, most dependencies require code to be downloaded and built in order to be used.

Package Collections

In Swift 5.5, SwiftPM adds support for package collections — bite size curated lists of packages that make it easy to discover, share and adopt packages.

Package collections embrace and promote the concept of curation. Instead of browsing through long lists of web search results, package collections narrow the selection to a small list of packages from curators you trust.

Swift Package Index

You can develop your app without ever publishing it in a place where others can see or use it. On the other hand, if one day you decide that your project should be available to a wider audience your sources are already in a form ready to be published.

The package manager is also independent of specific forms of distribution, so you can use it to share code within your personal projects, within your workgroup, team or company, or with the world.

The Swift Package Index is a search engine for packages that support the Swift Package Manager.

But this site isn’t simply a search tool. Choosing the right dependencies is about more than just finding code that does what you need. Are the libraries you’re choosing well maintained? How long have they been in development? Are they well tested? Picking high-quality packages is hard, and the Swift Package Index helps you make better decisions about your dependencies.

CocoaPods

CocoaPods manages dependencies for your Xcode projects. It aims to improve the engagement with, and discoverability of, third party open-source Cocoa libraries.

CocoaPods is built with Ruby and it will be installable with the default Ruby available on macOS.

$ sudo gem install cocoapods

You specify the dependencies for your project in a simple text file Podfile.

platform :ios, '9.0'

target 'MyApp' do
  pod 'AFNetworking', '~> 3.0'
  pod 'FBSDKCoreKit', '~> 4.9'
end

CocoaPods recursively resolves dependencies between libraries, fetches source code for all dependencies, and creates and maintains an Xcode workspace to build your project.

CocoaPods supports any source management system. (Currently supported are git, svn, mercurial, bazaar, and various types of archives downloaded over HTTP.)

When you use CocoaPods, it makes several changes to your Xcode project and binds the result, along with a special Pods project, into an Xcode workspace.

Whether or not you check in your Pods folder is up to you, as workflows vary from project to project. It’s recommended that you keep the Pods directory under source control, and don’t add it to your .gitignore. But ultimately this decision is up to you:

  • Checking in the Pods directory: After cloning the repo, the project can immediately build and run, even without having CocoaPods installed on the machine. The Pod artifacts are always available and guaranteed to be identical to those in the original installation after cloning the repo.
  • ignoring the Pods directory: The source control repo will be smaller and take up less space. There won’t be any conflicts to deal with when performing source control operations. Technically there is no guarantee that running pod install will fetch and recreate identical artifacts when not using a commit SHA in the Podfile.

CocoaPods itself does not require the use of a workspace. If you prefer to use sub-projects, you can do so by running pod install --no-integrate, which will leave integration into your project up to you as you see fit.

Carthage

Carthage is intended to be the simplest way to add frameworks to your Cocoa application.

brew install carthage

Once you have Carthage installed, you can begin adding frameworks to your project. Note that Carthage only supports dynamic frameworks, which are only available on iOS 8 or later (or any version of OS X).

List the desired dependencies in the Cartfile, for example:

github "Alamofire/Alamofire" ~> 5.5
github "Alamofire/AlamofireImage" ~> 3.4

Carthage builds your dependencies and provides you with binary frameworks, but you retain full control over your project structure and setup. Carthage does not automatically modify your project files or your build settings.

You must drag the built .xcframework bundles from Carthage/Build into the “Frameworks and Libraries” section of your application’s Xcode project.

Benefits of using Carthage:

  • It doesn’t change your Xcode project or force you to use a workspace.
  • You don’t need Podspecs or a centralized repository where library authors submit their pods. If you can build your project as a framework, you can use it with Carthage, which leverages existing information straight from Git and Xcode.
  • Carthage doesn’t do anything magically; you’re always in control. You add dependencies to your Xcode project and Carthage fetches and builds them.