Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 100 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,47 @@
# VariantIterable

[![CI](https://github.com/takasek/VariantIterable/actions/workflows/ci.yml/badge.svg)](https://github.com/takasek/VariantIterable/actions/workflows/ci.yml)
[![Swift 6.2+](https://img.shields.io/badge/Swift-6.2%2B-orange.svg)](https://swift.org)
[![Platforms](https://img.shields.io/badge/Platforms-iOS%2013%20%7C%20macOS%2010.15%20%7C%20tvOS%2013%20%7C%20watchOS%206-blue.svg)](https://swift.org/package-manager)
[![SPM Compatible](https://img.shields.io/badge/SPM-compatible-brightgreen.svg)](https://swift.org/package-manager)
[![License: MIT](https://img.shields.io/badge/License-MIT-lightgrey.svg)](LICENSE)

**`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.
`CaseIterable` stops working the moment your enum gains an associated value — and it never worked for structs at all. `VariantIterable` picks up where it leaves off:

---
```swift
@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)
}

## Motivation
Banner.allVariants
// [(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))]
```

One annotation, and every type — struct or enum — gets a static `allVariants` property: a **named list of representative instances**, generated at compile time with zero runtime overhead.

| | `CaseIterable` | `VariantIterable` |
|---|:---:|:---:|
| Simple enums | ✅ | ✅ |
| Enums with associated values | ❌ | ✅ |
| Structs | ❌ | ✅ |
| Display names for each entry | ❌ | ✅ |
| Multiple entries per case | ❌ | ✅ |

Swift's `CaseIterable` is convenient for simple enums, but it breaks down in two common situations:
---

## Why?

**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.
The pattern shows up everywhere once you notice it: you have a type, a handful of *interesting* instances of it, and somewhere else a hand-written list of those instances that silently drifts out of sync.

```swift
struct NetworkConfig {
Expand All @@ -35,9 +61,44 @@ let allConfigs: [(String, NetworkConfig)] = [
]
```

**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.
SwiftUI previews, debug menus, snapshot tests, demo catalogs — they all need that list. `VariantIterable` generates it for you, right next to the definitions it mirrors.

`VariantIterable` solves both problems with a single annotation.
---

## Use Cases

### SwiftUI Previews — render every state at once

```swift
#Preview {
VStack(spacing: 12) {
ForEach(Banner.allVariants, id: \.name) { variant in
BannerView(banner: variant.value)
}
}
}
```

Add a case, and the preview updates itself. No more previews that only show the happy path.

### Snapshot tests — one test, every variant

```swift
@Test(arguments: Banner.allVariants)
func snapshot(variant: (name: String, value: Banner)) {
assertSnapshot(of: BannerView(banner: variant.value), as: .image, named: variant.name)
}
```

The `name` doubles as the snapshot filename — descriptive, stable, and defined next to the value itself.

### Debug menus — switch environments without hardcoding

```swift
List(NetworkConfig.allVariants, id: \.name) { variant in
Button(variant.name) { apply(variant.value) }
}
```

---

Expand Down Expand Up @@ -104,6 +165,28 @@ extension NetworkConfig: VariantIterable {}

---

## Installation

Add the package in `Package.swift`:

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

---

## Usage

### `name:` — custom display names
Expand Down Expand Up @@ -302,7 +385,10 @@ A `private` type gets `fileprivate` members. The generated property satisfies th

## Diagnostics

The macro emits compile-time errors and warnings for misuse. Recoverable entries are skipped; the rest of `allVariants` is still generated.
The macro emits compile-time errors and warnings for misuse — mistakes are caught when you build, not when you run. Recoverable entries are skipped; the rest of `allVariants` is still generated.

<details>
<summary>See all diagnostics</summary>

**Positional arguments on a stored property**

Expand Down Expand Up @@ -377,6 +463,8 @@ enum Config {
}
```

</details>

---

## How It Works
Expand All @@ -402,31 +490,9 @@ Because all code generation happens at compile time and produces plain Swift sou

---

## Installation

Add the package in `Package.swift`:

```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
## Contributing

Clone the repository and run the test suite:
Issues and pull requests are welcome! Clone the repository and run the test suite:

```bash
swift test
Expand All @@ -439,6 +505,8 @@ The project is structured as a standard Swift package:
- `Sources/VariantIterableClient` — executable playground for manual testing
- `Tests/VariantIterableTests` — macro expansion tests via `assertMacroExpansion`

If VariantIterable saves you some boilerplate, consider giving it a ⭐️ — it helps others discover the library.

---

## License
Expand Down
Loading