autoscale: true slide-transition: true footer: Chris Patterson [email protected]
- Reducing Boilerplate with Swift Macros
- Useful Macro Examples
- Building an Auto API Client
- Protocol Witness Pattern
- Doximity Examples
- Compile-time code generation
- Project-specific & Shareable via Packages
- Introduced in Swift 5.9 / 6
- Reduce duplication, increase expressiveness
- 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 macros are typed according to how they modify the thing they attach to.
PeerMacroadds a new peer typeMemberMacroadds a new member to the thingAttributeMacroadds attributes to the thingAccessorMacro- adds accessors to the thing
- Reducing boilerplate for common design patterns
@Init,@Codable,@APIProvider
- Creating consistent logging/debugging
#myPrintMacro(.trace, "Foo.decode() called")
- Property wrappers or
Codablesyntheses@Shadow,@LockIsolated,@CodingKeyed
Create template files
swift package init --type macro
[.column]
- In Package.swift
- use
swift-tools-version> 5.9 - add necessary
imports
- use
[.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- ensure correct min platform version is set
platforms: [.macOS(.v10_15), .iOS(.v13)],[.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"]
),- add SwiftSyntax package dependency
dependencies: [
.package(
url: "https://github.com/swiftlang/swift-syntax.git",
from: "601.0.0-latest"
),
],- 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]
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"),
]
),
]
)- 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
}[.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
}
}- Blueprints declare endpoints
- Target type need only declare a host URL
- Macro generates protocol witness properties and methods
- Macro generates mock for testing
[.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
}
}protocol APIBlueprint {
associatedType Response
var path: String { get }
var method: Method { get }
var parameters: [String: String] { get }
}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)
}
}
}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))
}
)
}
}[.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")> A technique to achieve type erasure or abstraction over functionality using a value (struct) rather than conforming directly to a protocol.
struct Equating<T> {
let equals: (T, T) -> Bool
}
let intEquator = Equating<Int> { $0 == $1 }
intEquator.equals(3, 3) // truelet 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- Overcome limitations of protocol-oriented programming: associated types,
selfrequirements - Dependency injection without existential types
- Customizable behavior without subclassing
- Enables flexible composition
“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.
protocol Repository {
associatedtype Item
func fetch() -> Item
}
// ❌ Error: Protocol 'Repository' can
// only be used as a generic constraint...
let repo: Repository “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.
protocol Drawable { func draw() }
// ❌ Use of protocol 'Drawable' as a
// type must be written 'any Drawable'
let shape: Drawable
// ✅
let shape: any Drawable “Ambiguous use of ‘X’” with protocol extensions"
Occurs when multiple extensions provide default implementations.
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() “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.
protocol Parser {
associatedtype Output
func parse() -> Output
}
func run(p: any Parser) {
// ❌ Error: Value of type
// 'any Parser' has no member 'parse'
p.parse()
}> Using **Protocol Witnesses** in each of these cases eliminates the problem, since they are simple structs.
[.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!
}[.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
)
}()
}-
Video
-
Blogs
Let's look at some real-world examples...
@LockIsolated@Init
@Witness
**Thank You!**