Skip to main content

Swift Package Basics

Introduction

Swift Package Manager (SPM) is a tool for managing the distribution and use of Swift code. It's integrated directly into the Swift build system, making it easy to share code and include dependencies in your projects. Swift packages provide a standardized way to organize, build, and share Swift code across different projects and with the Swift community.

In this tutorial, you'll learn the fundamental concepts of Swift packages, how to create one from scratch, and how to incorporate packages into your own projects.

What is a Swift Package?

A Swift package is essentially a collection of Swift source files and a manifest file that describes the package's name, contents, dependencies, and other metadata. The manifest file is always named Package.swift.

Swift packages serve several purposes:

  • Code organization: Group related code into modules
  • Code sharing: Share code across multiple projects
  • Dependency management: Include third-party code in your project

Package Structure

A basic Swift package has the following structure:

MyPackage/
├── Package.swift // The manifest file
├── README.md // Documentation
├── Sources/ // Source code
│ └── MyPackage/ // Module directory
│ ├── MyPackage.swift
│ └── AnotherFile.swift
└── Tests/ // Unit tests
└── MyPackageTests/ // Test directory
└── MyPackageTests.swift

Let's explore what each component does:

  • Package.swift: The manifest file that defines your package and its dependencies
  • Sources: Contains the source code, organized by module
  • Tests: Contains test files for your package

Creating Your First Swift Package

Let's create a simple Swift package that provides utility functions for string manipulation.

Step 1: Create the Package Structure

You can create a new package using the Swift command-line tool:

bash
mkdir StringUtils
cd StringUtils
swift package init --type library

This command creates a basic package structure with the following output:

Creating library package: StringUtils
Creating Package.swift
Creating README.md
Creating .gitignore
Creating Sources/
Creating Sources/StringUtils/StringUtils.swift
Creating Tests/
Creating Tests/StringUtilsTests/
Creating Tests/StringUtilsTests/StringUtilsTests.swift

Step 2: Examine the Package.swift File

Open the generated Package.swift file:

swift
// 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: "StringUtils",
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "StringUtils",
targets: ["StringUtils"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
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 this package depends on.
.target(
name: "StringUtils",
dependencies: []),
.testTarget(
name: "StringUtilsTests",
dependencies: ["StringUtils"]),
]
)

This file defines:

  • The package name (StringUtils)
  • The library product that the package exports
  • Dependencies (currently none)
  • Targets (modules) that the package contains

Step 3: Add Functionality to Your Package

Let's add some string utility functions. Edit the Sources/StringUtils/StringUtils.swift file:

swift
public struct StringUtils {
/// Capitalizes the first letter of each word in a string
/// - Parameter string: The input string
/// - Returns: A string with the first letter of each word capitalized
public static func capitalizeWords(_ string: String) -> String {
return string.components(separatedBy: " ")
.map { $0.prefix(1).uppercased() + $0.dropFirst() }
.joined(separator: " ")
}

/// Reverses a string
/// - Parameter string: The input string
/// - Returns: The reversed string
public static func reverse(_ string: String) -> String {
return String(string.reversed())
}

/// Counts the occurrences of a substring in a string
/// - Parameters:
/// - substring: The substring to search for
/// - string: The string to search in
/// - Returns: The number of occurrences
public static func countOccurrences(of substring: String, in string: String) -> Int {
var count = 0
var searchRange = string.startIndex..<string.endIndex

while let range = string.range(of: substring, options: [], range: searchRange) {
count += 1
searchRange = range.upperBound..<string.endIndex
}

return count
}
}

Step 4: Write Tests for Your Package

Edit the Tests/StringUtilsTests/StringUtilsTests.swift file to add tests:

swift
import XCTest
@testable import StringUtils

final class StringUtilsTests: XCTestCase {
func testCapitalizeWords() {
let input = "hello swift package world"
let expected = "Hello Swift Package World"
XCTAssertEqual(StringUtils.capitalizeWords(input), expected)
}

func testReverse() {
let input = "hello"
let expected = "olleh"
XCTAssertEqual(StringUtils.reverse(input), expected)
}

func testCountOccurrences() {
let string = "hello hello world"
XCTAssertEqual(StringUtils.countOccurrences(of: "hello", in: string), 2)
XCTAssertEqual(StringUtils.countOccurrences(of: "world", in: string), 1)
XCTAssertEqual(StringUtils.countOccurrences(of: "bye", in: string), 0)
}
}

Step 5: Build and Test Your Package

Run the following commands to build and test your package:

bash
swift build      # Build the package
swift test # Run the tests

You should see output indicating that your tests have passed:

[4/4] Linking StringUtilsTests
Test Suite 'All tests' started
Test Suite 'StringUtilsTests.xctest' started
Test Suite 'StringUtilsTests' started
Test Case 'StringUtilsTests.testCapitalizeWords' started
Test Case 'StringUtilsTests.testCapitalizeWords' passed
Test Case 'StringUtilsTests.testReverse' started
Test Case 'StringUtilsTests.testReverse' passed
Test Case 'StringUtilsTests.testCountOccurrences' started
Test Case 'StringUtilsTests.testCountOccurrences' passed
Test Suite 'StringUtilsTests' passed
Test Suite 'StringUtilsTests.xctest' passed
Test Suite 'All tests' passed

Adding Dependencies to Your Package

One of the most powerful features of Swift Package Manager is the ability to add dependencies to your package.

Step 1: Update Package.swift to Include a Dependency

Let's add a popular Swift logging package as a dependency. Modify your Package.swift file:

swift
// swift-tools-version:5.5
import PackageDescription

let package = Package(
name: "StringUtils",
products: [
.library(
name: "StringUtils",
targets: ["StringUtils"]),
],
dependencies: [
// Add SwiftyBeaver as a dependency
.package(url: "https://github.com/SwiftyBeaver/SwiftyBeaver.git", from: "1.9.0"),
],
targets: [
.target(
name: "StringUtils",
dependencies: ["SwiftyBeaver"]), // Add dependency to the target
.testTarget(
name: "StringUtilsTests",
dependencies: ["StringUtils"]),
]
)

Step 2: Use the Dependency in Your Code

Update the StringUtils.swift file to use SwiftyBeaver for logging:

swift
import Foundation
import SwiftyBeaver

let log = SwiftyBeaver.self

public struct StringUtils {
/// Sets up the logger
public static func setupLogger() {
let console = ConsoleDestination()
console.format = "$DHH:mm:ss$d $L $M"
log.addDestination(console)
}

/// Capitalizes the first letter of each word in a string
/// - Parameter string: The input string
/// - Returns: A string with the first letter of each word capitalized
public static func capitalizeWords(_ string: String) -> String {
log.debug("Capitalizing words in: \(string)")
let result = string.components(separatedBy: " ")
.map { $0.prefix(1).uppercased() + $0.dropFirst() }
.joined(separator: " ")
log.info("Capitalization result: \(result)")
return result
}

/// Reverses a string
/// - Parameter string: The input string
/// - Returns: The reversed string
public static func reverse(_ string: String) -> String {
log.debug("Reversing string: \(string)")
return String(string.reversed())
}

/// Counts the occurrences of a substring in a string
/// - Parameters:
/// - substring: The substring to search for
/// - string: The string to search in
/// - Returns: The number of occurrences
public static func countOccurrences(of substring: String, in string: String) -> Int {
log.debug("Counting occurrences of '\(substring)' in '\(string)'")
var count = 0
var searchRange = string.startIndex..<string.endIndex

while let range = string.range(of: substring, options: [], range: searchRange) {
count += 1
searchRange = range.upperBound..<string.endIndex
}

log.info("Found \(count) occurrences")
return count
}
}

Step 3: Update Your Build

Run swift build again to fetch and build with the new dependency:

bash
swift build

Using Your Swift Package in a Project

Now that you've created a Swift package, let's see how to use it in a Swift project.

Adding a Local Package to Your Xcode Project

  1. Open your Xcode project
  2. Select your project in the Project Navigator
  3. Select your target, then go to "Package Dependencies" tab
  4. Click the "+" button
  5. Choose "Add Local Package" and navigate to your package directory
  6. Select the package and click "Add Package"

Adding a Remote Package to Your Xcode Project

If your package is hosted on a Git repository (like GitHub):

  1. Open your Xcode project
  2. Select your project in the Project Navigator
  3. Select your target, then go to "Package Dependencies" tab
  4. Click the "+" button
  5. Enter the repository URL (e.g., https://github.com/yourusername/StringUtils.git)
  6. Choose the version rule (exact version, version range, or branch)
  7. Click "Add Package"

Using the Package in Your Code

After adding the package to your project, you can import it in your Swift files:

swift
import StringUtils

func processString() {
StringUtils.setupLogger()

let input = "hello swift package world"
let capitalized = StringUtils.capitalizeWords(input)
print("Capitalized: \(capitalized)")

let reversed = StringUtils.reverse(input)
print("Reversed: \(reversed)")

let count = StringUtils.countOccurrences(of: "world", in: input)
print("Occurrences of 'world': \(count)")
}

Real-World Example: Building a Configuration Package

Let's create a more practical example: a configuration management package that can read and process application settings from various sources.

Step 1: Create the Package

bash
mkdir ConfigKit
cd ConfigKit
swift package init --type library

Step 2: Define the Configuration Structure

Edit the Sources/ConfigKit/ConfigKit.swift file:

swift
import Foundation

public enum ConfigSource {
case plist(String)
case json(String)
case environment
case defaults([String: Any])
}

public class ConfigKit {
private var configuration: [String: Any] = [:]

public init(sources: [ConfigSource]) {
loadConfiguration(from: sources)
}

private func loadConfiguration(from sources: [ConfigSource]) {
for source in sources {
switch source {
case .plist(let filename):
if let dict = loadPlist(named: filename) {
merge(dict)
}
case .json(let filename):
if let dict = loadJSON(named: filename) {
merge(dict)
}
case .environment:
let env = loadEnvironment()
merge(env)
case .defaults(let defaults):
merge(defaults)
}
}
}

private func loadPlist(named filename: String) -> [String: Any]? {
guard let url = Bundle.main.url(forResource: filename, withExtension: "plist"),
let data = try? Data(contentsOf: url),
let dict = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any] else {
return nil
}
return dict
}

private func loadJSON(named filename: String) -> [String: Any]? {
guard let url = Bundle.main.url(forResource: filename, withExtension: "json"),
let data = try? Data(contentsOf: url),
let dict = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
return nil
}
return dict
}

private func loadEnvironment() -> [String: Any] {
var result: [String: Any] = [:]
for (key, value) in ProcessInfo.processInfo.environment {
result[key] = value
}
return result
}

private func merge(_ dict: [String: Any]) {
for (key, value) in dict {
configuration[key] = value
}
}

public func string(forKey key: String) -> String? {
return configuration[key] as? String
}

public func int(forKey key: String) -> Int? {
return configuration[key] as? Int
}

public func bool(forKey key: String) -> Bool? {
return configuration[key] as? Bool
}

public func array(forKey key: String) -> [Any]? {
return configuration[key] as? [Any]
}

public func dictionary(forKey key: String) -> [String: Any]? {
return configuration[key] as? [String: Any]
}
}

Step 3: Create Tests for the Configuration Package

Edit the Tests/ConfigKitTests/ConfigKitTests.swift file:

swift
import XCTest
@testable import ConfigKit

final class ConfigKitTests: XCTestCase {
func testDefaultsSource() {
let defaults: [String: Any] = [
"appName": "TestApp",
"version": "1.0.0",
"debug": true,
"maxRetries": 3
]

let config = ConfigKit(sources: [.defaults(defaults)])

XCTAssertEqual(config.string(forKey: "appName"), "TestApp")
XCTAssertEqual(config.string(forKey: "version"), "1.0.0")
XCTAssertEqual(config.bool(forKey: "debug"), true)
XCTAssertEqual(config.int(forKey: "maxRetries"), 3)
}

func testEnvironmentSource() {
// This test assumes certain environment variables are set
// For a real test, you might want to use a mocking approach
let config = ConfigKit(sources: [.environment])

// Just test that it doesn't crash
XCTAssertNotNil(config)
}
}

Step 4: Using the Configuration Package in a Project

Here's how you might use this configuration package in a real app:

swift
import ConfigKit

// Create a configuration with multiple sources
let config = ConfigKit(sources: [
.defaults([
"apiTimeout": 30,
"maxRetries": 3,
"debugMode": false
]),
.json("config"), // Will load from config.json
.environment // Environment variables override previous settings
])

// Access configuration values
if let debugMode = config.bool(forKey: "debugMode"), debugMode {
print("Running in debug mode")
}

if let apiBaseUrl = config.string(forKey: "API_BASE_URL") {
print("Using API: \(apiBaseUrl)")
}

let timeout = config.int(forKey: "apiTimeout") ?? 30
print("API timeout set to \(timeout) seconds")

Summary

In this guide, we've explored the fundamentals of Swift packages:

  1. Structure of a Swift Package: Understanding the basic components including Package.swift, Sources, and Tests
  2. Creating a Package: Building a string utilities package from scratch
  3. Adding Dependencies: Incorporating third-party packages into your own package
  4. Using Packages: How to add and use packages in Swift projects
  5. Real-World Example: Creating a practical configuration management package

Swift Package Manager provides a powerful yet simple way to organize, share, and reuse code. As you gain more experience with Swift packages, you'll find they significantly enhance your productivity and code organization.

Additional Resources

Exercises

  1. Create a simple math utilities package with functions for basic operations, statistical calculations, and unit conversions.
  2. Add SwiftLint as a dependency to your package to enforce code style.
  3. Create a package that wraps a REST API client, with proper error handling and response parsing.
  4. Build a logging package that can write logs to multiple destinations (console, file, network service).
  5. Modify the ConfigKit example to add support for YAML configuration files (hint: you'll need to add a YAML parsing dependency).


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)