A new feature for Swift 5.3 is Package Manager Resources which allows files such as .md, .png, .jpg, .txt and .pdf to be explicitly designated as resources and included as part of the built product for a SPM module. The following is a demonstration of Package Manager Resources with a SPM CLI application called Dapper, that bundles files as templates and outputs these files with dynamic text inserted.

A Swift Package Manager executable application is created with an internal library call DapperCore. A directory called Templates is created under the DapperCore directory. Inside the Templates directory there is a file called Index.dapper (its content is HTML). Below is the full file structure for the application.

$ tree
.
├── Package.swift
├── README.md
├── Sources
│   ├── Dapper
│   │   └── main.swift
│   └── DapperCore
│       ├── DapperCore.swift
│       └── Templates
│           └── Index.dapper
└── Tests
    ├── DapperCoreTests
    │   ├── DapperCoreTests.swift
    │   └── XCTestManifests.swift
    ├── DapperTests
    │   ├── DapperTests.swift
    │   └── XCTestManifests.swift
    └── LinuxMain.swift

The Package Manifest explicitly designates the ‘Templates’ directory as a resource to be processed for the ‘DapperCore’ library target. Processing performs platform specific optimisations. The alternate to process is copy. Individual files can also be specified.

// Package.swift

// swift-tools-version:5.3
import PackageDescription

let package = Package(
    name: "Dapper",
    dependencies: [
    ],
    targets: [
        .target(
            name: "Dapper",
            dependencies: ["DapperCore"]),
        .target(
            name: "DapperCore",
            dependencies: [],
            resources: [.process("Templates")]),
        .testTarget(
            name: "DapperTests",
            dependencies: ["Dapper"]),
        .testTarget(
            name: "DapperCoreTests",
            dependencies: ["DapperCore"]),
    ]
)

Compiling the application will generate an extension on Bundle called ‘module’ that provides internal scope access to resources in the specified target.

Access the file path for ‘Index.dapper’ using the ‘module’ function on Bundle.

// DapperCore.swift

public func run() throws -> String {
    guard let filepath = Bundle.module.path(forResource: "Index", ofType: "dapper") else {
        throw ValidationError.templateNotFound
    }

    let contents = try String(contentsOfFile: filepath)
    return contents.replacingOccurrences(of: "{{title}}", with: "Hello world")
}

The contents of the template are output to the commandline, with the text “Hello world” inserted.

$ swift run
<!DOCTYPE html>
<html>
<body>
<h1>Hello world</h1>
</body>
</html>

You can learn more about new features in Swift and what’s happening with Swift Evolution from the Swift Weekly Brief.

Source code available on Github.

Written by Mark Brownsword

I've been working as an enterprise developer for more than 15 years, always using .NET platform and recently building for iOS with Swift. My degree is in Business Studies, majoring in information systems, where I learnt the fundamentals of software engineering for building systems for business.

%d bloggers like this: