On Swift Package Traits

I recently released a small little library for generating version 7 UUIDs in Swift that integrates with popular libraries such as GRDB, Structured Queries, Tagged, and more. Some of the interesting comments I’ve gotten relate to one of SPM’s relatively new features (introduced in Swift 6.1) called package traits. From what it seems, this new SPM feature is relatively unknown, yet I think many packages would benefit from utilizing this feature to support integrations with other packages.

If you’ve written Rust before, you may be familiar with cargo features. Package traits are essentially the equivalent of cargo features, but for Swift.

Though, I’m guessing that most people reading this are unfamiliar with Rust, so this article should give enough of an overview of traits. Alternatively, you can read the proposal for package traits, but that won’t tell you about how I used them in swift-uuidv7.


What are Package Traits?

Great question!

Package traits allow you to selectively enable functionality on a swift package. As a package author, you declare them in your Package.swift file like so.


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

import PackageDescription

let package = Package(
  name: "swift-uuidv7",
  platforms: [.iOS(.v13), .macOS(.v10_15), .tvOS(.v13), .watchOS(.v7), .macCatalyst(.v13)],
  products: [.library(name: "UUIDV7", targets: ["UUIDV7"])],
  traits: [
    .trait(
      name: "SwiftUUIDV7Tagged",
      description: "Adds integrated swift-tagged support to the UUIDV7 type."
    ),
    .trait(
      name: "SwiftUUIDV7StructuredQueries",
      description:
        "Adds swift-structured-queries support and column representations to the UUIDV7 type."
    ),
    .trait(
      name: "SwiftUUIDV7GRDB",
      description: """
        Conforms UUIDV7 to GRDB's DatabaseValueConvertible and StatementColumnConvertible \
        protocols, and adds database functions to generate, parse, and extract data from UUIDV7s.
        """
    ),
    .trait(
      name: "SwiftUUIDV7Dependencies",
      description:
        """
        Adds a dependency value to generate UUIDV7s, and interops the base UUID dependency with \
        UUIDV7 generation.
        """
    )
  ],
  // ...
)
        

Then, inside your package, you can selectively compile bits of your code by using them as a flag within compiler directive.


// If SwiftUUIDV7StructuredQueries is enabled, then UUIDV7 conforms to
// QueryBindable from StructuredQueries.

#if SwiftUUIDV7StructuredQueries
  import Foundation
  import StructuredQueriesCore

  extension UUIDV7: QueryBindable {}

  // ...
#endif
        

Selective Dependency Compilation

You can also choose to only selectively compile package dependencies when a specific trait is enabled. This works similarly to how you can tell a Swift Package to selectively compile a dependency for a specific platform. In other words, as a package author, your Package.swift will look like this.


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

import PackageDescription

let package = Package(
  // ...
  targets: [
    .target(
      name: "UUIDV7",
      dependencies: [
        // ...
        .product(
          name: "StructuredQueriesCore",
          package: "swift-structured-queries",
          condition: .when(traits: ["SwiftUUIDV7StructuredQueries"])
        ),
        // ...
      ]
    ),
    .testTarget(name: "UUIDV7Tests", dependencies: ["UUIDV7"])
  ]
)
        

In this example, StructuredQueriesCore is only compiled when the SwiftUUIDV7StructuredQueries trait is enabled. This feature is how swift-uuidv7 is able to integrate with many different large libraries whilst still keeping the build time as minimal as possible.

Dependent Traits

You may have traits that depend on other traits being enabled. For instance, there’s a branch in swift-uuidv7 that adds a new SwiftUUIDV7SharingGRDB trait that will be merged once CloudKit support for SharingGRDB is publicly released.

SharingGRDB depends both on StructuredQueries and GRDB, so enabling SwiftUUIDV7SharingGRDB trait should also enable SwiftUUIDV7StructuredQueries and SwiftUUIDV7GRDB. This is accomplished like so.


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

import PackageDescription

let package = Package(
  // ...,
  traits: [
    // ...
    .trait(
      name: "SwiftUUIDV7SharingGRDB",
      description: """
        Conforms UUIDV7 to IdentifierStringConvertible to make it compatible with CloudKit sync.

        This trait also enables SwiftUUIDV7GRDB and SwiftUUIDV7StructuredQueries.
        """,
      enabledTraits: ["SwiftUUIDV7GRDB", "SwiftUUIDV7StructuredQueries"]
    )
  ],
  // ...
)
        

Default Traits

You can also choose to enable some traits by default on a package. swift-uuidv7 has no need for this because all of its traits conditionally compile external dependencies. If the user has no need for those external dependencies, and just wants the base UUIDV7 type, then we shouldn’t compile them.

Regardless, default traits are enabled like so (this is a simplified example taken from swift-subprocess).


let package = Package(
  // ...
  traits: [
    "SubprocessFoundation",
    "SubprocessSpan",
    .default(enabledTraits: ["SubprocessFoundation", "SubprocessSpan"])
  ],
  // ...
)
        

Building With Traits Enabled

When building your package, you can specify which traits you want to build the package with like so.

swift build --traits SwiftUUIDV7Tagged

You can also enable all traits like so.

swift build --enable-all-traits

How do I use package traits?

We’ve gone over how to add package traits to a package, so now let’s go over how you can use them.

Unfortunately, at the time of writing this, Xcode’s UI does not support enabling traits on a Swift Package. Therefore, you cannot use package traits in a traditional Xcode project.

Instead, you must enable them in your own packages through your Package.swift file. Here’s how that is done.


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

import PackageDescription

let package = Package(
  // ...
  dependencies: [
    // ...
    .package(
      url: "https://github.com/mhayes853/swift-uuidv7",
      branch: "sharing-grdb-icloud",
      traits: ["SwiftUUIDV7SharingGRDB", "SwiftUUIDV7Tagged", "SwiftUUIDV7Dependencies"]
    )
  ],
)
        

That’s all, you can now begin using them right away!

However, passing an explicit set of traits will disable the default trait. If you still want to enable the default trait, you must also pass .default to the set of traits.


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

import PackageDescription

let package = Package(
  // ...
  dependencies: [
    // ...
    .package(
      url: "https://github.com/swiftlang/swift-subprocess.git",
      branch: "main",

      // Keeps the default trait enabled despite passing an explicit set of traits.
      traits: [.default]
    )
  ],
)
        

Using Traits to Integrate with Other Packages

On its own, swift-uuidv7 provides a simple UUIDV7 type that’s more robust than other Swift implementations in the wild by offering timestamp extraction, a Comparable conformance, and sub-millisecond monotonicity. Yet almost certainly you’ll want to store instances of UUIDV7 in your database using GRDB and Structured Queries, use them as entity identifiers for App Intents, and much more.

You can certainly create the very trivial @retroactive conformances on UUIDV7 in your own project. For instance.


// Inside your project...

import UUIDV7
import StructuredQueries

extension UUIDV7: @retroactive QueryBindable {}
        

However, this conformance would only live inside your own project. Either you would have to duplicate it between different projects, or you would have to create a dedicated package for this 1 line conformance. For many, copying it between different projects is probably fine, but it still isn’t the most ideal solution here. The latter option of creating a tiny package at scale would probably create the same micro-package culture that can be found in the NPM ecosystem.

Rather, we can use package traits to solve this issue, and provide an official conformance that can be used in any project.


// Inside swift-uuidv7...

#if SwiftUUIDV7StructuredQueries
  import Foundation
  import StructuredQueriesCore

  extension UUIDV7: QueryBindable {}
#endif
        

Who adds the trait?

In the above example, either StructuredQueries or UUIDV7 can add a trait to support this integration.

For instance, StructuredQueries could create a StructuredQueriesUUIDV7 trait that adds a dependency on swift-uuidv7, and that trait would provide the conformance that you see above. Alternatively, UUIDV7 could create a SwiftUUIDV7StructuredQueries trait that adds a dependency on swift-structured-queries, and that trait would provide the conformance you see above. Obviously, only 1 package can actually make such a trait because we would be in circular hell otherwise.

Since UUIDV7 is designed to integrate with numerous different libraries (5 at the time of writing this), it would be quite cumbersome if I had to convince the maintainers of each library to add a trait specifically for my very niche package. Therefore, it’s for the best if I provided all the integrations myself through UUIDV7, and in return it keeps all the integrations bundled in 1 simple place.

A Coordination Problem

That being said, this can turn into a nasty coordination problem if there isn’t any consideration around which packages add certain traits.

For instance, what if a hot new library comes around and adds UUIDV7 support through a trait of its own? Now the simplicity of having all of UUIDV7s integrations in a single place is lost, because a new library now maintains its own integration with UUIDV7.

We could define best practices that state that smaller and more stable packages like UUIDV7 should not offer any package integration traits, and instead only let other more volatile and larger packages (eg. GRDB, Structured Queries) define such traits. From the standpoint of how I think of objects, this makes a lot of sense because larger packages with more knowledge give meaning to the smaller and simpler packages with less knowledge.

However, this now creates a problem where maintainers of more volatile and larger packages have to specifically set out to implement their own integration of such smaller packages. On one hand, this can be a maintenance burden if those maintainers decide to integrate with a lot of smaller packages. On the other hand, it also means that integrations with smaller packages are not readily available because it would require the efforts of several large package maintainers, each with their own more important concerns.

If we want the best outcome, I don’t think we can rely on an entirely dogmatic standard. Rather, this may need to be solved on a package by package basis, where each package defines how it integrates with others. Of course, the definition of integration will likely change as the package evolves. For instance, I don’t think directly supporting 300 package integrations in UUIDV7 is necessarily the best maintenance idea, and at that point I would rather delegate those integrations to those package maintainers.

The larger community would of course have to adhere to those socially agreed upon practices for each package, and be informed when those practices change. Seeing how this will scale may be quite interesting.

— 8/8/25




PS. I’m creating a new series of content that I will call “Swift Bits” that will contain small Swift-specific articles such as these.

I mainly want to focus my writing more on higher level ideas that apply to building products and creating new mediums of spreading ideas. Those ideas carry over into how I write Swift on a day-to-day basis, but I think only focusing on Swift as a whole is quite a narrow viewpoint on designing real systems.

Still, seeing language specifics directly can be useful given the detachment most programming languages have from the systems they create. In that sense, I do believe that this kind of writing is worthwhile from time to time.