Skip to content

takasek/VariantIterable

Repository files navigation

VariantIterable

Swift 6.2+ Platforms SPM Compatible License: MIT

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.


Motivation

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.


Quick Start

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.com
See 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 {}

Usage

name: — custom display names

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()
}

static func — parameterized variants

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()

enum — cases without associated values

@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.

enum — cases with associated values

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.

@VariantIterableAllCases — skip annotations for simple enums

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"
}

Access level

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)] { ... }

Diagnostics

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.
}

How It Works

@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.


Requirements

Minimum
Swift 6.2
Xcode 26.0
iOS / Mac Catalyst 13.0
macOS 10.15
tvOS 13.0
watchOS 6.0

Installation

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.


Development

Clone the repository and run the test suite:

swift test

The 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 testing
  • Tests/VariantIterableTests — macro expansion tests via assertMacroExpansion

License

VariantIterable is released under the MIT license. See LICENSE for details.

About

Swift macro that brings `CaseIterable`-style enumeration to structs and enums with associated values.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors