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 UUIDV7
s 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.