DEV Community

Cover image for Swift 101: Building a Library with Swift Package Manager
Jake Barnby for Appwrite

Posted on

Swift 101: Building a Library with Swift Package Manager

The Swift Package Manager, or SwiftPM, is included with Swift 3.0 and above. Initially, it was only available for server-side or command line Swift projects. Since the release of Swift 5 and Xcode 11, SwiftPM is compatible with the Apple ecosystem for creating apps. This is great news because packages make it easier to divide your code into reusable, logical groups you can easily share across projects, or even with the entire world.

📦️ Modules

Before looking at packages, we first need to understand modules. Swift organises code into modules. Each module defines a namespace and which parts of the code can be used from outside the module. You can define all of your code in a single module, or break it up into multiple which can depend on each other. Using modules lets you easily build on your own re-usable code, or others code.

🎁 Packages

So what is a Swift package? A package is a collection of Swift source code files as well as a manifest file called Package.swift, that defines various properties about the package, such as its name, the products it produces, any dependencies it has, and the targets it is built up of.

🦴 Anatomy of a Package

  • Products define the libraries and executables produced by a package. A library is simply a collection of files, for use as a dependency by other Swift code. An executable is a package that can be run, such a web server.

  • Dependencies are other Swift Packages you want to use code from, within your package.

  • Targets are what defines the module(s) that a package contains and that other packages can import. Targets define their own dependencies and can depend on other targets the package, or products from packages that this package depends on.

⚙️ Creating a Swift Package

This tutorial assumes you already have Swift installed. You can check by running swift --help from your terminal.

Creating a Swift Package from the command line is easy, and can be completed with one simple command from the directory you want to create your package in. For this example we'll start with a directory named FooPackage.

$ mkdir FooPackage
$ cd FooPackage

FooPackage$ swift package init --type=library
Enter fullscreen mode Exit fullscreen mode

That’s it! There’ll be some output detailing the files created for your new package. You should see:

  • 1 source file created inside a Sources directory
  • 1 test source file inside a Tests directory
  • A Package.swift manifest file at the root level
  • A README.md file at the root level
  • A .gitignore file at the root level

Of these files, only the single file in the Sources directory and the manifest file are required for the package to build. This means you could easily create your own package by manually creating these two files as well.

By default, the Sources directory must contain all source code for the package, but you can use sub-directories to define sub-modules, if they are also defined as separate targets in your manifest file. Let's take a look at the generated Package.swift for the new package to see the pieces we've discussed so far:

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

import PackageDescription

let package = Package(
    name: "TestPackage",
    products: [
        .library(
            name: "FooPackage",
            targets: ["FooPackage"]),
    ],
    dependencies: [
    ],
    targets: [
        .target(
            name: "FooPackage",
            dependencies: [          
            ]
        )
        .testTarget(
            name: "FooPackageTests",
            dependencies: [     
                "FooPackage"     
            ]
        )
    ]
)
Enter fullscreen mode Exit fullscreen mode

Here you can see that our package defines one library, TestPackage, as well as one target of the same name, and one test target, with depends on the module target.

🥇 The first build

Now that the package has been created, let’s build it for the first time with the build command:

$ swift build
Enter fullscreen mode Exit fullscreen mode

Because the package has no dependencies or code yet, this should complete almost instantly, displaying “Build Completed!” on success.

➕ Adding dependencies

Let's add a dependency and some code. Adding dependencies with SwiftPM is easy as you can use git URL's directly. We can add the following to our Package.swift top level dependencies block to allow us to the Appwrite Swift SDK in our library:

.package(name: "Appwrite", url: "https://github.com/appwrite/sdk-for-swift", from: "0.1.0")
Enter fullscreen mode Exit fullscreen mode

This declares that our package will pull in the code from the Appwrite module in the sdk-for-swift repository on GitHub, from the tag 0.1.0 and allow us to add it to our targets dependencies as follows:

.target(
    name: "FooPackage",
    dependencies: [ 
        "Appwrite"    
    ]
)
Enter fullscreen mode Exit fullscreen mode

Here we added Appwrite, as this is the name of the library we're using from the sdk-for-swift repository.

Let's take a look at the full manifest file with the new dependency added:

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

import PackageDescription

let package = Package(
    name: "FooPackage",
    products: [
        .library(
            name: "FooPackage",
            targets: ["FooPackage"]),
    ],
    dependencies: [
        .package(name: "Appwrite", url: "https://github.com/appwrite/sdk-for-swift", from: "0.1.0")
    ],
    targets: [
        .target(
            name: "FooPackage",
            dependencies: [    
                "Appwrite"
            ]
        )
        .testTarget(
            name: "FooPackageTests",
            dependencies: [     
                "FooPackage"     
            ]
        )
    ]
)
Enter fullscreen mode Exit fullscreen mode

Since we've changed the dependencies of our package, we need to resolve them. This will happen automatically the first time you run swift build with a new dependency, but if you manually update a version, you'll need to manually resolve the new version. This can be done by running:

$ swift package resolve
Enter fullscreen mode Exit fullscreen mode

This will update the Package.resolved to contain the version metadata about the Appwrite module we just added.

What's going on here?

Swift Package Manager uses a lockfile system similar to package.lock for NPM and composer.lock for Composer. This comes in the form of a file called Package.resolved, which contains metadata about the packages dependencies versions, as well as their transitive dependencies. When you run swift build and the dependencies are fetched, the versions from the Package.resolved file will be used if found.

Once resolved, we can build our package with swift build again. This time we'll see the sdk-for-swift repository pulled into the build checkouts, as well as built with the rest of the library.

📥️ Adding library code

Time to add some code. Let's open up the source file created earlier as Sources/FooPackage/FooPackage.swift and update with the following:

import Appwrite

struct FooPackage {

    static let client = Client()
    static let account = Account(client)

    public static func login(
        endpoint: String,
        projectId: String,
        email: String,
        password: String,
        completion: @escaping (Result<Session, AppwriteError>) -> Void
    ) {
        client
            .setEndpoint(endpoint)
            .setProject(projectId)

        account.createSession(
            email: email,
            password: password,
            completion: completion
        )
    }
}

Enter fullscreen mode Exit fullscreen mode

We now have a login function! We just need to deploy the package, and we'll be able to use the login function from any other package or Apple app.

🧑‍💻 Deploying the package

Fortunately deploying packages with Swift Package Manager is very easy. As the packages are Git based, all you need to do is push your changes to your default branch and create a tag for your release:

$ git init
$ git add .
$ git remote add origin [GitHub Repository URL]
$ git commit -m "Initial Commit"
$ git tag 1.0.0
$ git push origin main --tags
Enter fullscreen mode Exit fullscreen mode

📥️ Using as a dependency

Using the same method we used earlier to add the Appwrite Apple SDK as a dependency, we can now add the newly deployed package as a dependency of a second package:


    ...

    dependencies: [
        .package(name: "FooPackage", url: "https://github.com/[YOUR GITHUB USERNAME]/[YOUR GITHUB REPOSITORY]", from: "1.0.0")
    ],
    targets: [
        .target(
            name: "FooPackage",
            dependencies: [    
                "FooPackage"  
            ]
        )
    ]

    ...

)
Enter fullscreen mode Exit fullscreen mode

🏗️ Using the dependency

With the package added as a dependency, we can now use the function we defined earlier anywhere in the second package:

import FooPackage

FooPackage.login(
    endpoint: "http://localhost/v1",
    projectId: "6bfgh45fng3",
    email: "test@test.test",
    password: "password"
) { result in 
    ...
}
Enter fullscreen mode Exit fullscreen mode

🔽 Updating your package

To update your package, the process is the same as deploying the initial version. You just need to push your changes to the default branch and a new version tag to go with.

✅ That's it!

You've now created, deployed, used and updated your very own Swift Package! Packages are a great way to re-use code and share your creations with the world. Looking forward to seeing what new packages come next for Swift!

📚️ Resources

Discussion (0)