Swift Package Manager for iOS

Learn how to use the Swift Package Manager (SwiftPM) to create, update and load local and remote Swift Packages. By Tom Elliott.

4.8 (35) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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