Unleashing Swift’s Potential: Enhancing Declarations with Member Macros

VINODH KUMAR
4 min readFeb 24, 2024

--

Photo by Sherif Maliqi on Unsplash

Swift, known for its flexibility and power, offers developers various mechanisms to extend and customize code functionality. Among these tools, Member Macros emerge as a potent means to dynamically augment type or extension declarations, enriching them with new members seamlessly. This article delves into the realm of Member Macros, elucidating their significance and demonstrating their utility through a practical example.

Understanding Member Macros

Member Macros represent a category of macros in Swift specifically designed to generate new declarations that become integral members of a type or extension. These macros are adorned with the “member” attribute and are implemented by types conforming to the MemberMacro protocol. By leveraging Member Macros, developers can infuse additional methods, properties, or declarations directly into the scope of a type or extension, enhancing its functionality without the need for separate extensions or external modifications.

Example Scenario: Enhancing Codable Conformant Types

In our example scenario, we’ll explore how Member Macros can be utilized to augment Codable conformant types with custom coding keys. Consider a situation where we have a Codable struct representing data from an external source. However, the property names in our Swift code differ from those in the external data source. Instead of manually mapping each property to its corresponding coding key, we can employ a Member Macro to automate this process.

Macro Definition

We define a Member Macro named CustomCodable, which generates CodingKeys enum cases for properties within Codable conformant types. The macro is adorned with the “member” attribute, indicating its role in producing new declarations within the same scope. Here’s the definition of the CustomCodable macro:

@attached(member, names: named(CodingKeys))
public macro CustomCodable() = #externalMacro(module: "MacroExamplesImplementation", type: "CustomCodable")

Macro Implementation

The CustomCodable macro implementation dynamically inspects the properties of the Codable conformant type and generates CodingKeys enum cases accordingly. If a property has a CodableKey macro applied to it, the macro extracts the custom coding key from the macro’s arguments. Here’s a snippet of the macro implementation:

import SwiftSyntax
import SwiftSyntaxMacros
public enum CustomCodable: MemberMacro {
public static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
// Extract members of the declaration
let memberList = declaration.memberBlock.members
// Generate CodingKeys enum cases
let cases = memberList.compactMap({ member -> String? in
// Check if it's a property
guard
let propertyName = member.decl.as(VariableDeclSyntax.self)?.bindings.first?.pattern.as(IdentifierPatternSyntax.self)?.identifier.text
else {
return nil
}
// Check if it has a CodableKey macro
if let customKeyMacro = member.decl.as(VariableDeclSyntax.self)?.attributes.first(where: { element in
element.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.description == "CodableKey"
}) {
// Extract value from the Macro
let customKeyValue = customKeyMacro.as(AttributeSyntax.self)!.arguments!.as(LabeledExprListSyntax.self)!.first!.expression
return "case \(propertyName) = \(customKeyValue)"
} else {
return "case \(propertyName)"
}
})
// Construct CodingKeys enum
let codingKeys: DeclSyntax = """
enum CodingKeys: String, CodingKey {
\(raw: cases.joined(separator: "\n"))
}
"""
return [codingKeys]
}
}

Implementation Walkthrough

1. Macro Struct Declaration: We define an enum named CustomCodable that conforms to the MemberMacro protocol, indicating that this macro is responsible for generating member declarations within a type or extension.

2. Expansion Function: The expansion function is where the macro’s logic is implemented. It takes three parameters:

  • node: Represents the attribute syntax node associated with the macro expansion.
  • declaration: Represents the original type or extension declaration to which this macro is attached, and from which member declarations will be generated.
  • context: Provides contextual information about the macro expansion process.

3. Extract Declaration Members: We extract the list of members (properties, methods, etc.) from the provided declaration.

4. Process Each Member: We iterate over each member and extract the property name. If the member has a CodableKey macro applied, we extract the custom key value from its arguments.

5. Generate CodingKeys Enum: We generate the CodingKeys enum using the extracted property names and custom key values (if applicable).

6. Return Generated Declarations: It dynamically inspects the properties of the provided type or extension declaration, generates CodingKeys enum cases accordingly, and returns the generated declarations.

Example Usage

Applying the CustomCodable macro to a structure declaration exemplifies its usage:

@CustomCodable
struct CustomCodableString: Codable {
@CodableKey(name: "OtherName")
var propertyWithOtherName: String
var propertyWithSameName: Bool
}

Expanded Code

struct CustomCodableString: Codable {
enum CodingKeys: String, CodingKey {
case propertyWithOtherName = "OtherName"
case propertyWithSameName
}
var propertyWithOtherName: String
var propertyWithSameName: Bool
}

Conclusion

Member Macros in Swift offer a robust mechanism to extend type or extension declarations with new members seamlessly. By leveraging Member Macros, developers can enhance code organization, maintainability, and flexibility, unlocking Swift’s potential to build resilient and adaptable applications.

--

--

VINODH KUMAR

📱 Senior iOS Developer | Swift Enthusiast | Tech Blogger 🖥️ Connect with me on linkedin.com/in/vinodhkumar-govindaraj-838a85100