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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Swift Package Manager for iOS
30 mins
- Getting Started
- Customizing the App
- Adding Open Source Packages
- Using Packages
- Package Versioning
- Updating Package Dependencies
- Swift Package Structure
- Code in Swift Packages
- Updating the Pen Image
- The Remote Image View
- Local Packages
- Adding the New Package to the App
- Publishing Packages
- Pushing Your Package to GitHub
- Converting Local Packages
- Re-Adding as a Remote Package
- Updating Packages
- Importing Your Updated Package
- Making Breaking Changes
- Where to Go From Here?
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:
- 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.
- Next, the
PackageDescription
library is imported. Apple provides this library which contains the type definitions for defining a package. - Finally, the package initializer itself. This commonly contains the following:
- The name of the package.
- Which platforms it can run on.
- 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.
- Any dependencies required by the package, specified as a URL to the Git repository containing the code, along with the version required.
- 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:
- A
fetch
method, which usesURLSession
to fetch data and set the result as the objectsimageData
. - A method for fetching the image data.
- 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:
- The remote image view contains properties to hold the remote image fetcher, the view’s content and a placeholder image.
- State to hold the a reference to the previous URL that was displayed and the image data.
- It is initialized with a placeholder image, a remote image fetcher and a closure that takes an
Image
. - The SwiftUI
body
variable, which obtains the URL and image data properties from the fetcher and stores them locally, before returning… - A
ZStack
containing either the image or the placeholder. This stack calls the private methodloadImage
when it appears, which… - 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.