Episode #420

Swift Package Manager

21 minutes
Published on December 6, 2019

This video is only available to subscribers. Get access to this video and 585 others.

Now that Swift Package Manager has started to see some adoption and we have some integration inside of Xcode, I think it's time to take a deeper look at how to use it both as a consumer and as a library author. In this episode we'll create a DiceKit library using SwiftPM, then use it in a command line utility.

Episode Links

Creating our Own Packages

Let's assume we want to make a dice rolling library for others to use.

We're going to make a folder to put our library in called DiceKit:

$ mkdir DiceKit
$ cd DiceKit

Then we can create our project:

$ swift package init

This will use the name of our folder as the name of the package. If you want to change this, you can add the --name flag and specify something else.

Note that in this folder there is no xcodeproj. This is because Swift packages leverage settings from Package.swift as well as files on the filesystem to determine what the package contains.

Building Packages

Since there is no Xcode project, you use the command line to build packages:

$ swift build

If you inspect the file system now there's a hidden/ignored folder called .build which contains all the built artifacts.

Let's create a simple type we can expose in this library. Create a file in Sources/DiceKit called Die.swift.

public struct Die {
    public let numberOfSides: Int

    public init(numberOfSides: Int) {
        self.numberOfSides = numberOfSides
    }

    public func roll() -> Int {
        return Int.random(in: 1...numberOfSides)
    }
}

Note that this file is already part of the target because we added it to the Sources/DiceKit folder.

There's no way for us to run this code yet because we just created a library. Let's create a command line executable that uses this library.

Creating a Command Line Swift Executable

Change to the parent directory and create a new one to contain our command-line application:

cd ..
mkdir DiceRoller
cd DiceRoller

Then we can initialize a new executable package:

$ swift init --type executable

Note that this creates a main.swift for us, and this is the entry point where our code starts to run when the app is executed.

Next let's add a dependency for the DiceKit library. We'll use a local path to refer to the package rather than a published version. In Package.swift:

import PackageDescription

let package = Package(
    name: "DiceRoller",
    dependencies: [
        .package(path: "../DiceKit")        
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages which this package depends on.
        .target(
            name: "DiceRoller",
            dependencies: ["DiceKit"]),
        .testTarget(
            name: "DiceRollerTests",
            dependencies: ["DiceRoller"]),
    ]
)

Now when we build it, it will pull in dependencies and build those too:

$ swift build
Completed resolution in 0.56s
[6/6] Linking DiceRoller

If we run the app we can see that it works:

$ .build/debug/DiceRoller
Hello, World!

Let's jump back over to main.swift and use our library:

import DiceKit

let die = Die(numberOfSides: 6)
let roll = die.roll()
print("🎲> You rolled a \(roll)")

Running swift build again, we can see that it works.

$ swift build
$ .build/debug/DiceRoller
🎲> You rolled a 4

Working in Xcode Projects

If we want to build & debug within Xcode we can, but we need to generate an Xcode project.

$ swift package generate-xcodeproj

Then if we open it we can run it, set breakpoints, etc.

Adding Packages from Xcode

If you already have an Xcode project and integrate some Swift packages you can do this from the File -> Swift Packages menu and enter in a URL to a library.

The best way that I've found to search for these packages is with SwiftPM.co.

This episode uses Xcode 11.2.1, Swift 5.1.