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

20分でわかる!速習resultBuilder(iOSDC 2022)

たまねぎ
September 12, 2022

20分でわかる!速習resultBuilder(iOSDC 2022)

たまねぎ

September 12, 2022
Tweet

More Decks by たまねぎ

Other Decks in Technology

Transcript

  1. ɹɹ ©︎ hey, Inc 20分でわかる!速習resultBuilder ヘイ株式会社 テクノロジー部門 モバイルアプリケーション本部 STORES レジ

    iOSエンジニア たまねぎ(Takuya Yokoyama) iOSDC2022
  2. ɹ ɹ 自己紹介 Profile { Name("ͨ·Ͷ͗") Twitter("@_chocoyama") Work { Company("hey

    inc") Product("STORES Regi") Role("Mobile Engineer") } Skill { SwiftUI() Flutter() Compose() } }
  3. ɹ ɹ

  4. ɹ ɹ 発表のゴール •resultBuilderとは何かがわかる •実装方法や使いどころのイメージが持てる

  5. ɹ ɹ アジェンダ •resultBuilderとは •仕組みと実装方法 •導入事例の紹介 •まとめ

  6. ɹ ɹ resultBuilderとは

  7. ɹ ɹ 複数要素の組み合わせ作業を、 命令的ではなく、宣言的に行えるようにする機能

  8. ɹ ɹ → 冗長な実装をシンプルに 複数要素の組み合わせ作業を、 命令的ではなく、宣言的に行えるようにする機能

  9. ɹ ɹ resultBuilderとは何か? • Swift5.4で追加されたAttribute • 列挙された要素を収集し、結果の値に変換して取得できる • Swiftで内部DSLを作るための補助機能 •

    特定領域で効果を発揮するミニチュア言語を作れる
  10. ɹ ɹ どこで使われてると思いますか? (ここから5秒間、心で念じる or コメント or Tweetする時間)

  11. ɹ ɹ どこで使われている? •ViewBuilder:SwiftUIのレイアウトを記述する時 •RegexComponentBuilder:正規表現を記述する時(NEW!)

  12. ɹ ɹ どこで使われている? let word = OneOrMore(.word) let emailPattern =

    Regex { Capture { ZeroOrMore { word "." } word } "@" Capture { word OneOrMore { "." word } } } struct AlbumDetail: View { var album: Album var body: some View { List(album.songs) { song in HStack { Image(album.cover) VStack(alignment: .leading) { Text(song.title) Text(song.artist.name) .foregroundStyle(.secondary) } } } } } ViewBuilder RegexComponentBuilder
  13. ɹ ɹ let word = OneOrMore(.word) let emailPattern = Regex

    { Capture { ZeroOrMore { word "." } word } "@" Capture { word OneOrMore { "." word } } } struct AlbumDetail: View { var album: Album var body: some View { List(album.songs) { song in HStack { Image(album.cover) VStack(alignment: .leading) { Text(song.title) Text(song.artist.name) .foregroundStyle(.secondary) } } } } } どこで使われている? ViewBuilder RegexComponentBuilder
  14. ɹ ɹ let word = OneOrMore(.word)

  15. ɹ ɹ どのように表現できる? 1.値の列挙にカンマが必要ない 2.if文やfor文などの制御構文も利用可能 @ArrayBuilder func someFunction2() -> [Int]

    { 1 2 if true { 3 } for i in (4...6) { i } }
  16. ɹ ɹ 何ができる? 1.値の列挙にカンマが必要ない 2.if文やfor文などの制御構文も利用可能 @ArrayBuilder func someFunction2() -> [Int]

    { 1 2 if true { 3 } for i in (4...6) { i } } ಉ݁͡Ռ͕ಘΒΕΔ = [1, 2, 3, 4, 5, 6] func someFunction1() -> [Int] { var result = [ 1, 2 ] if true { result.append(3) } for i in (4...6) { result.append(i) } return result }
  17. ɹ ɹ なぜ必要? struct AlbumDetail: View { var album: Album

    var body: some View { List(album.songs) { song in HStack { Image(album.cover) VStack(alignment: .leading) { Text(song.title) Text(song.artist.name) .foregroundStyle(.secondary) } } } } }
  18. ɹ ɹ resultBuilderを使わずに同じことを実現する場合 struct AlbumDetail: View { var album: Album

    var body: some View { List(album.songs) { song in HStack { Image(album.cover) VStack(alignment: .leading) { Text(song.title) Text(song.artist.name) .foregroundStyle(.secondary) } } } } } struct AlbumDetail: View { var album: Album var body: some View { var items: [View] = [] for song in album.songs { let hStack = HStack( contents: [ Image(album.cover), VStack( alignment: .leading, contents [ Text(song.title), Text(song.artist.name) .foregroundStyle(.secondary) ] ) ] ) items.append(hStack) } return List(contents: items) } } ※ ͜ͷίʔυ͸͋͘·ͰΠϝʔδͰ͢
  19. ɹ ɹ もしresultBuilderを使わずに同じことを実現すると • 制御文や変数宣言でコードが肥大化 • 階層構造が複雑で、結果が想像しづらい • 変更コストが大きい struct

    AlbumDetail: View { var album: Album var body: some View { var items: [View] = [] for song in album.songs { let hStack = HStack( contents: [ Image(album.cover), VStack( alignment: .leading, contents [ Text(song.title), Text(song.artist.name) .foregroundStyle(.secondary) ] ) ] ) items.append(hStack) } return List(contents: items) } } ※ ͜ͷίʔυ͸͋͘·ͰΠϝʔδͰ͢
  20. ɹ ɹ resultBuilder 何ができる?     シンプルな記述で、複数要素の収集と結果値への変換ができる どこで使われている? SwiftUIやRegexなどで用いられている なぜ必要?      可読性・保守性の向上が期待できるため

  21. ɹ ɹ SwiftUI, Regex以外に使い道あるの?

  22. ɹ ɹ SwiftUI, Regex以外に使い道あるの? ↓ カスタムのresultBuilderの作成で 実装効率を高められるケースがある

  23. ɹ ɹ 例えばこういう時 let mas = NSMutableAttributedString(string: “") mas.append(NSAttributedString( string:

    "Hello world", attributes: [ .font: UIFont.systemFont(ofSize: 24), .foregroundColor: UIColor.red ] )) mas.append(NSAttributedString( string: "\n" )) mas.append(NSAttributedString( string: "with Swift", attributes: [ .font: UIFont.systemFont(ofSize: 20), .foregroundColor: UIColor.orange ] ))
  24. ɹ ɹ • 要素指定と関係ない記述が多い • 構造がわかりづらい • 変更による修正量が多い 例えばこういう時 let

    mas = NSMutableAttributedString(string: “") mas.append(NSAttributedString( string: "Hello world", attributes: [ .font: UIFont.systemFont(ofSize: 24), .foregroundColor: UIColor.red ] )) mas.append(NSAttributedString( string: "\n" )) mas.append(NSAttributedString( string: "with Swift", attributes: [ .font: UIFont.systemFont(ofSize: 20), .foregroundColor: UIColor.orange ] ))
  25. ɹ ɹ 例えばこういう時 let mas = NSMutableAttributedString(string: “") mas.append(NSAttributedString( string:

    "Hello world", attributes: [ .font: UIFont.systemFont(ofSize: 24), .foregroundColor: UIColor.red ] )) mas.append(NSAttributedString( string: "\n" )) mas.append(NSAttributedString( string: "with Swift", attributes: [ .font: UIFont.systemFont(ofSize: 20), .foregroundColor: UIColor.orange ] )) let attributedString = NSAttributedString { AText("Hello world") .font(.systemFont(ofSize: 24)) .foregroundColor(.red) LineBreak() AText("with Swift") .font(.systemFont(ofSize: 20)) .foregroundColor(.orange) } • 要素指定と関係ない記述が多い • 構造がわかりづらい • 変更による修正量が多い • 要素指定以外の記述が少ない • 構造がわかりやすい • 変更による修正量が少ない // https://github.com/ethanhuang13/NSAttributedStringBuilder
  26. ɹ ɹ awesome-result-builders https://github.com/carson-katri/awesome-result-builders // Data Data { [UInt8(0)] UInt8(1)

    Int8(2) "\u{03}" Int16(1284) if dataClause { CustomData() } } // Parsing let capture = HTML { TryCapture("#hello") { (element: HTMLElement?) -> String? in return element?.textContent } Local("#group") { Capture("h1", transform: \.textContent) Capture("h2", transform: \.textContent) } } // Networking Request { Url("https://jsonplaceholder.typicode.com/posts") Method(.post) Header.ContentType(.json) Body(Json([ "title": "foo", "body": "bar", "usedId": 1 ]).stringified) } // GraphQL Operation(.query) { Add(\.country, alias: "canada") { Add(\.name) Add(\.continent) { Add(\.name) } }.code("CA") }
  27. ɹ ɹ 仕組みと実装方法

  28. ɹ ɹ 作る @ArrayBuilder func someFunction() -> [Int] { 1

    2 3 } someFunction() // [1, 2, 3]
  29. ɹ ɹ 作る @ArrayBuilder func someFunction() -> [Int] { 1

    2 3 } someFunction() // [1, 2, 3] @resultBuilder struct ArrayBuilder { static func buildBlock(_ components: Int...) -> [Int] { components } }
  30. ɹ ɹ @resultBuilder struct ArrayBuilder { static func buildBlock(_ components:

    Int...) -> [Int] { components } } 作る resultBuilderアノテーションの付与
  31. ɹ ɹ @resultBuilder struct ArrayBuilder { static func buildBlock(_ components:

    Int...) -> [Int] { components } } 作る buildBlock関数を定義
  32. ɹ ɹ @resultBuilder struct ArrayBuilder { static func buildBlock(_ components:

    Int...) -> [Int] { components } } 作る 引数として受け取れる型 & 返却値の型を指定
  33. ɹ ɹ @resultBuilder struct ArrayBuilder { static func buildBlock(_ components:

    Int...) -> [Int] { components } } 作る インプット値をアウトプット値に変換
  34. ɹ ɹ @ArrayBuilder func someFunction() -> [Int] { 1 2

    3 } someFunction() // [1, 2, 3] @resultBuilder struct ArrayBuilder { static func buildBlock(_ components: Int...) -> [Int] { components } } 使う 作成したクラス名をアノテーションでfunc, var, etcに付与する
  35. ɹ ɹ @ArrayBuilder func someFunction() -> [Int] { 1 2

    3 } someFunction() // [1, 2, 3] @resultBuilder struct ArrayBuilder { static func buildBlock(_ components: Int...) -> [Int] { components } } 使う buildBlockの引数の型に対応した値を列挙
  36. ɹ ɹ @ArrayBuilder func someFunction() -> [Int] { 1 2

    3 } someFunction() // [1, 2, 3] @resultBuilder struct ArrayBuilder { static func buildBlock(_ components: Int...) -> [Int] { components } } 使う 指定された返却値の型の値が返される
  37. ɹ ɹ @ArrayBuilder func someFunction() -> [Int] { 1 2

    3 } someFunction() // [2, 4, 6] @resultBuilder struct ArrayBuilder { static func buildBlock(_ components: Int...) -> [Int] { components.map { $0 * 2 } } } 使う 任意の処理を挟むことができる
  38. ɹ ɹ 最低限必要な実装はこれだけ、ただし…

  39. ɹ ɹ 制御構文への対応 @ArrayBuilder func function1() -> [Int] { //

    Closure containing control flow statement cannot be used with result builder 'ArrayBuilder' if true { 1 } } @ArrayBuilder func function2() -> [Int] { // Closure containing control flow statement cannot be used with result builder 'ArrayBuilder' for i in (2..<10) { i } } If,forͳͲͷར༻ʹ͸ɺରԠ͢ΔbuildXXXؔ਺Λ࣮૷͢Δඞཁ͕͋Δ
  40. ɹ ɹ buildXXX関数 ؔ਺ ݺͼग़͞ΕΔՕॴ buildBlock ϒϩοΫ͝ͱ buildExpression ࣜ͝ͱ buildOptional

    elseͷͳ͍෼ذ buildEither elseͷ͋Δ෼ذ switchʹΑΔ෼ذ buildArray ܁Γฦ͠ buildFinalResult ݁Ռͷ஋ͷ׬੒ buildLimitedAvailability #availableʹΑΔ෼ذ buildPartialResult ϒϩοΫͷߦ͝ͱ ※ ΦʔόʔϩʔυՄೳ
  41. ɹ ɹ buildBlock @ArrayBuilder func someFunction() -> [Int] { 1

    2 3 }
  42. ɹ ɹ buildBlock func someFunction() -> [Int] { let v0

    = 1 let v1 = 2 let v2 = 3 return ArrayBuilder.buildBlock(v0, v1, v2) }
  43. ɹ ɹ buildBlock func someFunction() -> [Int] { let v0

    = 1 let v1 = 2 let v2 = 3 return ArrayBuilder.buildBlock(v0, v1, v2) } @resultBuilder struct ArrayBuilder { static func buildBlock(_ components: Int...) -> [Int] { components } }
  44. ɹ ɹ 分岐への対応 @ArrayBuilder func someFunction() -> [Int] { 1

    if Bool.random() { 2 3 } }
  45. ɹ ɹ 分岐への対応 func someFunction() -> [Int] { let v0:

    Int = 1 let v1 if Bool.random() { 2 3 } return ArrayBuilder.buildBlock(v0, v1) }
  46. ɹ ɹ 分岐への対応 func someFunction() -> [Int] { let v0:

    Int = 1 let v1 if Bool.random() { let v1_0 = 2 let v1_1 = 3 let v1_block: [Int] = ArrayBuilder.buildBlock(v1_0, v1_1) } return ArrayBuilder.buildBlock(v0, v1) }
  47. ɹ ɹ 分岐への対応 func someFunction() -> [Int] { let v0:

    Int = 1 let v1 if Bool.random() { let v1_0 = 2 let v1_1 = 3 let v1_block: [Int] = ArrayBuilder.buildBlock(v1_0, v1_1) } return ArrayBuilder.buildBlock(v0, v1) } ؔ਺ ݺͼग़͞ΕΔՕॴ buildOptional elseͷͳ͍෼ذ
  48. ɹ ɹ 分岐への対応 func someFunction() -> [Int] { let v0:

    Int = 1 let v1 if Bool.random() { let v1_0 = 2 let v1_1 = 3 let v1_block: [Int] = ArrayBuilder.buildBlock(v1_0, v1_1) v1 = ArrayBuilder.buildOptional(v1_block) } else { v1 = ArrayBuilder.buildOptional(nil) } return ArrayBuilder.buildBlock(v0, v1) }
  49. ɹ ɹ 分岐への対応 func someFunction() -> [Int] { let v0:

    Int = 1 let v1: [Int] if Bool.random() { let v1_0 = 2 let v1_1 = 3 let v1_block: [Int] = ArrayBuilder.buildBlock(v1_0, v1_1) v1 = ArrayBuilder.buildOptional(v1_block) } else { v1 = ArrayBuilder.buildOptional(nil) } return ArrayBuilder.buildBlock(v0, v1) } @resultBuilder struct ArrayBuilder { static func buildBlock(_ components: Int...) -> [Int] { components } static func buildOptional(_ component: [Int]?) -> [Int] { component ?? [] } }
  50. ɹ ɹ 分岐への対応 func someFunction() -> [Int] { let v0:

    Int = 1 let v1: [Int] if Bool.random() { let v1_0 = 2 let v1_1 = 3 let v1_block: [Int] = ArrayBuilder.buildBlock(v1_0, v1_1) v1 = ArrayBuilder.buildOptional(v1_block) } else { v1 = ArrayBuilder.buildOptional(nil) } return ArrayBuilder.buildBlock(v0, v1) } @resultBuilder struct ArrayBuilder { static func buildBlock(_ components: Int...) -> [Int] { components } static func buildOptional(_ component: [Int]?) -> [Int] { component ?? [] } } Error!
  51. ɹ ɹ 分岐への対応 func someFunction() -> [Int] { let v0:

    Int = 1 let v1: [Int] if Bool.random() { let v1_0 = 2 let v1_1 = 3 let v1_block: [Int] = ArrayBuilder.buildBlock(v1_0, v1_1) v1 = ArrayBuilder.buildOptional(v1_block) } else { v1 = ArrayBuilder.buildOptional(nil) } return ArrayBuilder.buildBlock(v0, v1) } @resultBuilder struct ArrayBuilder { static func buildBlock(_ components: Int...) -> [Int] { components } static func buildOptional(_ component: [Int]?) -> [Int] { component ?? [] } } Int [Int]
  52. ɹ ɹ 分岐への対応 func someFunction() -> [Int] { let v0:

    Int = 1 let v1: [Int] if Bool.random() { let v1_0 = 2 let v1_1 = 3 let v1_block: [Int] = ArrayBuilder.buildBlock(v1_0, v1_1) v1 = ArrayBuilder.buildOptional(v1_block) } else { v1 = ArrayBuilder.buildOptional(nil) } return ArrayBuilder.buildBlock(v0, v1) } @resultBuilder struct ArrayBuilder { static func buildBlock(_ components: Int...) -> [Int] { components } static func buildOptional(_ component: [Int]?) -> [Int] { component ?? [] } } ᶃ v0Λ[Int]ʹม׵ͯ͠ܕ͕ἧ͏Α͏ʹ͢Δ ᶄ buildBlockͰ[Int]ͷՄม௕Ҿ਺Λ౉ͤΔΑ͏ʹ͢Δ
  53. ɹ ɹ ① v0を[Int]に変換して型が揃うようにする func someFunction() -> [Int] { let

    v0: Int = 1 let v1: [Int] if Bool.random() { let v1_0 = 2 let v1_1 = 3 let v1_block: [Int] = ArrayBuilder.buildBlock(v1_0, v1_1) v1 = ArrayBuilder.buildOptional(v1_block) } else { v1 = ArrayBuilder.buildOptional(nil) } return ArrayBuilder.buildBlock(v0, v1) } @resultBuilder struct ArrayBuilder { static func buildBlock(_ components: Int...) -> [Int] { components } static func buildOptional(_ component: [Int]?) -> [Int] { component ?? [] } } ᶃ v0Λ[Int]ʹม׵ͯ͠ܕ͕ἧ͏Α͏ʹ͢Δ
  54. ɹ ɹ ① v0を[Int]に変換して型が揃うようにする func someFunction() -> [Int] { let

    v0: [Int] = ArrayBuilder.buildExpression(expression: 1) let v1: [Int] if Bool.random() { let v1_0: [Int] = ArrayBuilder.buildExpression(expression: 2) let v1_1: [Int] = ArrayBuilder.buildExpression(expression: 3) let v1_block: [Int] = ArrayBuilder.buildBlock(v1_0, v1_1) v1 = ArrayBuilder.buildOptional(v1_block) } else { v1 = ArrayBuilder.buildOptional(nil) } return ArrayBuilder.buildBlock(v0, v1) } @resultBuilder struct ArrayBuilder { static func buildExpression(_ expression: Int) -> [Int] { [expression] } static func buildBlock(_ components: Int...) -> [Int] { components } static func buildOptional(_ component: [Int]?) -> [Int] { ؔ਺ ݺͼग़͞ΕΔՕॴ buildExpression ࣜ͝ͱ ᶃ v0Λ[Int]ʹม׵ͯ͠ܕ͕ἧ͏Α͏ʹ͢Δ
  55. ɹ ɹ ② buildBlockで[Int]の可変長引数を渡せるようにする func someFunction() -> [Int] { let

    v0: [Int] = ArrayBuilder.buildExpression(expression: 1) let v1: [Int] if Bool.random() { let v1_0: [Int] = ArrayBuilder.buildExpression(expression: 2) let v1_1: [Int] = ArrayBuilder.buildExpression(expression: 3) let v1_block: [Int] = ArrayBuilder.buildBlock(v1_0, v1_1) v1 = ArrayBuilder.buildOptional(v1_block) } else { v1 = ArrayBuilder.buildOptional(nil) } return ArrayBuilder.buildBlock(v0, v1) } @resultBuilder struct ArrayBuilder { static func buildExpression(_ expression: Int) -> [Int] { [expression] } static func buildBlock(_ components: Int...) -> [Int] { components } static func buildOptional(_ component: [Int]?) -> [Int] { ᶄ buildBlockͰ[Int]ͷՄม௕Ҿ਺Λ౉ͤΔΑ͏ʹ͢Δ
  56. ɹ ɹ ② buildBlockで[Int]の可変長引数を渡せるようにする func someFunction() -> [Int] { let

    v0: [Int] = ArrayBuilder.buildExpression(expression: 1) let v1: [Int] if Bool.random() { let v1_0: [Int] = ArrayBuilder.buildExpression(expression: 2) let v1_1: [Int] = ArrayBuilder.buildExpression(expression: 3) let v1_block: [Int] = ArrayBuilder.buildBlock(v1_0, v1_1) v1 = ArrayBuilder.buildOptional(v1_block) } else { v1 = ArrayBuilder.buildOptional(nil) } return ArrayBuilder.buildBlock(v0, v1) } @resultBuilder struct ArrayBuilder { static func buildExpression(_ expression: Int) -> [Int] { [expression] } static func buildBlock(_ components: [Int]...) -> [Int] { components.flatMap { $0 } } static func buildOptional(_ component: [Int]?) -> [Int] { ᶄ buildBlockͰ[Int]ͷՄม௕Ҿ਺Λ౉ͤΔΑ͏ʹ͢Δ
  57. ɹ ɹ 最終的な実装 func someFunction() -> [Int] { let v0:

    [Int] = ArrayBuilder.buildExpression(expression: 1) let v1: [Int] if Bool.random() { let v1_0: [Int] = ArrayBuilder.buildExpression(expression: 2) let v1_1: [Int] = ArrayBuilder.buildExpression(expression: 3) let v1_block: [Int] = ArrayBuilder.buildBlock(v1_0, v1_1) v1 = ArrayBuilder.buildOptional(v1_block) } else { v1 = ArrayBuilder.buildOptional(nil) } return ArrayBuilder.buildBlock(v0, v1) } @resultBuilder struct ArrayBuilder { static func buildExpression(_ expression: Int) -> [Int] { [expression] } static func buildBlock(_ components: [Int]...) -> [Int] { components.flatMap { $0 } } static func buildOptional(_ component: [Int]?) -> [Int] { component ?? [] } }
  58. ɹ ɹ 最終的な実装 @ArrayBuilder func someFunction() -> [Int] { 1

    if Bool.random() { 2 3 } } @resultBuilder struct ArrayBuilder { static func buildExpression(_ expression: Int) -> [Int] { [expression] } static func buildBlock(_ components: [Int]...) -> [Int] { components.flatMap { $0 } } static func buildOptional(_ component: [Int]?) -> [Int] { component ?? [] } }
  59. ɹ ɹ その他制御構文への対応 for i in (1...3) { i }

    var v0: [Int] = [] for i in (1...3) { // ... v0.append(i_block) } let v0_array = ArrayBuilder.buildArray(v0) if Bool.random() { 1 } else { 2 } if Bool.random() { // ... v0 = ArrayBuilder.buildEither(first: v1_block) } else { // ... v0 = ArrayBuilder.buildEither(second: v2_block) } WWDC2021: Write a DSL in Swift using result builders
  60. ɹ ɹ 導入事例の紹介

  61. ɹ ɹ STORES レジ • 2021.06リリースしたiPadアプリ • 店舗運営を支援するPOSシステムを提供 • フルSwiftUI

    SwiftUI+GraphQLͰϓϩμΫτͷܧଓతͳഁյʹཱͪ޲͔͏ʢ iOSDC2021ʣ
  62. ɹ ɹ レシート印刷

  63. ɹ ɹ レシート印刷の流れ ϨΠΞ΢τ༻ಠࣗXML ը૾σʔλ ϓϦϯλʔ΁ૹ৴ ม׵

  64. ɹ ɹ ϨΠΞ΢τ༻ಠࣗXML ը૾σʔλ ϓϦϯλʔ΁ૹ৴ ม׵ 独自ルールに基づいたXML文字列の生成 <receipt> <image src="http://www.example.com/images/logo.png"

    height="96" /> <title>͝ར༻໌ࡉ</title> <ruler alpha="0" /> <spacer /> <text align="center">ϔομʔϝοηʔδ</text> <spacer /> <text truncated="true">ΞΠςϜ໊1</text> <text label="1000ԁ x 2">2000ԁ</text> <text label="ΞΠςϜ໊2" truncated="true">2000ԁ</text> <!-- লུ --> </receipt> ※ 社内の別アプリでの仕組みを、STORES レジ用に移植
  65. ɹ ɹ 独自ルールに基づいたXML文字列の生成 <receipt> <image src="http://www.example.com/images/logo.png" height="96" /> <title>͝ར༻໌ࡉ</title> <ruler

    alpha="0" /> <spacer /> <text align="center">ϔομʔϝοηʔδ</text> <spacer /> <text truncated="true">ΞΠςϜ໊1</text> <text label="1000ԁ x 2">2000ԁ</text> <text label="ΞΠςϜ໊2" truncated="true">2000ԁ</text> <!-- লུ --> </receipt> var xml = "<receipt>" if let src = receiptSetting.logoImageURL?.absoluteString { xml += "<image src=\"\(src)\" height=\“96\”/>” } xml += """ <title>͝ར༻໌ࡉ</title> <ruler alpha="0" /> """ for item in items { if item.quantity >= 2 { let label = "(item.price) x \(item.quantity)" xml += """ <text truncated=\"true\"">\(item.name)</text> <text label=\"\(label)\">\(item.totalPrice)</text> “"" } else { xml += "<text label=\”\(item.name)\" truncated=\”true\””>" xml += "\(item.totalPrice)" xml += "</text>" } } // ~ লུ ~ xml += "</receipt>" return xml
  66. ɹ ɹ XMLベタ書きの問題 1.型安全でなく、想定外の文字があると壊れる 2.冗長な記述が多く、レイアウト構造と非対応で見通しが悪い 3.変更があった場合、改修コストが高い 4.指定可能な値の把握がしづらい var xml =

    "<receipt>" if let src = receiptSetting.logoImageURL?.absoluteString { xml += "<image src=\"\(src)\" height=\“96\”/>” } xml += """ <title>͝ར༻໌ࡉ</title> <ruler alpha="0" /> """
  67. ɹ ɹ var xml = "<receipt>" if let src =

    receiptSetting.logoImageURL?.absoluteString { xml += "<image src=\"\(src)\" height=\“96\”/>” } xml += """ <title>͝ར༻໌ࡉ</title> <ruler alpha="0" /> """ XMLベタ書きの問題 ཁ͸ɺݸʑͷXMLཁૉΛऩूͯ͠ɺจࣈྻΛੜ੒͍ͯ͠Δ͚ͩ → resultBuilderͰͷཁૉͷऩूʹద͍ͯ͠Δ
  68. ɹ ɹ 実装の概要 protocol ReceiptElement { var body: String {

    get } } 各要素を同一視するためのProtocolを定義
  69. ɹ ɹ 実装の概要 protocol ReceiptElement { var body: String {

    get } } struct Text: ReceiptElement { var body: String { “<text>some text</text>" } } struct Image: ReceiptElement { var body: String { “<image src=\”some url\”/>” } } 各要素を同一視するためのProtocolを定義 利用できるXML要素を準拠させる ※ આ໌ͷͨΊ؆ུԽͨ͠ίʔυΛࡌ͍ͤͯ·͢
  70. ɹ ɹ 実装の概要 @resultBuilder struct ReceiptBuilder { static func buildBlock(_

    components: ReceiptElement...) -> [ReceiptElement] { components } static func buildBlock(_ components: [ReceiptElement]...) -> [ReceiptElement] { components.flatMap { $0 } } static func buildExpression(_ expression: ReceiptElement) -> [ReceiptElement] { [expression] } static func buildExpression(_ expression: [ReceiptElement]) -> [ReceiptElement] { expression } static func buildEither(first component: [ReceiptElement]) -> [ReceiptElement] { component } static func buildEither(second component: [ReceiptElement]) -> [ReceiptElement] { component } static func buildOptional(_ component: [ReceiptElement]?) -> [ReceiptElement] { component ?? [] } static func buildArray(_ components: [[ReceiptElement]]) -> [ReceiptElement] { components.flatMap { $0 } } } ReceiptElementをresultBuilderで収集
  71. ɹ ɹ 実装の概要 struct Receipt { let buildElements: () ->

    [ReceiptElement] init(@ReceiptBuilder buildElements: @escaping () -> [ReceiptElement]) { self.buildElements = buildElements } var xml: String { let elements = buildElements() return Tag(.receipt).build(VStack(elements).body) } func image(width: CGFloat) async throws -> UIImage { let builder = ReceiptImageBuilder(width: width) return try await builder.build(xml: xml) } }
  72. ɹ ɹ 実装の概要 struct Receipt { let buildElements: () ->

    [ReceiptElement] init(@ReceiptBuilder buildElements: @escaping () -> [ReceiptElement]) { self.buildElements = buildElements } var xml: String { let elements = buildElements() return Tag(.receipt).build(VStack(elements).body) } func image(width: CGFloat) async throws -> UIImage { let builder = ReceiptImageBuilder(width: width) return try await builder.build(xml: xml) } } ① 1.resultBuilderをイニシャライザで受け取る
  73. ɹ ɹ 実装の概要 struct Receipt { let buildElements: () ->

    [ReceiptElement] init(@ReceiptBuilder buildElements: @escaping () -> [ReceiptElement]) { self.buildElements = buildElements } var xml: String { let elements = buildElements() return Tag(.receipt).build(VStack(elements).body) } func image(width: CGFloat) async throws -> UIImage { let builder = ReceiptImageBuilder(width: width) return try await builder.build(xml: xml) } } ① ② 1.resultBuilderをイニシャライザで受け取る 2.受け取ったresultBuilderでReceiptElementの配列を収集
  74. ɹ ɹ 実装の概要 struct Receipt { let buildElements: () ->

    [ReceiptElement] init(@ReceiptBuilder buildElements: @escaping () -> [ReceiptElement]) { self.buildElements = buildElements } var xml: String { let elements = buildElements() return Tag(.receipt).build(VStack(elements).body) } func image(width: CGFloat) async throws -> UIImage { let builder = ReceiptImageBuilder(width: width) return try await builder.build(xml: xml) } } ① ② ③ 1.resultBuilderをイニシャライザで受け取る 2.受け取ったresultBuilderでReceiptElementの配列を収集 3.収集した要素をXML文字列に変換
  75. ɹ ɹ 実装の概要 1.resultBuilderをイニシャライザで受け取る 2.受け取ったresultBuilderでReceiptElementの配列を収集 3.収集した要素をXML文字列に変換 4.XML文字列を画像に変換 struct Receipt {

    let buildElements: () -> [ReceiptElement] init(@ReceiptBuilder buildElements: @escaping () -> [ReceiptElement]) { self.buildElements = buildElements } var xml: String { let elements = buildElements() return Tag(.receipt).build(VStack(elements).body) } func image(width: CGFloat) async throws -> UIImage { let builder = ReceiptImageBuilder(width: width) return try await builder.build(xml: xml) } } ① ② ③ ④
  76. ɹ ɹ Before: XMLベタ書き var xml = "<receipt>" if let

    src = receiptSetting.logoImageURL?.absoluteString { xml += "<image src=\"\(src)\" height=\“96\”/>” } xml += """ <title>͝ར༻໌ࡉ</title> <ruler alpha="0" /> """ for item in items { if item.quantity >= 2 { let label = "(item.price) x \(item.quantity)" xml += """ <text truncated=\"true\"">\(item.name)</text> <text label=\"\(label)\">\(item.totalPrice)</text> “"" } else { xml += "<text label=\”\(item.name)\" truncated=\”true\””>" xml += "\(item.totalPrice)" xml += "</text>" } } // ~ লུ ~ xml += "</receipt>" return xml
  77. ɹ ɹ Before: XMLベタ書き Receipt { if let src =

    receiptSetting.logoImageURL { Image(src: src) .maxHeight(96) } Title(“͝ར༻໌ࡉ") Divider() VStack(spacing: 2) { for item in items { if item.quantity >= 2 { Text(item.name) .truncated() LabelText(label: "\(item.price) x \(item.quantity)", content: item.totalPrice) } else { LabelText(label: item.name, content: item.totalPrice) .truncated() } } } }
  78. ɹ ɹ Before: XMLベタ書き let receiptImage = try await Receipt

    { if let src = receiptSetting.logoImageURL { Image(src: src) .maxHeight(96) } Title(“͝ར༻໌ࡉ") Divider() VStack(spacing: 2) { for item in items { if item.quantity >= 2 { Text(item.name) .truncated() LabelText(label: "\(item.price) x \(item.quantity)", content: item.totalPrice) } else { LabelText(label: item.name, content: item.totalPrice) .truncated() } } } }.image(width: 240)
  79. ɹ ɹ let receiptImage = try await Receipt { if

    let src = receiptSetting.logoImageURL { Image(src: src) .maxHeight(96) } Title(“͝ར༻໌ࡉ") Divider() VStack(spacing: 2) { for item in items { if item.quantity >= 2 { Text(item.name) .truncated() LabelText(label: "\(item.price) x \(item.quantity)", content: item.totalPrice) } else { LabelText(label: item.name, content: item.totalPrice) .truncated() } } } }.image(width: 240) var xml = "<receipt>" if let src = receiptSetting.logoImageURL?.absoluteString { xml += "<image src=\"\(src)\" height=\“96\”/>” } xml += """ <title>͝ར༻໌ࡉ</title> <ruler alpha="0" /> """ for item in items { if item.quantity >= 2 { let label = "(item.price) x \(item.quantity)" xml += """ <text truncated=\"true\"">\(item.name)</text> <text label=\"\(label)\">\(item.totalPrice)</text> “"" } else { xml += "<text label=\”\(item.name)\" truncated=\”true\””>" xml += "\(item.totalPrice)" xml += "</text>" } } // ~ লུ ~ xml += "</receipt>" return xml After: resultBuilder利用
  80. ɹ ɹ resultBuilderの活用による効果 1. 型安全でなく、想定外の文字があると壊れる 2. シンプルな表現で、レイアウト構造と対応して見通しが良い 3. 変更する際に、コンパイラの助けを借りられる 4.

    指定可能な値をコード補完で把握できる XMLベタ書き resultBuilderを活用 苦しい実装 楽しい実装
  81. ɹ ɹ まとめ

  82. ɹ ɹ • ボイラープレートを削減し、冗長なコードを圧倒的にシンプル化できる • コードの見通しが良くなり、結果が予想しやすくなる • 宣言的な記述が主になるため、変更が容易になる resultBuilderの利点

  83. ɹ ɹ • DSLは利用者に学習を求めることになる • 無闇な導入は過度な抽象化をもたらし、逆にコストが高くなる恐れもある • 機能の利用自体が目的とならないよう、使い所の見極めは必要 注意点

  84. ɹ ɹ • 特定の結果を生成するために、複数要素を組み合わせる場合 • ユースケースに応じて、様々な組み合わせが生まれる場合 • 要素の追加や削除などが行われやすい場合 resultBuilderの効果的な使い所

  85. ɹ ɹ ありがとうございました! ࠾༻΋΍ͬͯΔΑʂ