Different macro implementation for different platforms

I am trying to create a macro that allows me to reference a color by name in the asset bundle.

For example, being able to do something like this:

let myColor = #color("MyColor")

On macOS, I would want this to expand to:

"NSColor(named: \(argument)) ?? NSColor.clear"

On iOS, I would want it to expand to:

"UIColor(named: \(argument))"

I have written the following macro:

public struct ColorMacro: ExpressionMacro {
	public static func expansion(of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) throws -> ExprSyntax {
		guard let argument = node.argumentList.first?.expression else {
			throw MacroError.invalidArguments
		}

#if canImport(AppKit)
		return "NSColor(named: \(argument)) ?? NSColor.clear"
#elseif canImport(UIKit)
		return "UIColor(named: \(argument))"
#else
		#error("Unsupported platform")
#endif
	}
}

And...

#if canImport(AppKit)
import AppKit

@freestanding(expression)
public macro color(_ named: String) -> NSColor = #externalMacro(module: "SwatchbookMacrosMacros", type: "ColorMacro")
#elseif canImport(UIKit)
import UIKit

@freestanding(expression)
public macro color(_ named: String) -> UIColor = #externalMacro(module: "SwatchbookMacrosMacros", type: "ColorMacro")
#endif

This works fine when building for macOS, but when using in a target that is being built for iOS, it seems to still be trying to use the AppKit branch and referencing NSColor.

Am I doing something wrong? Is it using the platform that is being built on to determine availability, rather than the target platform?

This also doesn't work:

public struct ColorMacro: ExpressionMacro {
	public static func expansion(of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) throws -> ExprSyntax {
		guard let argument = node.argumentList.first?.expression else {
			throw MacroError.invalidArguments
		}

		return """
		#if canImport(AppKit)
				NSColor(named: \(argument)) ?? NSColor.clear
		#elseif canImport(UIKit)
				UIColor(named: \(argument))
		#else
				#error("Unsupported platform")
		#endif
		"""
	}
}

Error: Expected macro expansion to produce an expression

The problem here is that an #if block is an IfConfigDeclSyntax, rather than an expression. You could wrap the whole thing in an immediately executed closure to make it into an expression though:

return """
  {
    #if canImport(AppKit)
      NSColor(named: \(argument)) ?? NSColor.clear
    #elseif canImport(UIKit)
      UIColor(named: \(argument))
    #else
      #error("Unsupported platform")
    #endif
  }()
  """

But if you're going to do all that, it might just be cleaner and simpler to create a helper function in your macro's library target and call that:

func __colorHelper(_ name: String) {
  #if canImport(AppKit)
    NSColor(named: name) ?? NSColor.clear
  #elseif canImport(UIKit)
    UIColor(named: name)
  #else
    #error("Unsupported platform")
  #endif
}

And have your macro implementation call that function:

return """
  YourModuleName.__colorHelper(\(argument))
  """
1 Like

Ah I see! So the macro itself is platform agnostic, but the macro library target can declare the helper code targeting the specific platform that's being used, to actually do the work. Ok, that makes more sense.

Still discovering macros, and there seems to be many moving parts that are a challenge to get right.

Thanks.