Skip to content

Instantly share code, notes, and snippets.

@cpatterson
Last active September 18, 2025 14:59
Show Gist options
  • Select an option

  • Save cpatterson/4a4e1d2a74a792aabd54924b9ea424d5 to your computer and use it in GitHub Desktop.

Select an option

Save cpatterson/4a4e1d2a74a792aabd54924b9ea424d5 to your computer and use it in GitHub Desktop.
Indy Cocoaheads 9/17/2025 - Can I Get a `@Witness`? Intro to Swift Macros and the Protocol Witness Pattern

autoscale: true slide-transition: true footer: Chris Patterson [email protected]

Can I Get a @Witness?

Intro to Swift Macros and the Protocol Witness Pattern


Agenda

  • Reducing Boilerplate with Swift Macros
  • Useful Macro Examples
  • Building an Auto API Client
  • Protocol Witness Pattern
  • Doximity Examples

Swift Macros: Overview

  • Compile-time code generation
  • Project-specific & Shareable via Packages
  • Introduced in Swift 5.9 / 6
  • Reduce duplication, increase expressiveness

Swift Macros: WWDC23 Videos


Swift Macro Types

  • Freestanding: #macroName
    • may be called anywhere like a function
    • may have arguments in parentheses
  • Attached: @MacroName
    • must be "attached" to a type declaration
    • may have arguments in parentheses

Attached Macro Types

Attached macros are typed according to how they modify the thing they attach to.

  • PeerMacro adds a new peer type
  • MemberMacro adds a new member to the thing
  • AttributeMacro adds attributes to the thing
  • AccessorMacro - adds accessors to the thing

Example Use Cases

  • Reducing boilerplate for common design patterns
    • @Init, @Codable, @APIProvider
  • Creating consistent logging/debugging
    • #myPrintMacro(.trace, "Foo.decode() called")
  • Property wrappers or Codable syntheses
    • @Shadow, @LockIsolated, @CodingKeyed

Implementing a Macro

Create template files

swift package init --type macro


Implementing a Macro

[.column]

  • In Package.swift
    • use swift-tools-version > 5.9
    • add necessary imports

[.column]

// swift-tools-version: 6.1

// The swift-tools-version 
// declares the minimum 
// version of Swift required 
// to build this package.

import PackageDescription
import CompilerPluginSupport

Implementing a Macro

  • ensure correct min platform version is set
    platforms: [.macOS(.v10_15), .iOS(.v13)],

Implementing a Macro

[.column]

  • add .macro() target for implementation
  • add macro .target() used by other targets

[.column]

.macro(
	name: "MyMacros",
	dependencies: [
		.product(
			name: "SwiftSyntaxMacros", 
			package: "swift-syntax"
		),
		.product(
			name: "SwiftCompilerPlugin", 
			package: "swift-syntax"
		)
	]
),
.target(
	name: "MyMacros", 
	dependencies: ["MyMacros"]
),

Implementing a Macro

  • add SwiftSyntax package dependency
    dependencies: [
        .package(
        	url: "https://github.com/swiftlang/swift-syntax.git", 
        	from: "601.0.0-latest"
        ),
    ],

Implementing a Macro

  • add macro dependency to other .target()s
        // A client of the library, which is able to use the macro in its own code.
        .executableTarget(name: "MyMacrosClient", dependencies: ["MyMacros"]),

        // A test target used to develop the macro implementation.
        .testTarget(
            name: "MyMacrosTests",
            dependencies: [
                "MyMacros",
                .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
            ]
        ),

[.column]

Complete

Package.swift

Demo

[.column]

// swift-tools-version: 6.1
import PackageDescription
import CompilerPluginSupport

let package = Package(
    name: "MyMacros",
    platforms: [.macOS(.v10_15), .iOS(.v13)],
    products: [
        .library(
        	name: "MyMacrosLib", 
        	targets: ["MyMacros"]
        ),
        .executable(
        	name: "MyMacrosClient", 
        	targets: ["MyMacrosClient"]
        ),
    ],
    dependencies: [
        .package(
        	url: "https://github.com/swiftlang/swift-syntax.git", 
        	from: "601.0.0-latest"
        ),
    ],
    targets: [
        .macro(
        	name: "MyMacros", 
        	dependencies: [
            	.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
            	.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
        	]
        ),
        .target(
        	name: "MyMacrosLib", 
        	dependencies: ["MyMacros"]
        ),
        .executableTarget(
        	name: "MyMacrosClient", 
        	dependencies: ["MyMacrosLib"]
        ),
        .testTarget(
        	name: "MyMacrosTests", 
        	dependencies: [
            	"MyMacros",
            	.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
        	]
        ),
    ]
)

Simple Macro Example: @Init

  • Auto generates a public initializer for a public struct, class, or actor
  • Great for data types in shared packages
@Init
public struct User {
    let id: UUID
    let name: String
}

Simple Macro Example: @Init

Generated Code

[.code-highlight: 5-11]

public struct User {
    let id: UUID
    let name: String
    
    public init(
        id: UUID,
        name: String
    ) {
        self.id = id
        self.name = name
    }
}

Macro Example: @APIProvider

  • Blueprints declare endpoints
  • Target type need only declare a host URL
  • Macro generates protocol witness properties and methods
  • Macro generates mock for testing

Macro Example: GetUser Blueprint

[.code-highlight: all] [.code-highlight: 3]

@APIProvider
struct MyRestAPIBlueprints {
	struct GetUser: APIBlueprint {
		typealias Response = User
		var path: String { "/users/\(id)" }
		let method = .get
		let parameters: [String: String] = [:]
		
		let id: String
	}
}

Macro Example: APIBlueprint Protocol

protocol APIBlueprint {
    associatedType Response
    var path: String { get }
    var method: Method { get }
    var parameters: [String: String] { get }
}

Macro Example: GetUser Blueprint

Generated Code

extension MyRestAPIBlueprints {
	struct Provider {
		private let host: URL
		private let _getUser: (String) async -> GetUser.Response?

		public init(
			host: URL,
			getUser: @escaping (String) -> GetUser.Response?
		) {
			self.host = host
			self.getUser = getUser
		}

		public func getUser(id: String) async -> GetUser.Response? {
			await _getUser(id)
		}
	}
}

Macro Example: GetUser Blueprint

Prod Implementation

extension MyRestAPIBlueprints.Provider {
	public static func live(host: URL) -> MyRestAPIBlueprints.Provider {
		.init(
			host: host,
			getUser: { (id: String) async -> User in
				// async function request(_ blueprint: APIBlueprint) is defined elsewhere
				// it builds an URLRequest then calls URLSession.dataTask()
				await Self.request(GetUser(id: id))
			}
		)
	}
}

Macro Example: GetUser Blueprint

[.column]

Generated Code

#ifdef DEBUG
extension MyRestAPIBlueprints.Provider {
	public static func mock(
		getUser: @escaping (id: String) async 
			-> GetUser.Response
	) -> MyRestAPIBlueprints.Provider {
		.init(
			host: URL("mock.myrestapi.dev"),
			getUser: getUser
		)
	}
}
#endif

[.column]

Test Code

let mockAPI = MyRestAPIBlueprints.Provider
	.mock(
		getUser: { _ in 
			User.sample 
		}
	)
	
...

let mockUser = await mockAPI.getUser(id: "1234")

Protocol Witness Pattern


> A technique to achieve type erasure or abstraction over functionality using a value (struct) rather than conforming directly to a protocol.

Equator Example

struct Equating<T> {
    let equals: (T, T) -> Bool
}

let intEquator = Equating<Int> { $0 == $1 }
intEquator.equals(3, 3) // true

Equator Example

let dayEquator = Equating(
    equals: { (date1: Date, date2: Date) -> Bool in
        let extractDay = { Calendar.current.dateComponents([.day], from: $0) }
        return extractDay(date1) == extractDay(date2)
    }
)

let now = Date()
let thirtySecsFromNow = now.advanced(by: 30)
let tomorrow = now.advanced(by: 60*60*24)

dayEquator.equals(now, thirtySecsFromNow) // true
dayEquator.equals(now, tomorrow) // false

Protocol Witness Pattern

Benefits

  • Overcome limitations of protocol-oriented programming: associated types, self requirements
  • Dependency injection without existential types
  • Customizable behavior without subclassing
  • Enables flexible composition

Common Protocol-Related Errors in Swift

“Protocol ‘X’ can only be used as a generic constraint because it has Self or associated type requirements”

Occurs when trying to use a protocol with an associatedType or Self requirement as a concrete type.


Common Protocol-Related Errors in Swift

protocol Repository {
    associatedtype Item
    func fetch() -> Item
}

// ❌ Error: Protocol 'Repository' can 
// only be used as a generic constraint...
let repo: Repository   

Common Protocol-Related Errors in Swift

“Use of protocol ‘X’ as a type must be written ‘any X’”

Swift’s existential any keyword forces you to distinguish between existential and generic use.


Common Protocol-Related Errors in Swift

protocol Drawable { func draw() }

// ❌ Use of protocol 'Drawable' as a 
// type must be written 'any Drawable'
let shape: Drawable   

// ✅
let shape: any Drawable  

Common Protocol-Related Errors in Swift

“Ambiguous use of ‘X’” with protocol extensions"

Occurs when multiple extensions provide default implementations.


Common Protocol-Related Errors in Swift

protocol Printer { 
	func printIt() 
}
extension Printer { 
	func printIt() { print("Default") } 
}
struct ConsolePrinter: Printer { 
	func printIt() { print("Custom") } 
}
let cp: Printer = ConsolePrinter()
 // ⚠️ Ambiguity can occur 
 // with overlapping defaults
cp.printIt()  

Common Protocol-Related Errors in Swift

“Value of type ‘any X’ has no member ‘foo’”

When calling a member that depends on Self or associatedType, but you’re holding a protocol existential.


Common Protocol-Related Errors in Swift

protocol Parser {
    associatedtype Output
    func parse() -> Output
}

func run(p: any Parser) {
	// ❌ Error: Value of type 
	// 'any Parser' has no member 'parse'
    p.parse()
}

Protocol Witness Pattern


> Using **Protocol Witnesses** in each of these cases eliminates the problem, since they are simple structs.

Protocol Witness Pattern

[.column] Protocol

protocol Parser {
    associatedtype Output
    func parse() -> Output
}

func run(p: any Parser) {
	// ❌ Error: Value of type 
	// 'any Parser' has no member 'parse'
    p.parse()   
}

[.column] Protocol Witness

struct Parser<Output> {
	let parse: () -> Output
	
	init(
		parse: @escaping () -> Output
	) {
		self.parse = parse
	}
}

func run(p: Parser) {
    p.parse()   // ✅ No problem!
}

Customizing behavior

[.column]

// Protocol Witness
struct VOIPSession {
	let placeCall: (to: String) -> VOIPCall
	let endCall: (VOIPCall) -> Void
	init(
		placeCall: @escaping (to: String) -> VOIPCall,
		endCall: @escaping (VOIPCall) -> Void
	) {
		self.placeCall = placeCall
		self.endCall = endCall
	}
}

[.column]

import TwilioVoice

// Twilio Implementation
extension VOIPSession {
	struct TwilioImplementation {
		let twilioCall: TwilioVoice.Call
		func placeCall(to: String) -> VOIPCall {
			self.twilioCall = TwilioVoiceSDK.connect(to: to)
			return VOIPCall(from: twilioCall)
		}
		func endCall(_ voipCall: VOIPCall) {
			guard voipCall.id == twilioCall.id else { return }
			TwilioVoiceSDK.disconnect(twilioCall)
			twilioCall = nil
		}
	}
}

// Instance of `VOIPSession` using `TwilioImplementation`
extension VOIPSession {
	static let twilio = {
		let impl = TwilioImplementation()
		return VOIPSession(
			placeCall: impl.placeCall,
			endCall: impl.endCall
		)
	}()
}

Protocol Witness Pattern: Links


Doximity Macros

Let's look at some real-world examples...

  • @LockIsolated
  • @Init

And the Grand Finale...

@Witness


Q&A


**Thank You!**
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment