Swift package manager

  • Tutorial

Along with the release in the open source Swift language on December 3, 2015, Apple introduced the decentralized dependency manager Swift Package Manager .

The notorious Max Howell , creator of Homebrew, and Matt Thompson , who wrote AFNetworking, had a hand in the public version . SwiftPM is designed to automate the dependency installation process, as well as further testing and building the Swift project on all available operating systems, but so far only macOS and Linux support it . If interested, go under the cat.

The minimum requirements are Swift 3.0. To open the project file, Xcode 8.0 or higher is required. SwiftPM allows you to work with projects without an xcodeproj file, so Xcode on OS X is optional, but on Linux it isn’t.

It is worth dispelling doubts - the project is still in active development. Using UIKit, AppKit, and other iOS and OS X SDK frameworks as dependencies is not available, since SwiftPM connects the dependencies in the form of source code, which it then collects. Thus, using SwiftPM on iOS, watchOS and tvOS is possible, but only using Foundation and dependencies of third-party libraries from open access. One single import UIKit makes your library unusable through SwiftPM.

All examples in the article are written using version 4.0.0-dev, you can check your version using the command in the terminal

swift package —version

Ideology Swift Package Manager


To work on the project, the * .xcodproj file is no longer needed - now it can be used as an auxiliary tool. Which files are involved in the assembly of the module depends on their location on the disk - for SwiftPM the names of directories and their hierarchy inside the project are important. The initial structure of the project directory is as follows:

  • Sources - source files for assembling the package, divided internally into product directories - each product has a separate folder.
  • Tests - tests for the developed product, the division into folders is similar to the Sources folder.
  • Package.swift - a file with a description of the package.
  • README.md - package documentation file.

Inside the Sources and Tests folders, SwiftPM recursively searches for all * .swift files and associates them with the root folder. A little later we will create subfolders with files.



Main components


Now let's look at the main components in SwiftPM:

  • Module - a set of * .swift – files that performs a specific task. One module can use the functionality of another module, which it connects as a dependency. A project can be assembled on the basis of a single module. Dividing the source code into modules allows you to select a function in a separate module that can be reused when building another project. For example, a network query module or a database module. The module uses the encapsulation threshold of the internal level and is a library (library), which can be connected to the project. A module can be connected both from the same package (presented as a different target) and from another package (presented as a different product).
  • Product (Product) - the result of the assembly target (target) of the project. It can be a library (library) or an executable file (executable). The product includes the source code that relates directly to this product, as well as the source code of the modules on which it depends.
  • Package (Package) - a set of * .swift – files and the manifest file Package.swift, which defines the name of the package and the set of source files. A package contains one or more modules.
  • Dependency is the module required for the source code in a package. The dependency should have a path (relative local or remote to the git repository), version, list of dependencies. SwiftPM must have access to the source code of the dependency to compile and connect to the main module. A target dependency can be a target from the same package or from a dependency package.



We get that the dependencies are arranged in a graph - each dependency can have its own and so on. Resolution of the dependency graph is the main task of the dependency manager.

I note that all source files must be written in Swift, there is no possibility to use Objective-C.

Each package must be self-contained and isolated. Its debugging is performed not by means of start (run), but by means of logical tests (test).

The following is a simple example of connecting an Alamofire dependency to a project.

Test project development


We’ll go through the terminal to the folder where our project will be located, create a directory for it and go to it.

mkdir IPInfoExample
cd IPInfoExample/

Next, initialize the package using the command

swift package init

As a result, the following hierarchy of source files is created.


├── Package.swift
├── README.md
├── Sources
│   └── IPInfoExample
│       └── main.swift
└── Tests
     └── IPInfoExampleTests
         ├ LinuxMain.swift
         └── IPInfoExampleTests
             └── IPInfoExampleTests.swift

In the absence of an index of the * .xcodeproj project file, the dependency manager needs to know which source files should be involved in the assembly process and in which targets to include them. Therefore, SwiftPM defines a strict hierarchy of folders and a list of files:

  • Package file;
  • README file;
  • Sources folder with source files - a separate folder for each target;
  • Tests folder - a separate folder for each test target.

We can now execute the commands


swift build
swift test

to build a package or to run a test Hello, world!

Adding source files


Create an Application.swift file and put it in the IPInfoExample folder.

public struct Application {}


We perform swift build and see that 2 files are already compiled in the module.

Compile Swift Module 'IPInfoExample' (2 sources)

Create the Model directory in the IPInfoExample folder, create the IPInfo.swift file, and delete the IPInfoExample.swift file as unnecessary.


//Используем протокол Codable для маппинга JSON в объект
public struct IPInfo: Codable { 
    let ip: String
    let city: String
    let region: String
    let country: String
}

After that, run the swift build command to verify.

Add Dependencies


Let's open the Package.swift file, the content fully describes your package: package name, dependencies, target. Add the Alamofire dependency.

// swift-tools-version:4.0
import PackageDescription // Модуль, в котором находится описание пакета
let package = Package(
    name: "IPInfoExample", // Имя нашего пакета
    products: [
        .library(
            name: "IPInfoExample",
            targets: ["IPInfoExample"]),
    ],
    dependencies: [
        // подключаем зависимость-пакет Alamofire, указываем ссылку на GitHub
        .package(url: "https://github.com/Alamofire/Alamofire.git", from: "4.0.0") 
    ],
    targets: [
        .target(
            name: "IPInfoExample",
            // указываем целевой продукт – библиотеку, которая зависима 
            // от библиотеки Alamofire
            dependencies: ["Alamofire"]), 
        .testTarget(
            name: "IPInfoExampleTests",
            dependencies: ["IPInfoExample"]),
    ]
)

Then again, swift build, and our dependencies are downloaded, a Package.resolved file is created with a description of the installed dependency (similar to Podfile.lock).

If your package has only one product, you can use the same names for the package name, product, and target. We have this IPInfoExample. Thus, the package description can be shortened by omitting the products parameter. If you look at the description of the Alamofire package, you will see that the targets are not described there. By default, one target is created with the package name and source code files from the Sources folder and one target with the package description file (PackageDescription). When using SwiftPM, the test target is not involved, therefore the test folder is excluded.


import PackageDescription
let package = Package(name: "Alamofire", dependencies : [], exclude: [“Tests"])

To make sure that the modules, targets, product are correctly created, we can run the command

swift package describe

As a result, for Alamofire we get the following log:


Name: Alamofire
Path: /Users/ivanvavilov/Documents/Xcode/Alamofire
Modules:
    Name: Alamofire
    C99name: Alamofire
    Type: library
    Module type: SwiftTarget
    Path: /Users/ivanvavilov/Documents/Xcode/Alamofire/Source
    Sources: AFError.swift, Alamofire.swift, DispatchQueue+Alamofire.swift, MultipartFormData.swift, NetworkReachabilityManager.swift, Notifications.swift, ParameterEncoding.swift, Request.swift, Response.swift, ResponseSerialization.swift, Result.swift, ServerTrustPolicy.swift, SessionDelegate.swift, SessionManager.swift, TaskDelegate.swift, Timeline.swift, Validation.swift

If a package has several products, then as a dependency we specify a dependency package, and already in a target dependency we indicate a dependence on the package module. For example, this is how SourceKitten is connected in our Synopsis library .

import PackageDescription
let package = Package(
    name: "Synopsis",
    products: [
        Product.library(
            name: "Synopsis",
            targets: ["Synopsis"]
        ),
    ],
    dependencies: [
        Package.Dependency.package(
            // зависимость от пакета SourceKitten
            url: "https://github.com/jpsim/SourceKitten", 
            from: "0.18.0"
        ),
    ],
    targets: [
        Target.target(
            name: "Synopsis",
            // зависимость от библиотеки SourceKittenFramework
            dependencies: ["SourceKittenFramework"] 
        ),
        Target.testTarget(
            name: "SynopsisTests",
            dependencies: ["Synopsis"]
        ),
    ]
)

This is the description of the SourceKitten package. The package describes 2 products


.executable(name: "sourcekitten", targets: ["sourcekitten"]),
.library(name: "SourceKittenFramework", targets: ["SourceKittenFramework"])

Synopsis uses the SourceKittenFramework product library.

Create project file


We can create a project file for our convenience by running the command

swift package generate-xcodeproj

and as a result, we get the IPInfoExample.xcodeproj file in the project root folder.
Open it, see all the sources in the Sources folder, including those with the Model subfolder, and the dependency sources in the Dependencies folder.

It is important to note that this step is optional during product development and does not affect the SwiftPM operation mechanism. Note that all source files are located in the same way as on disk.



Checking the connected dependency


Check if the dependency is connected correctly. In the example, we make an asynchronous request to the ipinfo service to obtain data about the current ip-address. We decode the response JSON into a model object - the IPInfo structure. For simplicity, we will not handle the JSON mapping error or server error.


// импортируем библиотеку так же, как при использовании cocoapods или carthage 
import Alamofire 
import Foundation
public typealias IPInfoCompletion = (IPInfo?) -> Void
public struct Application {
    public static func obtainIPInfo(completion: @escaping IPInfoCompletion) {
        Alamofire
            .request("https://ipinfo.io/json")
            .responseData { result in
                var info: IPInfo?
                if let data = result.data {
                    // Маппинг JSON в модельный объект
                    info = try? JSONDecoder().decode(IPInfo.self, from: data)
                }
                completion(info)
        }
    }
}

Next, we can use the build command in Xcode, and we can execute the swift build command in the terminal.

Project with executable file


Above is an example for initializing a library project. SwiftPM allows you to work with an executable file project. To do this, when initializing, use the command

swift package init —type executable.

You can also bring the current project to this form by creating the main.swift file in the Sources / IPInfoExample directory. When you run the executable, main.swift is the entry point.
Let's write one line in it

print("Hello, world!”)

And then run the swift run command, the cherished sentence will be displayed in the console.

Package Description Syntax


The description of the package in general is as follows:


Package(
    name: String,
    pkgConfig: String? = nil,
    providers: [SystemPackageProvider]? = nil,
    products: [Product] = [],
    dependencies: [Dependency] = [],
    targets: [Target] = [],
    swiftLanguageVersions: [Int]? = nil
)

  • name - the name of the package. The only required argument for the package.
  • pkgConfig - used for module packages installed in the system (System Module Packages), defines the name of the pkg-config file .
  • providers - used for system module packages, describes hints for installing missing dependencies through third-party dependency managers - brew, apt, etc.


import PackageDescription
let package = Package(
    name: "CGtk3",
    pkgConfig: "gtk+-3.0",
    providers: [
        .brew(["gtk+3"]),
        .apt(["gtk3"])
    ]
)

  • products - a description of the result of the assembly of the project target - an executable file or library (static or dynamic).


let package = Package(
    name: "Paper",
    products: [
        .executable(name: "tool", targets: ["tool"]),
        .library(name: "Paper", targets: ["Paper"]),
        .library(name: "PaperStatic", type: .static, targets: ["Paper"]),
        .library(name: "PaperDynamic", type: .dynamic, targets: ["Paper"])
    ],
    targets: [
        .target(name: "tool")
        .target(name: "Paper")
    ]
)

There are 4 products described in the package above: the executable file from the tool target, the Paper library (SwiftPM will select the type automatically), the static PaperStatic library, the dynamic PaperDynamic from one Paper target.

  • Dependencies – описание зависимостей. Необходимо указать путь (локальный или удаленный) и версию.

    Управление версиями в SwiftPM происходит через git-тэги. Само версионирование можно настроить достаточно гибко: зафиксировать версию языка, git-ветки, минимальную мажорную, минорную версию пакета или хэш коммита. Опционально к тэгам добавляется суффикс вида @swift-3, таким образом можно поддерживать старые версии. Например, с версиями вида 1.0@swift-3, 2.0, 2.1 для SwiftPM версии 3 будет доступна только версия 1.0, для последней версии 4 – 2.0 и 2.1.
    Также есть возможность указать поддержку версии SwiftPM для manifest-файла, указав суффикс в имени package@swift-3.swift. Указание версии можно заменить на ветку или хэш коммита.


// 1.0.0 ..< 2.0.0
.package(url: "/SwiftyJSON", from: "1.0.0"),
// 1.2.0 ..< 2.0.0
.package(url: "/SwiftyJSON", from: "1.2.0"),
// 1.5.8 ..< 2.0.0
.package(url: "/SwiftyJSON", from: "1.5.8"),
// 1.5.8 ..< 2.0.0
.package(url: "/SwiftyJSON", .upToNextMajor(from: "1.5.8")),
// 1.5.8 ..< 1.6.0
.package(url: "/SwiftyJSON", .upToNextMinor(from: "1.5.8")),
// 1.5.8
.package(url: "/SwiftyJSON", .exact("1.5.8")),
// Ограничение версии интервалом.
.package(url: "/SwiftyJSON", "1.2.3"..<"1.2.6"),
// Ветка или хэш коммита.
.package(url: "/SwiftyJSON", .branch("develop")),
.package(url: "/SwiftyJSON", .revision("e74b07278b926c9ec6f9643455ea00d1ce04a021"))

  • targets - description of targets. In the example, we declare 2 targets, the second - for tests of the first, in the dependencies we indicate the tested one.


let package = Package(
    name: "FooBar",
    targets: [
        .target(name: "Foo", dependencies: []),
        .testTarget(name: "Bar", dependencies: ["Foo"])
    ]
)

  • swiftLanguageVersions - Description of the supported version of the language. If version [3] is installed, swift 3 and 4 compilers will select version 3; if version [3, 4], swift 3 compiler selects the third version, swift 4 compiler will select the fourth.

Team Index


swift package init //инициализация проекта библиотеки
swift package init --type executable //инициализация проекта исполняемого файла
swift package --version //текущая версия SwiftPM
swift package update //обновить зависимости
swift package show-dependencies //вывод графа зависимостей
swift package describe // вывод описания пакета

Resources



Also popular now: