CaseIterable for structs and enums with associated values.
Attach @VariantIterable to a type and @Variant to its static members or enum cases. The macro generates a static allVariants property — a named list of representative instances — with no runtime overhead.
Swift's CaseIterable is convenient for simple enums, but it breaks down in two common situations:
Structs have no cases to iterate at all. If you want a fixed set of representative instances — say, for a debug menu or an Xcode preview — you end up writing the list by hand and keeping it in sync with your static members.
struct NetworkConfig {
let baseURL: String
let timeout: TimeInterval
static let development = NetworkConfig(baseURL: "http://localhost:8080", timeout: 60)
static let staging = NetworkConfig(baseURL: "https://staging.example.com", timeout: 30)
static let production = NetworkConfig(baseURL: "https://api.example.com", timeout: 10)
}
// Somewhere else — easy to get out of sync:
let allConfigs: [(String, NetworkConfig)] = [
("Development", .development),
("Staging", .staging),
("Production", .production),
]Enums with associated values cannot conform to CaseIterable at all. Each case needs concrete values to be instantiated, and there is no standard way to express that.
VariantIterable solves both problems with a single annotation.
import VariantIterable
@VariantIterable
struct NetworkConfig {
let baseURL: String
let timeout: TimeInterval
@Variant
static let development = NetworkConfig(baseURL: "http://localhost:8080", timeout: 60)
@Variant
static let staging = NetworkConfig(baseURL: "https://staging.example.com", timeout: 30)
@Variant
static let production = NetworkConfig(baseURL: "https://api.example.com", timeout: 10)
}
// The macro generates:
// static var allVariants: [(name: String, value: Self)] { ... }
for (name, config) in NetworkConfig.allVariants {
print("\(name): \(config.baseURL)")
}
// development: http://localhost:8080
// staging: https://staging.example.com
// production: https://api.example.comSee the generated code
// Expansion of @VariantIterable
struct NetworkConfig {
let baseURL: String
let timeout: TimeInterval
// @Variant is a peer macro that expands to nothing — it is consumed at compile time
static let development = NetworkConfig(...)
static let staging = NetworkConfig(...)
static let production = NetworkConfig(...)
// ↓ generated by @VariantIterable
static var allVariants: [(name: String, value: Self)] {
[
(name: "development", value: .development),
(name: "staging", value: .staging),
(name: "production", value: .production),
]
}
}
// ↓ generated by @VariantIterable
extension NetworkConfig: VariantIterable {}By default the member or case name is used as-is. Pass name: to override it with any string. Both static let and static var are supported.
@VariantIterable
struct Config {
@Variant // → name: "development"
static let development: Self = .init()
@Variant(name: "Staging 🏗") // → name: "Staging 🏗"
static let staging: Self = .init()
}Pass positional arguments to @Variant that match the function's parameter list. Apply multiple @Variant attributes to register multiple entries from a single function.
@VariantIterable
struct NetworkConfig {
@Variant(5, name: "Aggressive")
@Variant(120, name: "Lenient")
static func custom(timeout: TimeInterval) -> Self { ... }
}
// Generates:
// (name: "Aggressive", value: .custom(timeout: 5)),
// (name: "Lenient", value: .custom(timeout: 120)),Pass one positional argument per parameter — labels are inferred from the function signature, and underscore labels (_) are dropped:
@Variant(Date.distantFuture, name: "Far future")
static func scheduled(at date: Date) -> Self { ... }
// → .scheduled(at: Date.distantFuture)
@Variant("api.example.com", 443, name: "HTTPS")
static func connect(host: String, port: Int) -> Self { ... }
// → .connect(host: "api.example.com", port: 443)
@Variant(42, name: "ID=42")
static func byID(_ id: Int) -> Self { ... }
// → .byID(42) (underscore label is dropped)Zero-parameter functions are called with empty parentheses:
@Variant(name: "Default")
static func makeDefault() -> Self { ... }
// → .makeDefault()@VariantIterable
enum Banner {
@Variant
case success
@Variant
case warning
case dismissed // no @Variant → not collected
}Only annotated cases appear in allVariants. Unannotated cases are silently skipped.
Provide the associated values as positional arguments to @Variant. Apply multiple attributes for multiple entries.
@VariantIterable
enum Banner {
@Variant("Oops, something went wrong.", name: "Short error")
@Variant("A network error occurred. Please check your connection.", name: "Long error")
case error(String)
@Variant(503, name: "Server Error")
case httpError(code: Int)
@Variant("api.example.com", 443, name: "HTTPS")
case connection(host: String, port: Int)
}
// Generates:
// (name: "Short error", value: .error("Oops, something went wrong.")),
// (name: "Long error", value: .error("A network error occurred...")),
// (name: "Server Error", value: .httpError(code: 503)),
// (name: "HTTPS", value: .connection(host: "api.example.com", port: 443)),Note
VariantIterable inherits from Sendable. Types whose associated values are non-Sendable (e.g. closures) cannot conform and are out of scope for this library.
Note
@VariantIterableAllCases is designed for enums. For structs, use @VariantIterable.
When an enum has many cases without associated values, annotating every one with @Variant is repetitive. Use @VariantIterableAllCases instead: all cases are collected automatically, and CaseIterable conformance is added alongside VariantIterable.
@VariantIterableAllCases
enum Alert {
case success
case warning
@Variant("Network timeout.", name: "Timeout")
@Variant("Server returned an error.", name: "Server error")
case error(String)
}
Alert.allCases
// [.success, .warning, .error("Network timeout."), .error("Server returned an error.")]
Alert.allVariants
// [(name: "success", value: .success),
// (name: "warning", value: .warning),
// (name: "Timeout", value: .error("Network timeout.")),
// (name: "Server error", value: .error("Server returned an error."))]allCases is generated as allVariants.map(\.value), so both properties stay in sync automatically.
Note
When multiple @Variant annotations are applied to a single case, allCases will contain one entry per annotation. This differs from the standard CaseIterable contract of exactly one entry per case. If you need that guarantee, use @VariantIterable and omit the affected cases from your variants list.
Cases with associated values still require an explicit @Variant — the compiler will emit an error if they are not annotated:
@VariantIterableAllCases
enum Alert {
case success // ✅ auto-collected
case error(String) // 🛑 @VariantIterableAllCases: 'error' has associated
// values and requires an explicit @Variant annotation.
}When a case's associated value is too complex to express as a macro argument, define a static let that holds the complete value and reference it with @Variant(at:):
@VariantIterableAllCases
enum Config {
@Variant(at: Self.largePayload, name: "Large payload")
case withData(Data)
static let largePayload = Self.withData(Data(repeating: 0xFF, count: 1024))
}Use @Variant(name:) on individual cases to override the default name:
@VariantIterableAllCases
enum Status {
case active
@Variant(name: "Not active")
case inactive // → name: "Not active"
}The generated allVariants (and allCases, when using @VariantIterableAllCases) inherit the access level of the type they are attached to. A public type gets a public property; an internal type gets an internal one.
public struct NetworkConfig { ... }
// → public static var allVariants: [(name: String, value: Self)] { ... }
internal enum Banner { ... }
// → static var allVariants: [(name: String, value: Self)] { ... }The macro emits compile-time errors and warnings for misuse. Recoverable entries are skipped; the rest of allVariants is still generated.
Positional arguments on a stored property
@Variant(42, name: "bad") // 🛑 @Variant: 'production' expects no arguments.
static let production: Self = .init()Argument count mismatch on a function
@Variant(1, 2, name: "too many") // 🛑 @Variant: 'byID' expects 1 argument(s)
static func byID(_ id: Int) -> Self { ... } // but 2 were provided.Argument count mismatch on an enum case
@Variant(name: "HTTPS") // 🛑 @Variant: 'connection' expects 2 argument(s)
case connection(host: String, port: Int) // but 0 were provided.Multi-element case declaration
@Variant(name: "bad") // 🛑 @Variant cannot be applied to a multi-element case
case a, b // declaration (e.g. `case a, b`). Declare each case
// on its own line.Missing name: with multiple @Variant attributes
When multiple @Variant attributes are applied to the same declaration, omitting name: produces duplicate entries in allVariants. The macro emits a warning on each offending attribute:
@Variant(5) // ⚠️ @Variant: 'name:' is required when multiple @Variant
@Variant(120) // ⚠️ attributes are applied to the same declaration.
static func custom(timeout: TimeInterval) -> Self { ... }@Variant(at:) outside @VariantIterableAllCases
@VariantIterable
enum Config {
@Variant(at: Self.payload, name: "Large") // 🛑 @Variant(at:) is only supported
case withData(Data) // with @VariantIterableAllCases. To include
// 'withData' with @VariantIterable,
static let payload = Config.withData(...) // annotate the static let with @Variant directly.
}@VariantIterable and @VariantIterableAllCases are member + extension macros. When the compiler expands them, the macro reads every member of the type and collects those annotated with @Variant. In @VariantIterableAllCases mode, unannotated enum cases without associated values are also collected automatically. The macro then synthesises the allVariants computed property and adds the VariantIterable conformance via an extension.
@Variant itself is a peer macro that acts as a pure marker — its expansion always returns an empty list. Its sole purpose is to be readable by the parent macro at compile time.
Because all code generation happens at compile time and produces plain Swift source, there is no runtime reflection and no overhead beyond what you would write by hand.
| Minimum | |
|---|---|
| Swift | 6.2 |
| Xcode | 26.0 |
| iOS / Mac Catalyst | 13.0 |
| macOS | 10.15 |
| tvOS | 13.0 |
| watchOS | 6.0 |
Add the package in Package.swift:
dependencies: [
.package(url: "https://github.com/takasek/VariantIterable.git", from: "0.1.0"),
],
targets: [
.target(
name: "MyTarget",
dependencies: [
.product(name: "VariantIterable", package: "VariantIterable"),
]
),
]Or use File › Add Package Dependencies… in Xcode and paste the repository URL.
Clone the repository and run the test suite:
swift testThe project is structured as a standard Swift package:
Sources/VariantIterable— public API (protocol + macro declarations)Sources/VariantIterableMacros— macro implementation (swift-syntax)Sources/VariantIterableClient— executable playground for manual testingTests/VariantIterableTests— macro expansion tests viaassertMacroExpansion
VariantIterable is released under the MIT license. See LICENSE for details.