Quantcast
Viewing latest article 3
Browse Latest Browse All 9

The structure of a Swift Package

In the previous article, we talked about what SPM is and how we can use it. We briefly talked about packages, but we are missing a key point: how to create them. In this article, we’ll explore packages in more detail. If you don’t know what a package is, I recommend you first check the previous blog post before continuing.

Creating a package

We can use Xcode to create packages (in this article I’ll be using version 13.2.1). With Xcode running, Let’s go to File -> New -> Package:

Image may be NSFW.
Clik here to view.
Selecting the package creation menu.

Selecting this option will open the following window:

Image may be NSFW.
Clik here to view.
Package creation screen in Xcode.

Once we are good with the location and name of our package, we can click the create button, and SPM will create our Swift package with a default folder structure.

Folder structure

Image may be NSFW.
Clik here to view.
The folder structure of our package.

When created, a package comes with a default folder structure. Each file and folder has a purpose:

  • README.md: Describes the package to humans
  • Package.swift: A manifest file defining what the package is
  • Sources: Folder containing the source files of our package
  • Tests: Folder containing the unit test suites covering the code from the Sources folder

The manifest file

Every Swift package has a manifest (named Package.swift) identifying it. A manifest contains important information about a package:

  • Its name
  • The platforms it supports
  • The targets it consists of
  • Its dependencies
  • The products it distributes (libraries)

SPM uses this information to manage our packages. The following code comes from our Package.swift file:

// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "Simple-Package",
    products: [
        .library(
            name: "Simple-Package",
            targets: ["Simple-Package"]),
    ],
    dependencies: [
        // .package(url: /* package url */, from: "1.0.0"),
    ],
    targets: [
        .target(
            name: "Simple-Package",
            dependencies: []),
        .testTarget(
            name: "Simple-PackageTests",
            dependencies: ["Simple-Package"]),
    ]
)

Let’s understand the most important attributes of a package:

Targets

A package is divided into targets. They are the building blocks of a package. Each target has a specific purpose and defines its module:

targets: [
    .target(name: "Simple-Package", dependencies: []),
    // More targets can be defined here.
]

Targets have their own folder under Sources, containing their source code. They can also depend on external code or on other targets as well. This allows us to modularize our code if needed.

Test targets

We can write and run automated tests for the code contained in a target. All we have to do is define a test target with the corresponding folder and test suites (notice how it uses our Simple-Package as a dependency):

targets: [
    // ...
    .testTarget(name: "Simple-PackageTests", dependencies: ["Simple-Package"])
]

Products

The module defined by a target isn’t directly accessible to the clients of a package. It is initially internal. The only way to distribute the public API of a target is to define a product of the library type. It vends the module defined by its target, allowing the clients to use its public API via import:

products: [
    .library(name: "Simple-Package", targets: ["Simple-Package"]),
]

Dependencies

Packages can also depend on other packages. When we declare dependencies, we specify which versions we want:

dependencies: [
    .package(url: "https://github.com/apple/swift-algorithms.git", from: "1.0.0"),
]

Right after we edit our manifest, SPM will resolve our dependencies, the same process it does on a regular Xcode project:

Image may be NSFW.
Clik here to view.
The resolved dependencies (swift-algorithms depends on swift-numerics)

We can then use any products from swift-algorithms in our targets:

/// ...
.target(
    name: "Simple Package", 
    dependencies: [
        .product(name: "Algorithms", package: "swift-algorithms")
    ]
),
/// ...

Workflow

The workflow for packages using Xcode is pretty similar to the one for regular projects (e.g iOS or MacOS).

Adding files

To add swift files to a target, simply place them inside its folder. This file will then belong to the target’s module, which means we can add new code to it and access the other code already defined in there.

Image may be NSFW.
Clik here to view.
Adding a file to the `Simple Package` target.

Building and testing

Building is pretty simple. Just select the product from the list and build it. I’ve added some targets and products to exemplify:

Image may be NSFW.
Clik here to view.
Selecting a product to be built.

Here’s how the manifest looks like:

// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "Simple Package",
    products: [
        .library(
            name: "Simple Package",
            targets: ["Simple Package"]),
        .library(
            name: "Library1",
            targets: ["Target1"]),
        .library(
            name: "Library2",
            targets: ["Target2"]),
        .library(
            name: "Library3",
            targets: ["Target3"]),
    ],
    dependencies: [
        .package(url: "https://github.com/apple/swift-algorithms.git", from: "1.0.0"),
    ],
    targets: [
        .target(
            name: "Simple Package",
            dependencies: [
                .product(name: "Algorithms", package: "swift-algorithms")
            ]),
        .target(
            name: "Target1",
            dependencies: []),
        .target(
            name: "Target2",
            dependencies: []),
        .target(
            name: "Target3",
            dependencies: []),
        .testTarget(
            name: "Simple PackageTests",
            dependencies: ["Simple Package"]),
    ]
)

We also have to define the folders for each target:

Image may be NSFW.
Clik here to view.
The new folder structure in Sources.

Conclusion

We’ve learned how to build packages and what they look like in terms of structure. We explored what manifests are and how we can use them to define a package. We also looked at the key components a package has:

  • Dependencies
  • Targets
  • Products

The package we built doesn’t have any source code, just meaningless files. I encourage you to look at real-world examples, like the Time package we used in the previous post or the Algorithms package we used in this one.

In the next article, we’ll take a look at how we can publish and version our packages.


Viewing latest article 3
Browse Latest Browse All 9

Trending Articles