Slide 1

Slide 1 text

ɹɹ ©︎ hey, Inc 20分でわかる!速習resultBuilder ヘイ株式会社 テクノロジー部門 モバイルアプリケーション本部 STORES レジ iOSエンジニア たまねぎ(Takuya Yokoyama) iOSDC2022

Slide 2

Slide 2 text

ɹ ɹ 自己紹介 Profile { Name("ͨ·Ͷ͗") Twitter("@_chocoyama") Work { Company("hey inc") Product("STORES Regi") Role("Mobile Engineer") } Skill { SwiftUI() Flutter() Compose() } }

Slide 3

Slide 3 text

ɹ ɹ

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

ɹ ɹ resultBuilderとは

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

ɹ ɹ resultBuilderとは何か? ● Swift5.4で追加されたAttribute ● 列挙された要素を収集し、結果の値に変換して取得できる ● Swiftで内部DSLを作るための補助機能 ● 特定領域で効果を発揮するミニチュア言語を作れる

Slide 10

Slide 10 text

ɹ ɹ どこで使われてると思いますか? (ここから5秒間、心で念じる or コメント or Tweetする時間)

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

ɹ ɹ どこで使われている? 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

Slide 13

Slide 13 text

ɹ ɹ 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

Slide 14

Slide 14 text

ɹ ɹ let word = OneOrMore(.word)

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

ɹ ɹ 何ができる? 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 }

Slide 17

Slide 17 text

ɹ ɹ なぜ必要? 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) } } } } }

Slide 18

Slide 18 text

ɹ ɹ 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) } } ※ ͜ͷίʔυ͸͋͘·ͰΠϝʔδͰ͢

Slide 19

Slide 19 text

ɹ ɹ もし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) } } ※ ͜ͷίʔυ͸͋͘·ͰΠϝʔδͰ͢

Slide 20

Slide 20 text

ɹ ɹ resultBuilder 何ができる?     シンプルな記述で、複数要素の収集と結果値への変換ができる どこで使われている? SwiftUIやRegexなどで用いられている なぜ必要?      可読性・保守性の向上が期待できるため

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

ɹ ɹ 例えばこういう時 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 ] ))

Slide 24

Slide 24 text

ɹ ɹ • 要素指定と関係ない記述が多い • 構造がわかりづらい • 変更による修正量が多い 例えばこういう時 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 ] ))

Slide 25

Slide 25 text

ɹ ɹ 例えばこういう時 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

Slide 26

Slide 26 text

ɹ ɹ 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") }

Slide 27

Slide 27 text

ɹ ɹ 仕組みと実装方法

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

ɹ ɹ @resultBuilder struct ArrayBuilder { static func buildBlock(_ components: Int...) -> [Int] { components } } 作る インプット値をアウトプット値に変換

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

ɹ ɹ @ArrayBuilder func someFunction() -> [Int] { 1 2 3 } someFunction() // [2, 4, 6] @resultBuilder struct ArrayBuilder { static func buildBlock(_ components: Int...) -> [Int] { components.map { $0 * 2 } } } 使う 任意の処理を挟むことができる

Slide 38

Slide 38 text

ɹ ɹ 最低限必要な実装はこれだけ、ただし…

Slide 39

Slide 39 text

ɹ ɹ 制御構文への対応 @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ؔ਺Λ࣮૷͢Δඞཁ͕͋Δ

Slide 40

Slide 40 text

ɹ ɹ buildXXX関数 ؔ਺ ݺͼग़͞ΕΔՕॴ buildBlock ϒϩοΫ͝ͱ buildExpression ࣜ͝ͱ buildOptional elseͷͳ͍෼ذ buildEither elseͷ͋Δ෼ذ switchʹΑΔ෼ذ buildArray ܁Γฦ͠ buildFinalResult ݁Ռͷ஋ͷ׬੒ buildLimitedAvailability #availableʹΑΔ෼ذ buildPartialResult ϒϩοΫͷߦ͝ͱ ※ ΦʔόʔϩʔυՄೳ

Slide 41

Slide 41 text

ɹ ɹ buildBlock @ArrayBuilder func someFunction() -> [Int] { 1 2 3 }

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

ɹ ɹ 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 } }

Slide 44

Slide 44 text

ɹ ɹ 分岐への対応 @ArrayBuilder func someFunction() -> [Int] { 1 if Bool.random() { 2 3 } }

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

ɹ ɹ 分岐への対応 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) }

Slide 47

Slide 47 text

ɹ ɹ 分岐への対応 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ͷͳ͍෼ذ

Slide 48

Slide 48 text

ɹ ɹ 分岐への対応 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) }

Slide 49

Slide 49 text

ɹ ɹ 分岐への対応 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 ?? [] } }

Slide 50

Slide 50 text

ɹ ɹ 分岐への対応 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!

Slide 51

Slide 51 text

ɹ ɹ 分岐への対応 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]

Slide 52

Slide 52 text

ɹ ɹ 分岐への対応 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]ͷՄม௕Ҿ਺Λ౉ͤΔΑ͏ʹ͢Δ

Slide 53

Slide 53 text

ɹ ɹ ① 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]ʹม׵ͯ͠ܕ͕ἧ͏Α͏ʹ͢Δ

Slide 54

Slide 54 text

ɹ ɹ ① 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]ʹม׵ͯ͠ܕ͕ἧ͏Α͏ʹ͢Δ

Slide 55

Slide 55 text

ɹ ɹ ② 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]ͷՄม௕Ҿ਺Λ౉ͤΔΑ͏ʹ͢Δ

Slide 56

Slide 56 text

ɹ ɹ ② 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]ͷՄม௕Ҿ਺Λ౉ͤΔΑ͏ʹ͢Δ

Slide 57

Slide 57 text

ɹ ɹ 最終的な実装 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 ?? [] } }

Slide 58

Slide 58 text

ɹ ɹ 最終的な実装 @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 ?? [] } }

Slide 59

Slide 59 text

ɹ ɹ その他制御構文への対応 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

Slide 60

Slide 60 text

ɹ ɹ 導入事例の紹介

Slide 61

Slide 61 text

ɹ ɹ STORES レジ ● 2021.06リリースしたiPadアプリ ● 店舗運営を支援するPOSシステムを提供 ● フルSwiftUI SwiftUI+GraphQLͰϓϩμΫτͷܧଓతͳഁյʹཱͪ޲͔͏ʢ iOSDC2021ʣ

Slide 62

Slide 62 text

ɹ ɹ レシート印刷

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

ɹ ɹ ϨΠΞ΢τ༻ಠࣗXML ը૾σʔλ ϓϦϯλʔ΁ૹ৴ ม׵ 独自ルールに基づいたXML文字列の生成 ͝ར༻໌ࡉ ϔομʔϝοηʔδ ΞΠςϜ໊1 2000ԁ 2000ԁ ※ 社内の別アプリでの仕組みを、STORES レジ用に移植

Slide 65

Slide 65 text

ɹ ɹ 独自ルールに基づいたXML文字列の生成 ͝ར༻໌ࡉ ϔομʔϝοηʔδ ΞΠςϜ໊1 2000ԁ 2000ԁ var xml = "" if let src = receiptSetting.logoImageURL?.absoluteString { xml += "” } xml += """ ͝ར༻໌ࡉ """ for item in items { if item.quantity >= 2 { let label = "(item.price) x \(item.quantity)" xml += """ \(item.name) \(item.totalPrice) “"" } else { xml += "" xml += "\(item.totalPrice)" xml += "" } } // ~ লུ ~ xml += "" return xml

Slide 66

Slide 66 text

ɹ ɹ XMLベタ書きの問題 1.型安全でなく、想定外の文字があると壊れる 2.冗長な記述が多く、レイアウト構造と非対応で見通しが悪い 3.変更があった場合、改修コストが高い 4.指定可能な値の把握がしづらい var xml = "" if let src = receiptSetting.logoImageURL?.absoluteString { xml += "” } xml += """ ͝ར༻໌ࡉ """

Slide 67

Slide 67 text

ɹ ɹ var xml = "" if let src = receiptSetting.logoImageURL?.absoluteString { xml += "” } xml += """ ͝ར༻໌ࡉ """ XMLベタ書きの問題 ཁ͸ɺݸʑͷXMLཁૉΛऩूͯ͠ɺจࣈྻΛੜ੒͍ͯ͠Δ͚ͩ → resultBuilderͰͷཁૉͷऩूʹద͍ͯ͠Δ

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

ɹ ɹ 実装の概要 protocol ReceiptElement { var body: String { get } } struct Text: ReceiptElement { var body: String { “some text" } } struct Image: ReceiptElement { var body: String { “” } } 各要素を同一視するためのProtocolを定義 利用できるXML要素を準拠させる ※ આ໌ͷͨΊ؆ུԽͨ͠ίʔυΛࡌ͍ͤͯ·͢

Slide 70

Slide 70 text

ɹ ɹ 実装の概要 @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で収集

Slide 71

Slide 71 text

ɹ ɹ 実装の概要 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) } }

Slide 72

Slide 72 text

ɹ ɹ 実装の概要 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をイニシャライザで受け取る

Slide 73

Slide 73 text

ɹ ɹ 実装の概要 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の配列を収集

Slide 74

Slide 74 text

ɹ ɹ 実装の概要 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文字列に変換

Slide 75

Slide 75 text

ɹ ɹ 実装の概要 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) } } ① ② ③ ④

Slide 76

Slide 76 text

ɹ ɹ Before: XMLベタ書き var xml = "" if let src = receiptSetting.logoImageURL?.absoluteString { xml += "” } xml += """ ͝ར༻໌ࡉ """ for item in items { if item.quantity >= 2 { let label = "(item.price) x \(item.quantity)" xml += """ \(item.name) \(item.totalPrice) “"" } else { xml += "" xml += "\(item.totalPrice)" xml += "" } } // ~ লུ ~ xml += "" return xml

Slide 77

Slide 77 text

ɹ ɹ 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() } } } }

Slide 78

Slide 78 text

ɹ ɹ 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)

Slide 79

Slide 79 text

ɹ ɹ 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 = "" if let src = receiptSetting.logoImageURL?.absoluteString { xml += "” } xml += """ ͝ར༻໌ࡉ """ for item in items { if item.quantity >= 2 { let label = "(item.price) x \(item.quantity)" xml += """ \(item.name) \(item.totalPrice) “"" } else { xml += "" xml += "\(item.totalPrice)" xml += "" } } // ~ লུ ~ xml += "" return xml After: resultBuilder利用

Slide 80

Slide 80 text

ɹ ɹ resultBuilderの活用による効果 1. 型安全でなく、想定外の文字があると壊れる 2. シンプルな表現で、レイアウト構造と対応して見通しが良い 3. 変更する際に、コンパイラの助けを借りられる 4. 指定可能な値をコード補完で把握できる XMLベタ書き resultBuilderを活用 苦しい実装 楽しい実装

Slide 81

Slide 81 text

ɹ ɹ まとめ

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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