Swift macros: @freestanding(expression) example

Swift macros were introduced in WWDC 2023 and immediately caught my attention. I highly recommend two WWDC videos: “Write Swift Macros” and “Expand on Swift Macros,” where macros are discussed in detail.

The first thing that came to my mind when watching the Swift macros videos was the possibility of moving all kinds of utility and helper code to macros. That way, our codebase wouldn’t be “polluted” with utility stuff. Moreover, all the macros are highly testable, and test-driven development works like a charm with them.

Is this a good practice? Well, I’m still not sure, but I think it’s worth trying.

So, right after I became a little bit familiar with Swift macros, I wanted to try and solve a real-life problem.

When developing a UI, it’s very common to receive colors in hex format. Perhaps you have written a helper function in your code that looks something like this, as both UIColor and SwiftUI Color accept red, green, and blue parameters in the range from 0 to 1:

func hexStringToCGFloat(hex:String) -> (red: CGFloat, green: CGFloat, blue: CGFloat) {
    var cString:String = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()

    if (cString.hasPrefix("#")) {
        cString.remove(at: cString.startIndex)
    }

    if ((cString.count) != 6) {
        return (0,0,0)
    }

    var rgbValue:UInt64 = 0
    Scanner(string: cString).scanHexInt64(&rgbValue)

    return (
        red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0,
        green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0,
        blue: CGFloat(rgbValue & 0x0000FF) / 255.0
    )
}

This function basically converts hex string to red, green and blue CGFloat.

My idea was to move this code to Swift macro. That way I want be able to use SwiftUI color like this:

let myColor = #colorfy("#FFFF")

and hope the colorfy macro will expand into

let myColor = Color(red: 0.0, green: 0.0, blue: 0.0)

@freestanding(expression) Swift macro implementation

By investigating macro roles I figured out @freestanding(expression) macro is what I need for this use case.

First I started by implementing colorfy macro signature:

@freestanding(expression)
public macro colorfy(_ value: String) -> Color = #externalMacro(module: "MyMacros", type: "ColorfyMacro")

Here, I am simply defining a string as an input argument, and I want the macro to return a SwiftUI Color. I am also specifying that the role of the macro is @freestanding(expression).

The next step I took was moving my hexStringToCGFloat(_:String) function into the macro implementation file.

After that, I started with the macro implementation:

public struct ColorfyMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) -> ExprSyntax {
        guard let argument = node.argumentList.first?.expression else {
            fatalError("compiler bug: the macro does not have any arguments")
        }

        guard let stringArgument = StringLiteralExprSyntax(argument),
              let stringSegment = stringArgument.segments.first,
              let stringContent = StringSegmentSyntax(stringSegment)?.content.text else {
            fatalError("compiler bug: the macro does not have string argument")
        }
        
        if let firstChar = stringContent.first,
           firstChar == "#" {
            var newString = stringContent
            newString.removeFirst()
            if newString.filter(\.isHexDigit).count != newString.count {
                fatalError("compiler bug: argument is not hex")
            }
        }
        else {
            guard stringContent.filter(\.isHexDigit).count == stringContent.count else {
                fatalError("compiler bug: argument is not hex")
            }
        }

		let values = hexStringToCGFloat(hex: stringContent)
        
        let args = """
        Color(red: \(values.red), green: \(values.green), blue: \(values.blue))
        """
        
        let expr = StringLiteralExprSyntax(openQuote: "", content: args, closeQuote: "")
        
        return ExprSyntax(expr)
    }
}


Here, I am implementing a public struct called ColorfyMacro, which conforms to the ExpressionMacro protocol. In this case, we need to implement the public static func expansion(of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) -> ExprSyntax function.

From line 6 to line 28, I am performing various checks to ensure that the input string is in the expected format.

On line 30, I am calling the hexStringToCGFloat(_:String) function, and then on line 32, I am defining the Color.

Finally, as the last step, I am converting my return result to StringLiteralExprSyntax and ExprSyntax.

Swift macro unit test

I also wrote a simple unit test to check if everything works as expected:

func testColorfyMacro() {
        assertMacroExpansion(
            """
            #colorfy("#FFFF")
            """,
            expandedSource: """
            Color(red: 0.0, green: 0.0, blue: 0.0)
            """,
            macros: testMacros
        )
    }

Using #colorfy Swift macro

After all of the hustle I managed to simply use macro in my test project:

struct ContentView: View {
    let backgroundColor: Color = #colorfy("#c8e3d2")
    
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world!")
        }
        .background(backgroundColor)
        .padding()
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *