Skip to Content
Course content

100: Creating a Multi-Target Swift Package

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've spent a lot of time building libraries that I want to share across multiple projects, and I've noticed a pattern: I usually start with a single library target, but then I realize I need a way to test it or interact with it without booting up a full iOS or macOS app. For this session, let's build a NetworkLogger package. The goal is to have a core logic library that any app can use, but also a standalone Command Line Interface (CLI) tool that can parse those logs from a file.

Starting with a Simple Core

First, I'll just spin up a basic package. I'm going to create a library called NetworkLoggerCore. This is where the heavy lifting happens—formatting the HTTP requests and responses into something readable.

// Package.swift
let package = Package(
    name: "NetworkLogger",
    products: [
        .library(name: "NetworkLoggerCore", targets: ["NetworkLoggerCore"]),
    ],
    targets: [
        .target(name: "NetworkLoggerCore"),
    ]
)

I'll throw a simple LogFormatter class into Sources/NetworkLoggerCore/LogFormatter.swift. At this point, everything is fine. It compiles, it's a standard library. But here is where the limitation hits: if I want to verify how a specific log file looks when parsed, I have to create a whole new Xcode project, import this package, and run it. That's just too much friction.

The "Wait, Where Does the Code Go?" Moment

I want a CLI tool. I'll just add another target to my Package.swift. I'll call it NetworkLoggerCLI and make it an executableTarget.

// Updated Package.swift
targets: [
    .target(name: "NetworkLoggerCore"),
    .executableTarget(name: "NetworkLoggerCLI"),
]

I hit build, and immediately, SwiftPM screams at me. It says it can't find any source files for NetworkLoggerCLI. Now, if you're new to multi-target packages, you might think you can just put all your files in one folder and let the manifest sort it out. I tried that early in my career, and it's a nightmare of naming collisions. SwiftPM expects a strict directory mapping.

I need to look at my file tree. Right now I have:

  • Sources/NetworkLoggerCore/LogFormatter.swift

I need to create a sibling directory for the CLI tool. I'll move my main.swift (the entry point for any executable) into its own folder:

  • Sources/NetworkLoggerCLI/main.swift

Linking the Targets

Now it compiles, but when I try to use LogFormatter inside main.swift, I get a "Use of unresolved identifier" error. I forgot the most important part of a multi-target setup: targets are isolated by default. Just because NetworkLoggerCLI is in the same package as NetworkLoggerCore doesn't mean it can see its code.

I need to explicitly tell the CLI target that it depends on the core library. I'll adjust the targets array again.

targets: [
    .target(name: "NetworkLoggerCore"),
    .executableTarget(
        name: "NetworkLoggerCLI", 
        dependencies: ["NetworkLoggerCore"] // This is the magic link
    ),
]

I'll also update the products section. I want the library to be available for app developers, but I also want the CLI tool to be a distributable product.

products: [
    .library(name: "NetworkLoggerCore", targets: ["NetworkLoggerCore"]),
    .executable(name: "netlog-tool", targets: ["NetworkLoggerCLI"]),
]

Now, when I run swift run netlog-tool, it works. I've effectively split my package into a "Logic" layer and a "Tooling" layer. The logic stays lean and has no dependencies on the CLI's environment, while the CLI can leverage everything in the core library without duplicating a single line of code.




📋 Practical Task

Build a DataValidator Package with a CLI Inspector

Create a Swift Package named DataValidator that consists of two targets:

  • DataValidatorCore: A library target containing a struct called Validator with a method isValid(input: String) -> Bool that returns true if the input string is not empty and contains at least one digit.
  • ValidatorInspector: An executable target that depends on DataValidatorCore. Its main.swift should take a command-line argument (a string), pass it to the Validator, and print whether the string is valid or invalid.

Ensure your folder structure correctly separates the sources for both targets and that the Package.swift manifest defines both a library and an executable product.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.