A Swift macro for enums that generates case projections, providing type-safe access to associated values via KeyPaths.
Enums with associated values are one of Swift’s most powerful features—but the syntax can be tricky.
@CaseProjection
removes this friction by letting you work with enum cases directly through key paths, subscripts, and SwiftUI bindings.
Add swift-case-projection with Swift Package Manager:
.package(url: "https://github.com/swhitty/swift-case-projection.git", from: "0.1.0")
Then add "swift-case-projection"
as a dependency in your target.
Annotate your enum with @CaseProjection
to enable projections:
import CaseProjection
@CaseProjection
enum Item {
case foo
case bar(String)
}
var item: Item = .foo
item.isCase(\.foo) // true
item.isCase(\.bar) // false
You can read associated values from each case using the case:
subscript:
item = .bar("Fish")
item[case: \.bar] // "Fish"
item[case: \.foo] // nil
When the enum is optional, you can set or clear cases directly:
var item: Item?
item[case: \.bar] = "Chips"
item == .bar("Chips")
item[case: \.bar] = nil
item == nil
Setting nil
on an inactive case has no effect:
item = .foo
item[case: \.bar] = nil // still .foo
item == .foo
item[case: \.foo] = nil
item == nil
Expanding the macro reveals the projected view of the enum with a mutable property for each case.
extension Item: CaseProjecting {
struct Cases: CaseProjection {
var base: Item
init(_ base: Item) {
self.base = base
}
var foo: Void? {
get {
guard case .foo = base else { return nil }
return ()
}
set {
if newValue != nil {
base = .foo
} else if foo != nil {
base = nil
}
}
}
var bar: String? {
get {
guard case let .bar(p0) = base else {
return nil
}
return p0
}
set {
if let newBase = newValue.map(Base.bar) {
base = newBase
} else if bar != nil {
base = nil
}
}
}
}
}
When using case key paths like item[case: \.foo]
the type is rooted in this Cases
projection.
let fooPath = \Item.Cases.foo
let barPath = \Item.Cases.bar
var item: Item = .foo
item.isCase(fooPath) // true
item.isCase(barPath) // false
Project optional enums into SwiftUI bindings to drive presentation from associated values.
.sheet(item: $viewModel.item.unwrapping(case: \.baz)) { id in
BazView(id: id)
}
Prefer stricter semantics? Use .guarded(case:)
to allow writes only when the enum is already in that case; otherwise, assignments are ignored.
.sheet(item: $viewModel.item.guarded(case: \.baz)) { id in
BazView(id: id)
}
Or trigger presentations when a case is present.
.sheet(isPresented: $viewModel.item.isPresent(case: \.baz)) {
BazView()
}
When presented views are dismissed, the binding calls wrappedValue[case: \.baz] = nil
, which clears the associated value and resets the enum to nil
if that case was active.
CaseProjection is primarily the work of Simon Whitty.