-
Write a DSL in Swift using result builders
Some problems are easier to solve by creating a customized programming language, or “domain-specific language.” While creating a DSL traditionally requires writing your own compiler, you can instead use result builders with Swift 5.4 to make your code both easier to read and maintain. We'll take you through best practices for designing a custom language for Swift: Learn about result builders and trailing closure arguments, explore modifier-style methods and why they work well, and discover how you can extend Swift's normal language rules to turn Swift into a DSL.
To get the most out of this session, it's helpful (though not necessary) to have some experience writing SwiftUI views. You won't need to know anything about parser or compiler implementation.Ressources
- Result Builders - The Swift Programming Language
- Attributes - The Swift Programming Language
- Fruta: Building a feature-rich app with SwiftUI
Vidéos connexes
WWDC21
WWDC19
-
Rechercher dans cette vidéo…
-
-
3:15 - FavoriteSmoothies view
struct FavoriteSmoothies: View { @EnvironmentObject private var model: FrutaModel var body: some View { SmoothieList(smoothies: model.favoriteSmoothies) .overlay( Group { if model.favoriteSmoothies.isEmpty { Text("Add some smoothies!") .foregroundColor(.secondary) .frame(maxWidth: .infinity, maxHeight: .infinity) } } ) .navigationTitle("Favorites") } } -
3:38 - FavoriteSmoothies view (hypothetical alternative)
// Hypothetical code--not actually supported by SwiftUI struct FavoriteSmoothies: View { @EnvironmentObject private var model: FrutaModel var body: some View { var list = SmoothieList(smoothies: model.favoriteSmoothies) let overlay: View if model.favoriteSmoothies.isEmpty { var text = Text("Add some smoothies!") text.foregroundColor = .secondary var frame = Frame(subview: text) frame.maxWidth = .infinity frame.maxHeight = .infinity overlay = frame } else { overlay = EmptyView() } list.addOverlay(overlay) list.navigationTitle = "Favorites" return list } } -
3:59 - FavoriteSmoothies view
struct FavoriteSmoothies: View { @EnvironmentObject private var model: FrutaModel var body: some View { SmoothieList(smoothies: model.favoriteSmoothies) .overlay( Group { if model.favoriteSmoothies.isEmpty { Text("Add some smoothies!") .foregroundColor(.secondary) .frame(maxWidth: .infinity, maxHeight: .infinity) } } ) .navigationTitle("Favorites") } } -
6:17 - FavoriteSmoothies view
struct FavoriteSmoothies: View { @EnvironmentObject private var model: FrutaModel var body: some View { SmoothieList(smoothies: model.favoriteSmoothies) .overlay( Group { if model.favoriteSmoothies.isEmpty { Text("Add some smoothies!") .foregroundColor(.secondary) .frame(maxWidth: .infinity, maxHeight: .infinity) } } ) .navigationTitle("Favorites") } } -
9:26 - Simple result builder example
VStack { Text("Title").font(.title) Text("Contents") } -
9:36 - Simple result builder example + struct VStack
VStack { Text("Title").font(.title) Text("Contents") } struct VStack<Content: View>: View { … init(@ViewBuilder content: () -> Content) { self.content = content() } } -
9:40 - Simple result builder example + struct VStack + trailing closure applied
VStack /* .init(content: */ { Text("Title").font(.title) Text("Contents") } /* ) */ struct VStack<Content: View>: View { … init(@ViewBuilder content: () -> Content) { self.content = content() } } -
9:50 - Simple result builder example + struct VStack + trailing closure applied + enum ViewBuilder
VStack /* .init(content: */ { Text("Title").font(.title) Text("Contents") /* return // TODO: build results using ‘ViewBuilder’ */ } /* ) */ struct VStack<Content: View>: View { … init(@ViewBuilder content: () -> Content) { self.content = content() } } @resultBuilder enum ViewBuilder { static func buildBlock(_: View...) -> some View { … } } -
VStack /* .init(content: */ { /* let v0 = */ Text("Title").font(.title) /* let v1 = */ Text("Contents") /* return ViewBuilder.buildBlock(v0, v1) */ } /* ) */ struct VStack<Content: View>: View { … init(@ViewBuilder content: () -> Content) { self.content = content() } } @resultBuilder enum ViewBuilder { static func buildBlock(_: View...) -> some View { … } } -
14:49 - Fruta's smoothie lists, pre-DSL
// Fruta’s Smoothie lists extension Smoothie { static let berryBlue = Smoothie( id: "berry-blue", title: "Berry Blue", description: "Filling and refreshing, this smoothie will fill you with joy!", measuredIngredients: [ MeasuredIngredient(.orange, measurement: Measurement(value: 1.5, unit: .cups)), MeasuredIngredient(.blueberry, measurement: Measurement(value: 1, unit: .cups)), MeasuredIngredient(.avocado, measurement: Measurement(value: 0.2, unit: .cups)) ], hasFreeRecipe: true ) static let carrotChops = Smoothie(…) static let crazyColada = Smoothie(…) // Plus 12 more… } extension Smoothie { private static let allSmoothies: [Smoothie] = [ .berryBlue, .carrotChops, .crazyColada, // Plus 12 more… ] static func all(includingPaid: Bool = true) -> [Smoothie] { if includingPaid { return allSmoothies } logger.log("Free smoothies only") return allSmoothies.filter { $0.hasFreeRecipe } } } -
14:50 - Fruta's smoothie lists, pre-DSL (hypothetical alternative)
// Fruta’s Smoothie lists (hypothetical alternative) extension Smoothie { static let berryBlue = Smoothie( id: "berry-blue", title: "Berry Blue", description: "Filling and refreshing, this smoothie will fill you with joy!", measuredIngredients: [ MeasuredIngredient(.orange, measurement: Measurement(value: 1.5, unit: .cups)), MeasuredIngredient(.blueberry, measurement: Measurement(value: 1, unit: .cups)), MeasuredIngredient(.avocado, measurement: Measurement(value: 0.2, unit: .cups)) ], hasFreeRecipe: true ) static let carrotChops = Smoothie(…) static let crazyColada = Smoothie(…) // Plus 12 more… } extension Smoothie { static func all(includingPaid: Bool = true) -> [Smoothie] { var allSmoothies: [Smoothie] = [ .berryBlue, .carrotChops, ] if includingPaid { allSmoothies += [ .crazyColada, // Plus more ] } else { logger.log("Free smoothies only") } return allSmoothies } } -
14:51 - Fruta's smoothie lists, pre-DSL
// Fruta’s Smoothie lists extension Smoothie { static let berryBlue = Smoothie( id: "berry-blue", title: "Berry Blue", description: "Filling and refreshing, this smoothie will fill you with joy!", measuredIngredients: [ MeasuredIngredient(.orange, measurement: Measurement(value: 1.5, unit: .cups)), MeasuredIngredient(.blueberry, measurement: Measurement(value: 1, unit: .cups)), MeasuredIngredient(.avocado, measurement: Measurement(value: 0.2, unit: .cups)) ], hasFreeRecipe: true ) static let carrotChops = Smoothie(…) static let crazyColada = Smoothie(…) // Plus 12 more… } extension Smoothie { private static let allSmoothies: [Smoothie] = [ .berryBlue, .carrotChops, .crazyColada, // Plus 12 more… ] static func all(includingPaid: Bool = true) -> [Smoothie] { if includingPaid { return allSmoothies } logger.log("Free smoothies only") return allSmoothies.filter { $0.hasFreeRecipe } } } -
18:05 - Near-final DSL design
// DSL top-level design @SmoothieArrayBuilder static func all(includingPaid: Bool = true) -> [Smoothie] { Smoothie( // TODO: Change these parameters id: "berry-blue", title: "Berry Blue", description: "Filling and refreshing, this smoothie will fill you with joy!", measuredIngredients: [ Ingredient.orange.measured(with: .cups).scaled(by: 1.5), Ingredient.blueberry.measured(with: .cups), Ingredient.avocado.measured(with: .cups).scaled(by: 0.2) ] ) Smoothie(…) if includingPaid { Smoothie(…) Smoothie(…) } else { logger.log("Free smoothies only") } } -
19:57 - Possible DSL description/ingredient designs (start)
// Possible DSL description/ingredient designs Smoothie( id: "berry-blue", title: "Berry Blue", description: "Filling and refreshing, this smoothie will fill you with joy!", measuredIngredients: [ Ingredient.orange.measured(with: .cups).scaled(by: 1.5), Ingredient.blueberry.measured(with: .cups), Ingredient.avocado.measured(with: .cups).scaled(by: 0.2) ] ) -
20:11 - Possible DSL description/ingredient designs (modifiers)
// Possible DSL description/ingredient designs Smoothie(id: "berry-blue", title: "Berry Blue") .description("Filling and refreshing, this smoothie will fill you with joy!") .ingredient(Ingredient.orange.measured(with: .cups).scaled(by: 1.5)) .ingredient(Ingredient.blueberry.measured(with: .cups)) .ingredient(Ingredient.avocado.measured(with: .cups).scaled(by: 0.2)) -
20:25 - Possible DSL description/ingredient designs (all marker types)
// Possible DSL description/ingredient designs Smoothie { ID("berry-blue") Title("Berry Blue") Description("Filling and refreshing, this smoothie will fill you with joy!") Recipe( Ingredient.orange.measured(with: .cups).scaled(by: 1.5), Ingredient.blueberry.measured(with: .cups), Ingredient.avocado.measured(with: .cups).scaled(by: 0.2) ) } -
20:36 - Possible DSL description/ingredient designs (some marker types)
// Possible DSL description/ingredient designs Smoothie(id: "berry-blue", title: "Berry Blue") { Description("Filling and refreshing, this smoothie will fill you with joy!") Recipe( Ingredient.orange.measured(with: .cups).scaled(by: 1.5), Ingredient.blueberry.measured(with: .cups), Ingredient.avocado.measured(with: .cups).scaled(by: 0.2) ) } -
21:13 - Possible DSL description/ingredient designs (no marker types)
// Possible DSL description/ingredient designs Smoothie(id: "berry-blue", title: "Berry Blue") { "Filling and refreshing, this smoothie will fill you with joy!" Ingredient.orange.measured(with: .cups).scaled(by: 1.5) Ingredient.blueberry.measured(with: .cups) Ingredient.avocado.measured(with: .cups).scaled(by: 0.2) } -
21:43 - Final DSL design
// DSL top-level design @SmoothieArrayBuilder static func all(includingPaid: Bool = true) -> [Smoothie] { Smoothie(id: "berry-blue", title: "Berry Blue") { "Filling and refreshing, this smoothie will fill you with joy!" Ingredient.orange.measured(with: .cups).scaled(by: 1.5) Ingredient.blueberry.measured(with: .cups) Ingredient.avocado.measured(with: .cups).scaled(by: 0.2) } Smoothie(…) { … } if includingPaid { Smoothie(…) { … } } else { logger.log("Free smoothies only") } } -
24:05 - Basic SmoothieArrayBuilder
@resultBuilder enum SmoothieArrayBuilder { static func buildBlock(_ components: Smoothie...) -> [Smoothie] { return components } } -
24:39 - How ‘buildBlock(…)’ works
// How ‘buildBlock(…)’ works @SmoothieArrayBuilder static func all(includingPaid: Bool = true) { /* let v0 = */ Smoothie(id: "berry-blue", title: "Berry Blue") { … } /* let v1 = */ Smoothie(id: "carrot-chops", title: "Carrot Chops") { … } // …more smoothies… /* return SmoothieArrayBuilder.buildBlock(v0, v1, …) */ } -
25:03 - Basic SmoothieArrayBuilder
@resultBuilder enum SmoothieArrayBuilder { static func buildBlock(_ components: Smoothie...) -> [Smoothie] { return components } } -
25:56 - Smoothie initializer (incomplete)
extension Smoothie { init(id: Smoothie.ID, title: String, /* FIXME */ _ makeIngredients: () -> (String, [MeasuredIngredient])) { let (description, ingredients) = makeIngredients() self.init(id: id, title: title, description: description, measuredIngredients: ingredients) } } -
27:47 - SmoothieArrayBuilder with simple ‘if’ statements (incorrect)
@resultBuilder enum SmoothieArrayBuilder { static func buildOptional(_ component: [Smoothie]?) -> [Smoothie] { return component ?? [] } static func buildBlock(_ components: Smoothie...) -> [Smoothie] { return components } } -
28:01 - How ‘if’ statements work with ‘buildOptional(_:)’
// How ‘if’ statements work with ‘buildOptional(_:)’ @SmoothieArrayBuilder static func all(includingPaid: Bool = true) { /* let v0 = */ Smoothie(id: "berry-blue", …) { … } /* let v1 = */ Smoothie(id: "carrot-chops", …) { … } /* let v2: [Smoothie] */ if includingPaid { /* let v2_0 = */ Smoothie(id: "crazy-colada", …) { … } /* let v2_1 = */ Smoothie(id: "hulking-lemonade", …) { … } /* let v2_block = SmoothieArrayBuilder.buildBlock(v2_0, v2_1) v2 = SmoothieArrayBuilder.buildOptional(v2_block) */ } /* else { v2 = SmoothieArrayBuilder.buildOptional(nil) } */ /* return SmoothieArrayBuilder.buildBlock(v0, v1, v2) */ } -
29:07 - SmoothieArrayBuilder with simple ‘if’ statements (incorrect)
@resultBuilder enum SmoothieArrayBuilder { static func buildOptional(_ component: [Smoothie]?) -> [Smoothie] { return component ?? [] } static func buildBlock(_ components: Smoothie...) -> [Smoothie] { return components } } -
29:28 - Why didn’t our ‘buildOptional(_:)’ work?
// Why didn’t our ‘buildOptional(_:)’ work? @SmoothieArrayBuilder static func all(includingPaid: Bool = true) { /* let v0 = */ Smoothie(id: "berry-blue", …) { … } /* let v1 = */ Smoothie(id: "carrot-chops", …) { … } /* let v2: [Smoothie] */ if includingPaid { /* let v2_0 = */ Smoothie(id: "crazy-colada", …) { … } /* let v2_1 = */ Smoothie(id: "hulking-lemonade", …) { … } /* let v2_block = SmoothieArrayBuilder.buildBlock(v2_0, v2_1) v2 = SmoothieArrayBuilder.buildOptional(v2_block) */ } /* else { v2 = SmoothieArrayBuilder.buildOptional(nil) } */ /* return SmoothieArrayBuilder.buildBlock(v0, v1, v2) */ } -
29:40 - SmoothieArrayBuilder with simple ‘if’ statements (still incorrect)
@resultBuilder enum SmoothieArrayBuilder { static func buildOptional(_ component: [Smoothie]?) -> [Smoothie] { return component ?? [] } static func buildBlock(_ components: [Smoothie]...) -> [Smoothie] { return components.flatMap { $0 } } } -
30:14 - Why didn’t our ‘buildOptional(_:)’ work?
// Why didn’t our ‘buildOptional(_:)’ work? @SmoothieArrayBuilder static func all(includingPaid: Bool = true) { /* let v0 = */ Smoothie(id: "berry-blue", …) { … } /* let v1 = */ Smoothie(id: "carrot-chops", …) { … } /* let v2: [Smoothie] */ if includingPaid { /* let v2_0 = */ Smoothie(id: "crazy-colada", …) { … } /* let v2_1 = */ Smoothie(id: "hulking-lemonade", …) { … } /* let v2_block = SmoothieArrayBuilder.buildBlock(v2_0, v2_1) v2 = SmoothieArrayBuilder.buildOptional(v2_block) */ } /* else { v2 = SmoothieArrayBuilder.buildOptional(nil) } */ /* return SmoothieArrayBuilder.buildBlock(v0, v1, v2) */ } -
31:23 - The ‘buildExpression(_:)’ method
// The ‘buildExpression(_:)’ method @SmoothieArrayBuilder static func all(includingPaid: Bool = true) { /* let v0 = SmoothieArrayBuilder.buildExpression( */ Smoothie(id: "berry-blue", …) { … } /* ) */ /* let v1 = SmoothieArrayBuilder.buildExpression( */ Smoothie(id: "carrot-chops", …) { … } /* ) */ /* let v2: [Smoothie] */ if includingPaid { /* let v2_0 = SmoothieArrayBuilder.buildExpression( */ Smoothie(id: "crazy-colada", …) { … } /* ) */ /* let v2_1 = SmoothieArrayBuilder.buildExpression( */ Smoothie(id: "hulking-lemonade", …) { … } /* ) */ /* let v2_block = SmoothieArrayBuilder.buildBlock(v2_0, v2_1) v2 = SmoothieArrayBuilder.buildOptional(v2_block) */ } /* else { v2 = SmoothieArrayBuilder.buildOptional(nil) } */ /* return SmoothieArrayBuilder.buildBlock(v0, v1, v2) */ } -
31:44 - SmoothieArrayBuilder with simple ‘if’ statements (correct)
@resultBuilder enum SmoothieArrayBuilder { static func buildOptional(_ component: [Smoothie]?) -> [Smoothie] { return component ?? [] } static func buildBlock(_ components: [Smoothie]...) -> [Smoothie] { return components.flatMap { $0 } } static func buildExpression(_ expression: Smoothie) -> [Smoothie] { return [expression] } } -
32:48 - SmoothieArrayBuilder with ‘if’-‘else’ statements
@resultBuilder enum SmoothieArrayBuilder { static func buildEither(first component: [Smoothie]) -> [Smoothie] { return component } static func buildEither(second component: [Smoothie]) -> [Smoothie] { return component } static func buildOptional(_ component: [Smoothie]?) -> [Smoothie] { return component ?? [] } static func buildBlock(_ components: [Smoothie]...) -> [Smoothie] { return components.flatMap { $0 } } static func buildExpression(_ expression: Smoothie) -> [Smoothie] { return [expression] } } -
32:53 - How ‘if’-‘else’ statements work with ‘buildEither(…)’
// How ‘if’-‘else’ statements work with ‘buildEither(…)’ @SmoothieArrayBuilder static func all(includingPaid: Bool = true) -> [Smoothie] { /* let v0: [Smoothie] */ if includingPaid { /* let v0_0 = SmoothieArrayBuilder.buildExpression( */ Smoothie(…) { … } /* ) */ /* let v0_block = SmoothieArrayBuilder.buildBlock(v0_0) v0 = SmoothieArrayBuilder.buildEither(first: v0_block) */ } else { /* let v0_0 = SmoothieArrayBuilder.buildExpression( */ logger.log("Only got free smoothies!") /* ) */ /* let v0_block = SmoothieArrayBuilder.buildBlock(v0_0) v0 = SmoothieArrayBuilder.buildEither(second: v0_block) */ } /* return SmoothieArrayBuilder.buildBlock(v0) */ } -
33:37 - How more complicated statements work with ‘buildEither(…)’
// How more complicated statements work with ‘buildEither(…)’ var v0: [Smoothie] switch userRegion { case .americas: // ...smoothies omitted... /* let v0_block = SmoothieArrayBuilder.buildBlock(...parameters omitted...) v0 = SmoothieArrayBuilder.buildEither(first: SmoothieArrayBuilder.buildEither(first: v0_block)) */ case .asiaPacific: // ...smoothies omitted... /* let v0_block = SmoothieArrayBuilder.buildBlock(…) v0 = SmoothieArrayBuilder.buildEither(first: SmoothieArrayBuilder.buildEither(second: v0_block)) */ case .eastAtlantic: // ...smoothies omitted... /* let v0_block = SmoothieArrayBuilder.buildBlock(…) v0 = SmoothieArrayBuilder.buildEither(second: v0_block) */ } -
34:12 - SmoothieArrayBuilder with ‘if’-‘else’ statements
@resultBuilder enum SmoothieArrayBuilder { static func buildEither(first component: [Smoothie]) -> [Smoothie] { return component } static func buildEither(second component: [Smoothie]) -> [Smoothie] { return component } static func buildOptional(_ component: [Smoothie]?) -> [Smoothie] { return component ?? [] } static func buildBlock(_ components: [Smoothie]...) -> [Smoothie] { return components.flatMap { $0 } } static func buildExpression(_ expression: Smoothie) -> [Smoothie] { return [expression] } } -
34:54 - How ‘if’-‘else’ statements work with ‘buildEither(…)’
// How ‘if’-‘else’ statements work with ‘buildEither(…)’ @SmoothieArrayBuilder static func all(includingPaid: Bool = true) -> [Smoothie] { /* let v0: [Smoothie] */ if includingPaid { /* let v0_0 = SmoothieArrayBuilder.buildExpression( */ Smoothie(…) { … } /* ) */ /* let v0_block = SmoothieArrayBuilder.buildBlock(v0_0) v0 = SmoothieArrayBuilder.buildEither(first: v0_block) */ } else { /* let v0_0 = SmoothieArrayBuilder.buildExpression( */ logger.log("Only got free smoothies!") /* ) */ /* let v0_block = SmoothieArrayBuilder.buildBlock(v0_0) v0 = SmoothieArrayBuilder.buildEither(second: v0_block) */ } /* return SmoothieArrayBuilder.buildBlock(v0) */ } -
35:07 - SmoothieArrayBuilder with support for ‘Void’ results
@resultBuilder enum SmoothieArrayBuilder { static func buildEither(first component: [Smoothie]) -> [Smoothie] { return component } static func buildEither(second component: [Smoothie]) -> [Smoothie] { return component } static func buildOptional(_ component: [Smoothie]?) -> [Smoothie] { return component ?? [] } static func buildBlock(_ components: [Smoothie]...) -> [Smoothie] { return components.flatMap { $0 } } static func buildExpression(_ expression: Smoothie) -> [Smoothie] { return [expression] } static func buildExpression(_ expression: Void) -> [Smoothie] { return [] } } -
36:41 - Modifier-style methods on Ingredient and MeasuredIngredient
extension Ingredient { func measured(with unit: UnitVolume) -> MeasuredIngredient { MeasuredIngredient(self, measurement: Measurement(value: 1, unit: unit)) } } extension MeasuredIngredient { func scaled(by scale: Double) -> MeasuredIngredient { return MeasuredIngredient(ingredient, measurement: measurement * scale) } } -
37:32 - Closures and result builders
// Closures and result builders @SmoothieArrayBuilder static func all(includingPaid: Bool = true) -> [Smoothie] { /* let v0 = SmoothieArrayBuilder.buildExpression( */ Smoothie(…) { "Filling and refreshing, this smoothie will fill you with joy!" Ingredient.orange.measured(with: .cups).scaled(by: 1.5) Ingredient.blueberry.measured(with: .cups) Ingredient.avocado.measured(with: .cups).scaled(by: 0.2) } /* ) */ /* let v1 = SmoothieArrayBuilder.buildExpression( */ Smoothie(…) { "Packed with vitamin A and C, Carrot Chops is a great way to start your day!" Ingredient.orange.measured(with: .cups).scaled(by: 1.5) Ingredient.carrot.measured(with: .cups).scaled(by: 0.5) Ingredient.mango.measured(with: .cups).scaled(by: 0.5) } /* ) */ /* return SmoothieArrayBuilder.buildBlock(v0, v1) */ } -
39:22 - Smoothie initializer (final) and SmoothieBuilder (initial)
extension Smoothie { init(id: Smoothie.ID, title: String, @SmoothieBuilder _ makeIngredients: () -> (String, [MeasuredIngredient])) { let (description, ingredients) = makeIngredients() self.init(id: id, title: title, description: description, measuredIngredients: ingredients) } } @resultBuilder enum SmoothieBuilder { static func buildBlock(_ description: String, components: MeasuredIngredient...) -> (String, [MeasuredIngredient]) { return (description, components) } } -
40:38 - Accepting different types
// Accepting different types Smoothie(…) /* @SmoothieBuilder */ { /* let v0 = */ "Filling and refreshing, this smoothie will fill you with joy!" /* let v1 = */ Ingredient.orange.measured(with: .cups).scaled(by: 1.5) /* let v2 = */ Ingredient.blueberry.measured(with: .cups) /* let v3 = */ Ingredient.avocado.measured(with: .cups).scaled(by: 0.2) /* return SmoothieBuilder.buildBlock(v0, v1, v2, v3) */ } -
41:01 - Smoothie initializer (final) and SmoothieBuilder (initial)
extension Smoothie { init(id: Smoothie.ID, title: String, @SmoothieBuilder _ makeIngredients: () -> (String, [MeasuredIngredient])) { let (description, ingredients) = makeIngredients() self.init(id: id, title: title, description: description, measuredIngredients: ingredients) } } @resultBuilder enum SmoothieBuilder { static func buildBlock(_ description: String, components: MeasuredIngredient...) -> (String, [MeasuredIngredient]) { return (description, components) } } -
42:43 - SmoothieBuilder without the string
// SmoothieBuilder without the string Smoothie(…) /* @SmoothieBuilder */ { // "Filling and refreshing, this smoothie will fill you with joy!" /* let v0 = */ Ingredient.orange.measured(with: .cups).scaled(by: 1.5) /* let v1 = */ Ingredient.blueberry.measured(with: .cups) /* let v2 = */ Ingredient.avocado.measured(with: .cups).scaled(by: 0.2) /* return SmoothieBuilder.buildBlock(v0, v1, v2) */ } extension SmoothieBuilder { static func buildBlock(_ description: String, _ ingredients: ManagedIngredients...) -> (String, [ManagedIngredients]) { … } } -
43:38 - How Swift improves diagnostics
// How Swift improves diagnostics func fn0() throws {} func fn1() rethrows {} func fn2() {} func fn3() deinit {} func fn4() try {} -
44:30 - SmoothieBuilder without the string
// SmoothieBuilder without the string Smoothie(…) /* @SmoothieBuilder */ { // "Filling and refreshing, this smoothie will fill you with joy!" /* let v0 = */ Ingredient.orange.measured(with: .cups).scaled(by: 1.5) /* let v1 = */ Ingredient.blueberry.measured(with: .cups) /* let v2 = */ Ingredient.avocado.measured(with: .cups).scaled(by: 0.2) /* return SmoothieBuilder.buildBlock(v0, v1, v2) */ } extension SmoothieBuilder { static func buildBlock(_ description: String, _ ingredients: ManagedIngredients...) -> (String, [ManagedIngredients]) { … } @available(*, unavailable, message: "missing ‘description’ field") static func buildBlock(_ ingredients: ManagedIngredients...) -> (String, [ManagedIngredients]) { fatalError() } } -
44:55 - Smoothie initializer (final) and SmoothieBuilder (with error handling)
extension Smoothie { init(id: Smoothie.ID, title: String, @SmoothieBuilder _ makeIngredients: () -> (String, [MeasuredIngredient])) { let (description, ingredients) = makeIngredients() self.init(id: id, title: title, description: description, measuredIngredients: ingredients) } } @resultBuilder enum SmoothieBuilder { static func buildBlock(_ description: String, components: MeasuredIngredient...) -> (String, [MeasuredIngredient]) { return (description, components) } @available(*, unavailable, message: "first statement of SmoothieBuilder must be its description String") static func buildBlock(_ components: MeasuredIngredient...) -> (String, [MeasuredIngredient]) { fatalError() } }
-