Generate boilerplate code
Macros are finally here in 2023 and it’s adding a lot more expressiveness, less boilerplate, and fulfilling what property wrappers couldn’t.
Just looking at the available macro roles and knowing we can combine them too, there is a lot to talk about here. So in this article, I’m going to explore the @attached(member) role.
So for this article, we will need a few key things — Xcode 15+ (as of this writing it’s in beta).
To start off we will need to create a new package from File -> New. There we can create a brand new package.
Let’s choose Swift Macro which should be in the bottom right of the options.
This will prompt us for the name of the package.
Once you have named your package you should see the directories below, I’ve named this package MediumArticle .
Looking in at Sources we should have three folders;
- MediumArticle contains our declarations
- MediumArticleClient is for us to run it
- MediumArticleMacros is the actual definition of it.
Each of these files already has templated code that you would have likely seen in WWDC 23 sessions on macros.
Alright, let’s dive into a use case walkthrough from start to finish.
I want to be able to log issues on a per-object basis, only in debug though. There are a few different ways we could solve this with macros however the focus here will be on @attached(member) roles. Normally we want to log a bunch of different things throughout but for this example, we will be logging Strings and it’s going to print only during debugging.
This time around decided to name it DebugLogger to keep in the spirit of what we are trying to achieve, logging during debugging.
If you open up DebugLogger file you’ll see something like below.
For more info on freestanding expressions please take a look at the WWDC sessions at the end of this article.
Let’s get rid of this and replace it with
Let’s walk through this by line
- Line 13 — tells this macro we want to have memberwise func/properties on whomever. In this instance, I want a function named log(issue:) but this names parameter allows us to pass in multiple options.
- Line 14 — declares our macro name and says where and what type our actual macro implementation is.
Next is our implementation, if we open up DebugLoggerMacro we will see something like this.
We will get rid of everything but the Plugin portion at the bottom, we will need this to let others consume our macro.
We can replace it with DebugLoggerMacro what is a bit more appropriate for what we are after. Since we are creating a @attached(member) we will need to conform to MemberMacro . This will give us this little expansion function.
- Line 8 — AttributeSyntax provides information about the current arguments that are attached to this given macro.
- Line 9 — DeclGroupSyntax gives all the information on the given declaration that this macro is attached to.
- Line 10 — MacroExpressionContext is an interface to extract information about the context in which a given macro is expanded.
In the next section, we will see DeclGroupSyntax in action.
Now for this macro, I want to just attach it to a Class or Struct but we could attach it to others. On lines 23–30, I’m using declaration to check/cast to a Class or Struct DeclSyntax, if not I’m tossing an error.
With this I’m focused on grabbing the identifier from these objects, this will identify the naming of this Token, which should be the object name that it’s attached to.
Now down on line 46 since we want to create a function that allows for logging, let’s create FunctionDeclSyntax. This requires us to write it out as a String with the same name & parameter that we defined at the start in the DebugLogger file.
This has a result builder that allows us to have multiple Syntax objects that let us produce the inner code.
The inner code is just a Statement — StmtSyntax, this can take the string literal option. Here I want to use print only in debug. I’m printing out the object name it’s attached to and also providing the issue from the function parameter.
On the return we need to return DeclSyntax so we pass in a ___DeclSyntax to it, in this instance it’s the FunctionDeclSyntax.
We gotta add tests to make sure everything is good and as expected. In these tests, all we testing for is that the generated macro is the way it’s expected to be.
Now we should be able to use it. We can create a class and attach @DebugLogger to it. Within anything; function, computed/stored properties, closures, etc, we can use this log function.
I just scratched the surface of what is actually available and honestly, the combinations and what you can do with it seems almost limitless.
More info can be found on the WWDC23 videos & I’ve also created a reuse identifier macro which also used @attached(member) that we just explored here.
- Write Swift macros – WWDC23 – Videos – Apple Developer
- Expand on Swift macros – WWDC23 – Videos – Apple Developer
- GitHub – collisionspace/ReuseIdentifierMacro: A Reuse Identifier Macro that is useful in generation of a reuse id for your UICollectionViewCells and UITableViewCells