Connor Neville

I’m an iOS Engineer and Engineering Manager with a passion for building quality products.

My Favorite Macro Use Case: StaticMemberIterable

07 July 2023

Last month, WWDC came packed with exciting new developments for the iOS community, as it always does. Every year, one of the most anticipated talks for me is What’s new in Swift, because along with writing iOS apps, I like to leverage Swift for server-side projects too.

With this WWDC comes Swift 5.9, and I’d describe the set of changes overall as somewhat low-level and tough to grasp. I don’t anticipate using the ownership APIs or C++ interoperability, and while I’m sure I’ll discover a use case for type parameter packs, there is a lot of new syntax to process.

Macros are definitely nontrivial to grasp as well, but the value proposition to developers is pretty clear - extend the compiler in useful ways that previously required custom code generation tooling. Historically, when iOS developers have found repetitive boilerplate that existing tools can’t help with automating, Sourcery had been the best tool for the job. Both Sourcery and the new macro system fundamentally work by allowing you to examine your code as an abstract syntax tree and output some new Swift code to be compiled. Even as the Swift compiler improves, there are still a bunch of common use cases for such a tool:

  • Custom Equatable, Hashable and Codable implementations
  • Generating mocks for type declarations
  • Mapping user preferences in User Defaults to a UIKit/SwiftUI form

If you want to learn more about macros, their benefits, and how they work, you should watch the WWDC session Expand on Swift macros first, and then Write Swift macros.

There’s already one mainstay use case for macros in most of my projects, and to explain why I find it useful, I need to back up and talk about a stylistic discussion that occurs a lot in the context of Swift projects.

You Might Not Want an Enum…

The situation I want to examine is when a Swift project has a data type with a bunch of preset values. That’s (intentionally) a pretty vague description, and examples include:

  • The set of all HTTP methods: GET, PUT, POST, DELETE, and so on.
  • Possible roles for users on a platform: Admin, Moderator, User, Guest, etc.
  • Models of non-player characters in a video game.
  • A client-side list of supported countries and their flags.
  • A set of themes for users to customize their UI.

The list of examples is quite long. And the related style discussion that comes up is: should the data type be a struct or an enum? Both of the following definitions have the same callsite and the same usage semantics.

enum AppTheme {
  // …
  case metal
  case skyBlue
  // …

  var name: String {
    switch self {
    // …
    case .metal: return "Metal"
    case .skyBlue: return "Sky Blue"
    // …
    }
  }

  var headingColor: String {
    switch self {
    // …
    case .metal: return "000000"
    case .skyBlue: return "33FFFF"
    // …
    }
  }
}

and:

struct AppTheme {
  let name: String
  let headingColor: String
  init(name: String, headingColor: String) {
    self.name = name
    self.headingColor = headingColor
  }

  // …
  static let metal = AppTheme(name: "Metal", headingColor: "000000")
  static let skyBlue = AppTheme(name: "Sky Blue", headingColor: "33FFFF")
  // …
}

can both be called identically (for example):

print(AppTheme.metal.name)

Enums have many valid use cases and are a critical part of the Swift language, but in the above described situations, I would say the struct is preferable here. I’m going to keep the “why” brief, because there already exist some good resources on this topic, but:*

  • It’s easier to extend with new values. When adding a new case to the enum, you need to fill in the case inside of var name: String and then navigate to the case inside of var headingColor: String - which sounds like a small deal, but at scale, these value types will often have many more than 2 properties, and many more than 2 cases. With a struct, all of those properties are localized together.
  • You can lock down the initialization (if you want): with a struct, you can choose to make the init private, or you can customize which properties are directly instantiable. With enums, you can add a case if you’re inside the owning module, and not otherwise.
  • At scale, it’s easy for convoluted logic or accidental mismatches to end up inside the computed vars. With structs, all you can do is declare the type.

For more on this choice and why I advocate for structs, see Matt Diephouse’s post.

…But You Probably Want to Iterate

While I’ve established that I think structs are the right tool for this job, regardless of implementation, it will often be valuable to iterate over this set of static values. Maybe you want to show a modal UI with all of the supported AppTheme values, or list the supported countries, or you want to snapshot test the JSON representation of all of the user roles. Admittedly, this is the one area where enums have a leg up - Apple supports this out of the box by just conforming to CaseIterable:

// only works for free if using an enum!
extension AppTheme: CaseIterable {} 
print(AppTheme.allCases.map(\.name))

So, how do we patch this gap in functionality with structs?1 This is a perfect use case for (historically) Sourcery and (now) Swift macros! If the above examples don’t sound valuable, I’ll be clear that I find this a very valuable piece of functionality because 1) every app always has these data types with a bunch of preset values and 2) I find they very often end up being representable as JSON, some UI, or both. Being able to iterate over the set means you can get new JSON snapshot tests, UI snapshot tests, SwiftUI previews and more, without any additional work when you add a new value.

So, that’s why I have a StaticMemberIterable in most of my projects.

StaticMemberIterable

As mentioned above, this is a good example of a gap in current compiler functionality, which we can patch up via code generation - either via Sourcery, or, starting with Xcode 15, Swift macros.

As a Sourcery Template

You can see my Sourcery implementation of StaticMemberIterable here as a gist. If you’re looking to install it but aren’t familiar with Sourcery, you should give the documentation in Sourcery’s repo a read first. You’ll see that this template works with Sourcery’s annotations system, which means you just need to add a comment above the struct to opt-in to the StaticMemberIterable code generation:

// sourcery:StaticMemberIterable
struct AppTheme {
  // …
}

The template could easily be tweaked to make it work by conforming your struct type to a protocol instead, however: the template is a .swifttemplate file, which, while documented, is not as easy to work with or diagnose errors as regular Swift, so I don’t iterate on it much - that’s one of the handful of reasons why macros are a big improvement here.

As a Macro

As far as the Macro-version of the implementation, I started using Ian Keen’s MacroKit, and the outputted code is functionally identical as before.

So, why is the Macro implementation so much better?

  • They are written as pure Swift, which means compilation checks on your macro code and the ability to unit test your StaticMemberIterable implementation (not that I’ve gotten around to that part!).
  • They are installed via a plugin system, which has its own overhead, but with Sourcery at some level you will need to invoke it via shell, passing arguments (or providing a YAML file) and making sure it’s being executed as (usually) a build phase. Macros can be iterated on more effectively and safely, because their entire integration is visible and inspectable within Xcode.
  • Macros are predictable and additive - every declaration or expression that is affected by a Macro is given a clear annotation, and macros can only add and extend code, not modify or delete it. Xcode has first class support for viewing the diff produced by the Macro, inline. Sourcery is also additive, but as mentioned earlier, its tooling stack often goes outside of Xcode and can have a higher learning curve.

I should also note the one major advantage that Sourcery still has over Swift macros at this time: Sourcery will discover all declarations in the Swift files that are specified in the shell prompt or YAML file, whereas macros only have visibility into the declaration they are attached to. That means that putting the static members in an extension like:

@StaticMemberIterable struct AppTheme {
  // …
}

// ⚠️
extension AppTheme {
  // …
  static let metal = AppTheme(name: "Metal", headingColor: "000000")
  static let skyBlue = AppTheme(name: "Sky Blue", headingColor: "33FFFF")
  // …
}

will not include metal and skyBlue in its generated allCases, because the @StaticMemberIterable macro only applies to the declaration it is attached to (the struct, not the extension). The Sourcery approach, however, would include those values in the generated code, because it discovers all declarations within the scoped files.

Future Directions

StaticMemberIterable is a good addition to a lot of projects, and I’m excited to see developers push other brand new compiler capabilities with macros. A few things on this topic I’m still thinking about:

  • A separate @ViewFromStaticMemberIterable Macro that must be attached to a SwiftUI View, provides a type that conforms to StaticMemberIterable, and produces snapshots for each instance2. I could write a single XCTestCase that iterates over them in a for loop, but test failure reporting is subpar that way - all failures point to the same line and it’s not clear which instances had their tests fail. Better to generate a whole XCTestCase with a test function per instance.
    • I have this working as a Sourcery template and will update here whenever I have a macro version to share.
  • Extend StaticMemberIterable to include values produced by static functions, if their only argument is also CaseIterable or StaticMemberIterable:
@StaticMemberIterable(recursive: true)
struct AppTheme {
  static let metal: AppTheme = // …
  static let skyBlue: AppTheme = //…

  // Improve `StaticMemberIterable` to produce cases like
  // `.promotionTheme(.holiday), .promotionTheme(.anniversary), etc.
  // for this function if `PromotionKind` is `CaseIterable` or `StaticMemberIterable`,
  // and if `recursive` is true.
  static func promotionTheme(promotionKind: PromotionKind) -> AppTheme {
    // …
  }

}

This would be a substantial test of the SwiftSyntax macros API, but I believe should be possible.

That’s all for now on this topic. In addition to the resources linked above, I also recommend checking out the swift-macro-examples repo if you’re just getting started with this new language feature.

  1. Of course, you can implement static var allCases: [Self] for such a struct manually. But that means accepting that, on a project at scale with multiple developers working in the project, static members will probably be forgotten and left out of this declaration. It also means possible merge conflicts when multiple developers manually edit the declaration. 

  2. I’m a big proponent of snapshot testing. There is enough to be said on snapshot testing to merit a separate post, but in short, I think it’s a low-effort, high-reward way of adding test coverage to iOS apps. I am a fan of PointFree’s snapshot testing framework. If you already use snapshot tests in your project, I would say the likelihood increases that StaticMemberIterable is useful to you in some capacity.