Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Function Body Macros で、SwiftUI の View に Accessi...

Avatar for Yuki Miida Yuki Miida
September 04, 2025

Function Body Macros で、SwiftUI の View に Accessibility Identifier を自動付与する/Function Body Macros: Autogenerate accessibility identifiers for SwiftUI Views

本スライドは Ebisu.mobile #11 〜会場に入れなかったトークたち〜 で使用したスライドになります。

Avatar for Yuki Miida

Yuki Miida

September 04, 2025
Tweet

More Decks by Yuki Miida

Other Decks in Technology

Transcript

  1. STORES גࣜձࣾ Yuki Miida (@miichan_ocha) Ebisu.mobile #11 ʙձ৔ʹೖΕͳ͔ͬͨτʔΫͨͪʙ 2025೥ 9݄4೔

    Function Body Macros ͰɺSwiftUI ͷ View ʹ Accessibility Identifier Λࣗಈ෇༩͢Δ
  2. ࣗݾ঺հ - Yuki Miida - GitHub: mii-chan - X: @miichan_ocha

    - Ϩδάϧʔϓ / iOSΤϯδχΞ - 2025೥3݄ೖࣾ - ྘஡ 🍵 ͕޷͖Ͱຖ೔ҿΜͰ͍·͢ 2
  3. 1. Ϙλϯͷϥϕϧ໊Ͱಛఆ - Pros - Modifier Λ෇͚Δख͕͔͔ؒΒͳ͍ - Cons -

    ҆ఆੑ͕௿͍ - ಉҰը໘ͷϥϕϧ໊ॏෳͰࣦഊ - ϩʔΧϥΠζ͞Εͨจࣈྻ - ୺຤ͷݴޠઃఆมߋͰςετ͕յΕΔ 6
  4. 2. Accessibility Identifier Ͱಛఆ - Pros - ҆ఆੑ͕ߴ͍ - Cons

    - Modifier Λ֤ View ʹ෇͚Δख͕͔͔ؒΔ 7
  5. Function Body MacroʢBody Macroʣ1/2 - طଘͷؔ਺ͷத਎Λॻ͖׵͑ΒΕΔ Swift Macro - Swift

    6.0 ͔Β࢖͑Δ 10 https://github.com/swiftlang/swift-evolution/blob/main/proposals/0415-function-body-macros.md#proposed-solution @Logged func g(a: Int, b: Int) -> Int { return a + b } func g(a: Int, b: Int) -> Int { log("Entering g(a: \(a), b: \(b))") defer { log("Exiting g") } return a + b }
  6. Function Body MacroʢBody Macroʣ2/2 - Read-Only Computed Property ʹ΋෇༩Ͱ͖ΔΒ͍͠ -

    Proposalௐ΂ 11 https://github.com/swiftlang/swift-evolution/blob/main/proposals/0415-function-body-macros.md#proposed-solution
  7. SwiftUI ͷ View ΁ͷద༻ - body property ʹ΋෇༩Ͱ͖Δʂʁ - ࠓճ͸

    body ಺ͷશͯͷ Button ʹ accessibilityIdentifier Λࣗಈ෇༩͠·͢ 12 @SomeBodyMacro var body: some View { VStack { … }
  8. એݴํ๏ - @attached(body) Ͱએݴ 15 @attached(body) public macro UITestable() =

    #externalMacro( module: "UITestableMacros", type: "UITestableMacro" )
  9. protocol BodyMacro Λ࣮૷͢Δ 16 /// Describes a macro that can

    create the body for a function that does not /// have one. public protocol BodyMacro: AttachedMacro { /// Expand a macro described by the given custom attribute and /// attached to the given declaration and evaluated within a /// particular expansion context. /// /// The macro expansion can introduce a body for the given function. static func expansion( of node: AttributeSyntax, providingBodyFor declaration: some DeclSyntaxProtocol & WithOptionalCodeBlockSyntax, in context: some MacroExpansionContext ) throws -> [CodeBlockItemSyntax] }
  10. Step1. TypeName ͷ prefix Λ࡞Δ - context.lexicalContext ʹܕ৘ใ͕഑ྻͰೖ͍ͬͯΔ 17 ///

    The macro expansion can introduce a body for the given function. static func expansion( of node: AttributeSyntax, providingBodyFor declaration: some DeclSyntaxProtocol & WithOptionalCodeBlockSyntax, in context: some MacroExpansionContext ) throws -> [CodeBlockItemSyntax]
  11. Step1. TypeName ͷ prefix Λ࡞Δ 18 private static func makeTypeHierarchyPrefix(

    from context: some MacroExpansionContext ) -> String { context.lexicalContext .compactMap { node in if let structDecl = node.as(StructDeclSyntax.self) { return structDecl.name.text } return nil } .reversed() .joined(separator: "_") }
  12. Step2. body ಺ͷ Button ʹ Modifier Λ෇༩ - SyntaxRewriter Λ࢖͏

    19 final class ButtonModifierRewriter: SyntaxRewriter { override func visit( _ node: FunctionCallExprSyntax ) -> ExprSyntax { // Detect Button call guard let buttonCall = findButtonCall(node) else { return super.visit(node) } … }
  13. Step2. body ಺ͷ Button ʹ Modifier Λ෇༩ - Modifier chain

    ͷ్தͷ৔߹͸εΩοϓ͢Δ 21 // Skip if in the middle of a modifier chain if let parent = node.parent, parent.is(MemberAccessExprSyntax.self) { return super.visit(node) }
  14. Step2. body ಺ͷ Button ʹ Modifier Λ෇༩ 22 let newExpr

    = FunctionCallExprSyntax( calledExpression: MemberAccessExprSyntax( base: ExprSyntax(node), period: .periodToken(leadingTrivia: newlineIndent), name: .identifier("accessibilityIdentifier") ), leftParen: .leftParenToken(), arguments: LabeledExprListSyntax { LabeledExprSyntax( expression: ExprSyntax(StringLiteralExprSyntax(content: "\(typeHierarchyPrefix)_\(label)")) ) }, rightParen: .rightParenToken() ) return ExprSyntax(super.visit(newExpr))
  15. ·ͱΊ - Function Body Macro Ͱ Modifier Λࣗಈ෇༩Ͱ͖ͨ - ৭ʑศརʹ׆༻Ͱ͖Δέʔε͕͋Γͦ͏

    - طଘͷؔ਺ͷத਎Λॻ͖׵͑ΔͷͰ΍Γ͗͢ʹ͸஫ҙ - Կ͔ςετɾσόοά༻ͷػೳΛ଍ͯ͠ #if DEBUG Ͱғͬͯ࢖ͬͨΓ͢ Δͷ͸ྑ͍׆༻ํ๏͔΋ - Read-Only Computed Property ͕ରԠ͞ΕΔͱྑ͍ͳ 🙏 32