Mobile Development 20 min read

Understanding Swift Macros in Swift 5.9 and Their Applications

Swift 5.9’s new macro system lets developers generate type‑safe code at compile time via external plug‑ins, using role‑based declarations such as @attached and @freestanding to create properties, extensions, or warnings, thereby reducing repetitive boiler‑plate and improving safety in large‑scale architectures like MVVP modules and exposure frameworks.

Bilibili Tech
Bilibili Tech
Bilibili Tech
Understanding Swift Macros in Swift 5.9 and Their Applications

Swift Macro was introduced in Swift 5.9 and Xcode 15, providing a way to reduce repetitive code by generating code at compile time.

Unlike Objective‑C #define macros, Swift macros are expanded during compilation, allowing type‑safe checks and better diagnostics.

Swift macros are mainly external macros handled by a Compiler Plug‑in. A macro consists of a role declaration (e.g., @attached(member), @freestanding(expression)) and a method declaration marked with the macro and #externalMacro keywords.

#define SQUARE(x)  x * x
// ❌ 展开后逻辑不符
int a = 5;
int result = SQUARE(a + 1);      // 结果为 5 + 1 * 5 + 1 = 11
// ❌ 缺少类型检查
int result = SQUARE("BiliBili")  // 结果为 BiliBili * BiliBili

Example: a DefaultAge macro that generates an Int32 age property.

public macro DefaultAge(_ age: Int32)
@DefaultAge(12.0)
class Test {}
class Test {
  var age: Int32 = ❌ // Cannot convert value of type 'Double' to specified type 'Int32'
}
public macro DefaultAge(_ age: Int32) = #externalMacro(
    module: "宏实现的模块",
    type: "宏实现的类型"
)

During expansion the compiler builds an AST, passes it to the plug‑in, which returns a new AST that is merged into the original source.

ClassDecl 
    -- MemberBlock
        -- MemberBlockItemList
            -- MemberBlockItem  
                -- name (String) == "BiliBili"
@DefaultAge(12)
class Test {
    var name: String = "BiliBili"
}
ClassDecl 
    -- MemberBlock
        -- MemberBlockItemList
            -- name (String) == "BiliBili"
            -- MemberBlockItem
                -- age  (Int)    == 12

Macro role definitions:

@attached(member, names: arbitrary)
@attached(peer, names: overloaded)
@freestanding(expression)
@freestanding(declaration)

Freestanding expression macro example that logs function info:

@freestanding(expression)
public macro function()
func logInfo(function: String = #function, lineNum: UInt = #line) {}

Freestanding declaration macro that creates a warning:

@freestanding(declaration)
public macro warning(_ message: String) -> ()
#warning("插入一句警告")

Attached macros can generate members, extensions, or accessors. Example of a member macro that creates an age property:

public macro DefaultAge(_ age: Int32) = #externalMacro(
    module: "Macro",
    type: "DefaultAgeMacro"
)

Application scenario 1 – modular code generation for BiliBili’s MVVP modules:

protocol BiliModule: AnyObject {
    associatedtype ModuleView: BiliModuleView
    associatedtype ModulePresenter: BiliModulePresenter
    static var moduleIdentifier: BiliModuleIdentifier { get }
    var view: ModuleView? { get set }
    var presenter: ModulePresenter { get }
    var moduleSize: CGSize { get }
}
class TabModule: BiliModule {
    typealias ModuleView = TabView
    typealias ModulePresenter = TabPresenter
    static var moduleIdentifier: BiliModuleIdentifier { .TAB }
    var view: TabView?
    var presenter: TabPresenter
    required init(context: BiliContext, data: BiliModuleInfo) {
        self.presenter = TabPresenter(context: context, data: data)
    }
}
@attached(member, names: arbitrary)
@attached(extension, conformances: BiliModule)
public macro BiliModuleDefine
(_ presenter: P, _ view: V, _ type: BiliModuleIdentifier)

Application scenario 2 – exposure system macros that automatically add container, target and region logic.

class ExposureCollectionView: UICollectionView, BiliExposureContainer {
    var exposureCheckFlag: Int32 = 0
    var exposureContext: BiliExposureContext = BiliExposureContext()
    public func visibleExposureTargets() -> [UIView]? { return self.visibleCells }
}
@ExposureContainer
class ExposureCollectionView: UICollectionView {}
@ExposureTarget(100, true)
class ExposeData: NSObject {}
@ExposureRegion
class VipCollectionViewCell: UICollectionViewCell, BiliExposureRegion {
    @ExposureTargetMember
    var exposureData: ExposureData?
    func exposureTarget() -> BiliExposureTarget? { return exposureData }
}

These examples demonstrate how Swift macros can encapsulate repetitive patterns, enforce compile‑time safety, and streamline large‑scale code bases.

Code GenerationiOSmetaprogrammingSwiftCompiler PluginMacro
Bilibili Tech
Written by

Bilibili Tech

Provides introductions and tutorials on Bilibili-related technologies.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.