Home · iOS & Swift Tutorials

Swift Package Manager for iOS

In this tutorial, you’ll learn how to use the Swift Package Manager (SPM) to create, update and load local and remote Swift Packages.

5/5 7 Ratings

Version

  • Swift 5, iOS 13, Xcode 11

The concept of a package is nothing new. Packages let you use third-party, open-source code with ease. They also make it easier to split your code into reusable, logical chunks you can easily share across your projects, or indeed with the entire world. :]

The Swift Package Manager, or SPM, has been around since Swift 3.0. It was initially only available for server-side or command line Swift projects. Since the release of Swift 5 and Xcode 11, SPM is compatible with the iOS, macOS and tvOS build systems for creating apps. This is great news for app developers because they can now unleash its power on their codebases!

A package is simply a collection of Swift source code files and a metadata, or manifest, file called Package.swift that defines various properties about the package, such as its name and any code dependencies it has.

In this tutorial, you’ll use learn all about the power of SPM. You’ll add extra functionality into an app called Pen of Destiny, which is a silly app to solve a common problem: randomly choosing a person for a task. You’ll reuse an existing package and you’ll also create your very own package.

Getting Started

Download the project materials using the Download Materials button at the top or bottom of this tutorial. Open the starter project in Xcode. Build and run.

Pen of Destiny title, image of a black Sharpie pointing up and a slider bar at the bottom.

Within my team, the same person always volunteers to start a stand-up or scribe meeting notes. Ideally, everyone should share this load, so I started spinning a pen on the table to pick somebody at random for each task. This method worked well, but we didn’t always have a pen or a table.

So, like any good app developer, I built an app the team can use instead.

Tap the pen. It’ll spin before settling in a random direction. A slider controls the number of resting positions under the pen. These represent the number of people in your team.

You can access a settings screen via the gear icon in the navigation bar. There you can set the style of the pen.

Note: At the time of writing, 13.3 is the latest version of iOS. Unfortunately, there’s an annoying simulator only bug since 13.2, which means navigating to new screens a second time is broken. There’s an interesting discussion of the problem on this forum thread.

Apple has acknowledged the bug and is working on a fix. Workarounds are available, but they add complexity to the app. Since this isn’t a navigation tutorial, I decided not to add the fix to the sample code.

Just run the sample on a device, or you’ll have to restart the app if navigating to Settings a second time doesn’t work.

Customizing the App

The app works, but is also missing functionality.

Navigate to the settings screen and select the fountain pen. Now navigate to the main screen. Notice the pen image hasn’t updated.

In Xcode, open SpinningPenView.swift. This file contains a simple SwiftUI view.

The body property defines a VStack. This starts with a Button control which renders an Image that’s hard coded to the sharpie asset. No wonder the image isn’t updating!

You could add images for the other pen types to the asset catalog, but this solution isn’t scalable. What if you wanted to add more pen types later, perhaps triggered by an in-app purchase or as a reward after unlocking extra functionality?

Instead, you’ll add functionality to generate the list of available pens at run time and load remote images from a server.

Add a new file using File ▸ New ▸ File…. Select an Empty file underneath the Other heading. Click Next, name your file pens.yml and click Create.

In the file menu, iOS template and Empty file selected with a blue Next button at the bottom of the screen

This file will contain the data about the available pens. In a real project, you’d download this from an API running on a server, but to keep things simple you’re including it in your app directly.

Open pens.yml and add the following text:

- key: sharpie
  label: Sharpie
  url: https://koenig-media.raywenderlich.com/uploads/2019/12/sharpie-1.png
- key: fountain
  label: Fountain Pen
  url: https://koenig-media.raywenderlich.com/uploads/2019/12/fountainPen-2.png
- key: biro
  label: Biro
  url: https://koenig-media.raywenderlich.com/uploads/2019/12/biro-1.png

YAML stands for Yet Another Markup Language. It’s an extensive and human readable format for structured data. Even if you haven’t seen YAML before, it should be fairly easy to understand. Each new pen starts with a dash character and contains three properties: a key, label and url.

But how to parse the YAML? You could write your own YAML parsing code, but surely somebody has already done this. This is where SPM comes in.

Adding Open Source Packages

Yams is an open source YAML parser that’s already done all the hard work.

In Xcode, select File ▸ Swift Packages ▸ Add Package Dependency…. Copy the following and paste into the combined search/input box:

https://github.com/jpsim/Yams.git

This is the location of the GitHub repository containing the package. Click Next.

Screen to with Add a Hosted Account text, a field to enter a url and a next button at the bottom.

The next screen asks you to define the package options for this package. For now, select the default which accepts any version of the package up to the next major version. (As of writing this is therefore from 2.0.0, inclusive, to 3.0.0, exclusive.) Click Next.

Choose Package Options screen with Version selected under Rules and Up to next Major and 2.0.0 selected in dropdowns

Xcode downloads the code from GitHub and adds the Yams package to the PenOfDestiny target. Click Finish.

Note that there’s now a Swift Package Dependencies section in the Project Navigator on the left and a Swift Packages tab in the Project settings.

Project Navigator screen with Yams 2.0.0 file circled under Swift Package Dependencies and Swift Packages in the main field

Select the PenOfDestiny target and note that Yams is automatically linked as a framework to your project target. Neat!

In Project Navigator Yams is listed under Frameworks, Libraries and Embedded Content

Build and run just to check that everything is still working. Yams will now be built as well and linked to your app.

Using Packages

Now that Yams is available in your project, it’s time to use it. Open SettingsStore.swift. This class defines a simple data store that saves the selected pen to UserDefaults as well as a function to get all available pens and the array of pens themselves.

At the top of the file, after importing Foundation, import the Yams library:

import Yams

Next, delete the availablePens static variable and replace the contents of getAvailablePens with the following:

// 1
if let path = Bundle.main.path(forResource: "pens", ofType: "yml") {
  do {
    // 2
    let yamlString = try String(contentsOfFile: path)
    // 3
    let decoder = YAMLDecoder()
    return try decoder.decode([Pen].self, from: yamlString)
  } catch {
    print("Could not load pen JSON!")
  }
}
// 4
return []

In this code, you:

  1. Obtain the path of the YAML file you created earlier.
  2. Fetch the contents of that file.
  3. Use the YAMLDecoder class provided by Yams to decode the YAML file and return an array of type Pen.
  4. Return an empty array if an error occurs along the way.

Build and run the app. Navigate to the settings screen and confirm that the three pens from before are still available.

Pen of Destiny settings screen, list of three pen types.

You haven’t added any user facing changes to your app yet, but now the list of available pens is determined at run time rather than compile time. Furthermore, your app is running code you didn’t write and haven’t seen.

Package Versioning

No code is perfect and all code changes over time, either to add new functionality or to fix bugs. Now that your app depends on somebody else’s code, how do you know when updates are available? And more importantly, how do you know if an update is likely to break your project?

All Swift packages should adhere to semantic versioning. Every time a package is released it has a version number, made up of three sections in the following format: major.minor.patch. For example, if a package is currently at version 3.5.1, that would be major version 3, minor version 5 and patch version 1.

Package authors should increment the patch version when they make a backward compatible bug fix that doesn’t change any external API. The minor version is for backward compatible additional functionality, such as adding a new method. And finally, the major version should change whenever incompatible API changes are introduced into a package.

In this manner, you should be able to update to the latest version of a package with the same major version number you currently use, without breaking your app.

For more information, check out the semver website.

Updating Package Dependencies

You can update to the latest version of any packages you depend on at any time by selecting File ▸ Swift Packages ▸ Update to Latest Package Versions.

Having just added the Yams package earlier in this tutorial, it’s unlikely a newer version is available. But if it was, Xcode would download the latest code and update your project to use it automatically.

Swift Package Structure

In the previous section, you learned that a Swift package is a collection of source code files and a manifest file called Package.swift. But what specifically goes into the manifest file?

Here’s an example of a typical Package.swift manifest file:

// 1
// swift-tools-version:5.0
// 2
import PackageDescription

// 3
let package = Package(
  // 4
  name: "YourPackageName",
  // 5
  platforms: [.iOS(.v13), .macOS(.v10_14)],
  // 6
  products: [
    .library(name: "YourPackageName", targets: ["YourPackageTarget"])
  ],
  // 7
  dependencies: [
    .package(url: "https://github.com/some/package", from: "1.0.0"),
  ]
  // 8
  targets: [
    .target(name: "YourPackageTarget"),
    .testTarget(
      name: "YourPackageTargetTests", 
      dependencies: ["YourPackageTarget"]
    )
  ]
)

Here’s a breakdown of each section:

  1. The first line of the manifest file must contain a formatted comment which tells SPM the minimum version of the Swift compiler required to build the package.
  2. Next, the PackageDescription library is imported. Apple provides this library which contains the type definitions for defining a package.
  3. Finally, the package initializer itself. This commonly contains the following:
  4. The name of the package.
  5. Which platforms it can run on.
  6. The products the package provides. These can be either a library, code which can be imported into other Swift projects, or an executable, code which can be run by the operating system. A product is a target that will be exported for other packages to use.
  7. Any dependencies required by the package, specified as a URL to the Git repository containing the code, along with the version required.
  8. And finally, one or more targets. Targets are modules of code that are built independently.

Code in Swift Packages

What about the code itself?

By convention, the code for each non-test target lives within a directory called Sources/TARGET_NAME. Similarly, a directory at the root of the package called Tests contains test targets.

In the example above, the package contains both a Sources and Tests directory. Sources then contain a directory called YourPackageTarget and Tests contain a directory called YourPackageTargetTests. These contain the actual Swift code.

You can see a real manifest file by looking inside the Yams package in Xcode. Use the disclosure indicator next to the Yams package to open its contents, then select Package.swift. Note how the Yams manifest file has a similar structure to above.

For the moment, Swift packages can only contain source code and unit test code. You can’t add resources like images.

However, there’s a draft proposal in progress to add functionality allowing Swift packages to support resources.

Updating the Pen Image

Now you’ll fix that bug in Pen of Destiny by setting the correct image based on which pen was selected in the settings.

Create a new Swift file in the project called RemoteImageFetcher.swift. Replace the code in the file with the following:

import SwiftUI

public class RemoteImageFetcher: ObservableObject {
  @Published var imageData = Data()
  let url: URL

  public init(url: URL) {
    self.url = url
  }

  // 1
  public func fetch() {
    URLSession.shared.dataTask(with: url) { (data, _, _) in
      guard let data = data else { return }
      DispatchQueue.main.async {
        self.imageData = data
      }
    }.resume()
  }

  // 2
  public func getImageData() -> Data {
    return imageData
  }

  // 3
  public func getUrl() -> URL {
    return url
  }
}

Given this isn’t a SwiftUI tutorial, I’ll go over this fairly briefly. In essence, this file defines a class called RemoteImageFetcher which is an observable object.

If you’d like to learn more about SwiftUI then why not check out our video course.

Observable objects allow their properties to be used as bindings. You can learn more about them here. This class contains three public methods:

  1. A fetch method, which uses URLSession to fetch data and set the result as the objects imageData.
  2. A method for fetching the image data.
  3. A method for fetching the URL.

The Remote Image View

Next, create a second new Swift file called RemoteImageView.swift. Replace its code with the following:

import SwiftUI

public struct RemoteImageView<Content: View>: View {
  // 1
  @ObservedObject var imageFetcher: RemoteImageFetcher
  var content: (_ image: Image) -> Content
  let placeHolder: Image

  // 2
  @State var previousURL: URL? = nil
  @State var imageData: Data = Data()

  // 3
  public init(
    placeHolder: Image,
    imageFetcher: RemoteImageFetcher,
    content: @escaping (_ image: Image) -> Content
  ) {
    self.placeHolder = placeHolder
    self.imageFetcher = imageFetcher
    self.content = content
  }

  // 4
  public var body: some View {
    DispatchQueue.main.async {
      if (self.previousURL != self.imageFetcher.getUrl()) {
        self.previousURL = self.imageFetcher.getUrl()
      }

      if (!self.imageFetcher.imageData.isEmpty) {
        self.imageData = self.imageFetcher.imageData
      }
    }

    let uiImage = imageData.isEmpty ? nil : UIImage(data: imageData)
    let image = uiImage != nil ? Image(uiImage: uiImage!) : nil;

    // 5
    return ZStack() {
      if image != nil {
        content(image!)
      } else {
        content(placeHolder)
      }
    }
    .onAppear(perform: loadImage)
  }

  // 6
  private func loadImage() {
    imageFetcher.fetch()
  }
}

This file contains a SwiftUI view that renders an image with either the data fetched from a RemoteImageFetcher or a placeholder provided during initialization. In detail:

  1. The remote image view contains properties to hold the remote image fetcher, the view’s content and a placeholder image.
  2. State to hold the a reference to the previous URL that was displayed and the image data.
  3. It is initialized with a placeholder image, a remote image fetcher and a closure that takes an Image.
  4. The SwiftUI body variable, which obtains the URL and image data properties from the fetcher and stores them locally, before returning…
  5. A ZStack containing either the image or the placeholder. This stack calls the private method loadImage when it appears, which…
  6. Requests the image fetcher to fetch the image data.

Finally, it’s time to use the remote image view in the app! Open SpinningPenView.swift. At the top of the body property add the following:

let imageFetcher = RemoteImageFetcher(url: settingsStore.selectedPen.url)

This creates an image fetcher to fetch data from the URL set on the selected pen.

Next, still inside body, find the following code:

Image("sharpie")
  .resizable()
  .scaledToFit()

And replace it with the following code:

RemoteImageView(placeHolder: Image("sharpie"), imageFetcher: imageFetcher) {
  $0
    .resizable()
    .scaledToFit()
}

The spinning pen view now uses your RemoteImageView in place of the default Image view.

Build and run your app. Tap the settings icon in the upper right of the screen and select a pen other than the Sharpie. Navigate back to the root view and note how the image updated to match the pen.

App start page with a ballpoint pen instead of a Sharpie

Local Packages

You now know how SPM helps you access open source packages written by other people. But did you know it also lets you split your code into packages for easier reuse among your projects.

Local packages differ from remote packages because you can edit their code directly within your project. That’s very useful when creating a new package that’s likely to change a lot before it’s ready for its first release.

In the previous section, you added the ability to download images from a remote URL. Since this functionality isn’t specific to Pen of Destiny, it’s a good candidate for factoring out into its own package.

In Xcode, select File ▸ New ▸ Swift Package…. Name the package RemoteImageView and add it to the Pen of Destiny project. Click Create.

Finder screen with Save As: RemmoteImageView and Add To: PenOfDestiny circled.

Helpfully, Xcode created the package structure for you, with a README.md, Package.swift manifest file and folders for the sources and tests.

Left navigation menu showing Readme and Package.swift files and source folders under RemoteImageView folder.

Open Package.swift and, after the name parameter, add the following line of code:

platforms: [.iOS(.v13)],

Here you tell the SPM that this package can only be built for iOS and only for versions 13 and higher.

In the RemoteImageView package, open Sources/RemoteImageView and delete the RemoteImageView.swift file that Xcode just created. Drag both the finished RemoteImageView and RemoteImageFetcher files from the Pen of Destiny project into the RemoteImageView directory.

In left navigation menu RemoteImageView and RemoteImageFetcher are now under the RemoteImageView folder.

Next, open RemoteImageViewTests.swift from within the Tests/RemoteImageViewTests directory and delete all the code within the RemoteImageViewTests class. It would be great to have tests but that is beyond the scope of this tutorial!

Adding the New Package to the App

Open the PenOfDestiny target settings. In the General tab under Frameworks, Libraries, and Embedded Content, select the + icon and add the RemoteImageView library to your project.

Dialog for adding libraries showing RemoteImageView library selected in dropdown under RemoteImageView file.

Finally, open SpinningPenView.swift. At the top, after importing SwiftUI, add the following:

import RemoteImageView

Build and run the app. Nothing has changed, but now the remote image view code is in its own package.

Publishing Packages

Xcode makes publishing your own packages really easy. You’re now going to see how to do that by publishing the remote image view library you just created.

First, create a Git repository for your package. Select the README.md file and add a brief description of the remote image view library (you can put what you want in here!). Then select Source Control ▸ Create Git Repositories… from the menu bar in Xcode.

Select only the RemoteImageView project and click Create.

Box in front of RemoteImageView folder has blue checked box.

This creates a Git repository on your computer and an initial commit of your code. You can verify this by opening the RemoteImageView directory in a terminal:

(base) Toms-MBP:RemoteImageView % ls -lA
total 24
drwxr-xr-x  12 matt  staff   384  2 Feb 20:53 .git
-rw-r--r--   1 matt  staff    53  2 Feb 20:47 .gitignore
drwxr-xr-x   3 matt  staff    96  2 Feb 20:47 .swiftpm
-rw-r--r--@  1 matt  staff  1090  2 Feb 20:47 Package.swift
-rw-r--r--@  1 matt  staff    43  2 Feb 20:52 README.md
drwxr-xr-x   3 matt  staff    96  2 Feb 20:47 Sources
drwxr-xr-x   4 matt  staff   128  2 Feb 20:47 Tests

(base) Toms-MBP:RemoteImageView % git st
On branch master
nothing to commit, working tree clean

(base) Toms-MBP:RemoteImageView % git log
commit 8045920909fe3c3fd9517dc43a53c79c6b22351b (HEAD -> master)
Author: Matt Galloway <matt@galloway.me.uk>
Date:   Sun Feb 2 20:53:08 2020 +0000

    Initial Commit

Pushing Your Package to GitHub

Before you can publish your package for others to use, it must be available publicly. The easiest way to do this is to publish to GitHub.

If you don’t have a GitHub account you can create one for free at github.com. Then, if you haven’t done so already, add your GitHub account to Xcode by selecting Xcode ▸ Preferences in the Xcode menu and then selecting Accounts.

Click the + button to add a new account. Select GitHub and fill in your credentials as requested.

Dialog box with GitHub selected and a Continue button.

Open the Source Control navigator and select the RemoteImageView package. Open the context menu (Right-click or Control-click) and select Create “RemoteImageView” Remote…. You can change the visibility to Private or accept the default settings. Click Create.

Pop up menu with Create RemoteViewImage selected.

This creates a new repository on GitHub and automatically pushes the code there for you.

Next, create a Tag for your package, again using the context menu. This time, select Tag master…. Tag as version 1.0.0 and click Create.

Dialog box with 1.0.0 in Tag field.

Finally, select Source Control ▸ Push… from the Xcode menu bar. Make sure Include tags is selected then click Push. This pushes the tag to GitHub, where the SPM can read it. Version 1.0.0 of your package is now live. :]

Push local changes dialog box with Include Tags selected.

Converting Local Packages

Now that your remote image view package is on GitHub and released as version 1.0.0, you can add it to your app as a remote package.

Open the context menu for the remote image view package and select View on GitHub…. This opens a browser window and loads the newly created GitHub repository for you.

Select Clone or Download and copy the Git repository URL in the pop-up.

Clone with SSH pop up dialog with the url in the field.

Next, go back to Xcode. Open the Project navigator and select the Package.swift for the RemoteImageView package. Open the context menu and select Show in Finder.

Your computer switches to Finder with the enclosing folder open. You don’t need this yet, but it’ll come in handy later, so make sure you keep this Finder window open. Switch back to Xcode.

Select the RemoteImageView package and delete it using Edit ▸ Delete from the menu bar. Then select Remove Reference.

Pop up asking how you want to remove with Remove Reference selected.

Re-Adding as a Remote Package

Select File ▸ Swift Packages ▸ Add Package Dependency… and paste the Git repository URL you copied above in the search bar. Click Next. Depending on your GitHub settings, you may need to authenticate your SSH key here.

Under Rules, select Up to next Major, set the version to 1.0.0 and then click Next. After Xcode fetches the package, ensure the RemoteImageView product is selected and added to the Pen of Destiny app target, then select Finish.

Dialog box with RemoteImageView library selected and Finish button.

Note how the package now displays next to Yams as a remote package in the Xcode package explorer.

Xcode package explorer with circle around RemoteImageView under Yams.

Build and run your app to make sure everything runs as before.

Updating Packages

Say you want to add some new functionality to the RemoteImageView package. How should you go about this?

Open RemoteImageFetcher.swift from the Swift Package Dependencies section of the Project Navigator. If you try to edit the file, nothing happens because you’re using a remote version of the package.

What to do?

Switch to Finder to view the window you opened in the previous section, showing the root directory for the RemoteImageView package. Double click Package.swift.

The package opens in Xcode in its own window. This time you can edit the sources files!

Xcode window showing editable source files.

Open RemoteImageFetcher.swift. A ticket has come in, requesting that users of your package can purge the image data from the image fetcher, perhaps to free memory when things get tight. Add the following after getUrl():

public func purge() {
  imageData = Data()
}

This method sets the image data back to an empty Data struct.

Now, to publish your change, select Source Control ▸ Commit from Xcode’s menu bar, add a commit message and click Commit.

Commit version 1.1.0

Next, add a new tag as before. Open the Source Control navigator, then open the context menu on the package and select Tag “master”.

Pop up context menu on RemoteImageView source control navigator with Tag master selected.

But what version should you use for the new version? Remember semantic versioning from before?

In this instance, you’ve added a new method, but the package is backward compatible with the previous version. So, this requires a minor version increase. Tag your package as 1.1.0 and select Create.

Finally, select Source Control ▸ Push from the menu bar in Xcode, remember to select Include tags and select the Push button to push your changes to GitHub.

Importing Your Updated Package

In the menu bar, select Window ▸ PenOfDestiny to switch back to the app project. Look for the remote image view package in the Project Navigator and notice how the version number, 1.0.0, is displayed against the package.

Xcode screen with circle showing RemoteImageView is still version 1.0.0

Select File ▸ Swift Packages ▸ Update to Latest Package Versions in the Xcode menu and note how the remote image view package is now version 1.1.0.

Xcode screen showing just RemoteImageView package at version 1.1.0

Making Breaking Changes

Select Window ▸ RemoteImageView to switch back to the remote image view package window. Open RemoteImageFetcher.swift.

The initializer for the remote image fetcher isn’t as Swifty as it could be. Replace it with the following:

public init(from url: URL) {
  self.url = url
}

This new initializer simply adds an argument label, from, to the URL parameter.

Build the package and ensure it builds correctly, making sure you have an iOS device or simulator selected as build destination.

Follow the steps as before, committing the changes and adding a tag. Only this time, because the public API for RemoteImageFetcher has changed, you must create a new major version: 2.0.0. Push the changes to GitHub, remembering to include tags.

Switch back to the PenOfDestiny app and run Update to Latest Package Versions again. This time, nothing happens and RemoteImageFetcher is still set to 1.1.0.

That’s because you set the maximum version to be exclusive of 2.0.0 when you initially added the package to your project. Open the Swift Packages tab in the Project Settings.

Double click RemoteImageView, update the dependency to 2.0.0 and click Done. This time, when you update to the latest package versions, the RemoteImageView package shows 2.0.0.

Dialog box with Version selected under Rules and Up to next Major in the drop down and 2.0.0 in the field.

But remember, this was a breaking change. So, you have to update your code accordingly. Open SpinningPenView.swift, locate the line with the image fetcher initialized and replace url with from as you did before.

Where to Go From Here?

I hope you enjoyed learning how you can use Swift Package Manager in your iOS apps. Now you can split your code into self-contained packages and publish them for everyone to use.

If you’d like to learn more about Swift Package Manager, check out its documentation.

There are some great videos from WWDC about the SPM. I found this one and this one really useful.

Finally, the full documentation for the manifest file is available here.

If you have any questions or comments, please join the forum discussion below.

Average Rating

5/5

Add a rating for this content

7 ratings

More like this

Contributors

Comments