ハイライト
Swift 5.4 の Result Builders を使用すると、開発者は Swift 構文を使用して埋め込みドメイン固有言語を実装し、クロージャー内の複数の宣言を配列、ビュー ツリー、またはカスタム データ構造に収集して、ボイラープレート コードとメンテナンス エラーを削減できます。
主要内容
あなたはレシピ アプリを管理しています。各スムージーには ID、タイトル、説明、材料、支払いステータスがあります。これを記述する最も簡単な方法は、レシピごとに静的定数を作成し、全体の配列を維持することです。
問題はすぐに現れました。
新しいレシピを追加するとき、開発者は定数を作成するだけで、それらを全体の配列に含めるのを忘れる可能性があります。同じレシピを2回追加することも可能です。食材の列はまだ長いです。MeasuredIngredient、Measurement、measurementこれらの言葉は何度も出てきますが、本当に重要なメッセージは「オレンジ 1.5 カップ」です。
Fruta のレシピは、デザイナー、マーケティング担当者、マネージャーによっても更新されます。コードが初期化子の引数リストに似ているほど、エンジニア以外の人でもコードを修正するのが簡単になります。
このセッションで、Apple は Fruta を使用して、結果ビルダーを使用して埋め込み DSL (ドメイン固有言語) を作成するというソリューションをデモンストレーションしました。 DSLは依然としてSwiftです。末尾クロージャを使用します。@resultBuilderそして、連鎖修飾子メソッドは「配列の組み立て方法」を隠し、メンテナーはレシピ自体を書くことだけを可能にします。
最終的な目標は、レシピ リストを次のように記述することです。スムージーは関数本体に直接リストされます。有料コンテンツが必要な場合は、通常のコンテンツを使用してくださいif; 各スムージーの留め具には、最初に説明を書き、次に材料を書きます。 Swift コンパイラは、これらのステートメントをビルダーの静的メソッドに渡す役割を果たします。
詳細
Result Builder の機能: クロージャ内の式を収集します
(09:26) SwiftUIVStack最も身近な例です。クロージャに複数を書きますText明示的に配列を返したり、手動で呼び出したりすることなく、addSubview。
VStack {
Text("Title").font(.title)
Text("Contents")
}
キーポイント:
VStack末尾のクロージャを受け取ります。Text("Title").font(.title)最初のビューを作成し、同じステートメントで修飾子を適用します。Text("Contents")2 番目のビューを作成します。- クロージャ内のステートメントは次のようになります。
ViewBuilder集める、形成するVStackコンテンツ。
(09:50)ViewBuilder基本的な形式は次のとおりです。
@resultBuilder enum ViewBuilder {
static func buildBlock(_: View...) -> some View { … }
}
キーポイント:
@resultBuilderこの型を結果ビルダーとして使用できることをコンパイラーに伝えます。buildBlockクロージャで生成された複数の値を受け取ります。- 戻り値は、呼び出し元に与えられたクロージャの最終結果です。
- ここ
…セッション コード スニペットから、SwiftUI の内部実装が省略されていることを示します。
Result Builder はコンパイル時の機能です。新しいシステム バージョンは必要ありません。 Swift 5.4 では、Xcode 12.5 に対応する最終バージョンが言語に組み込まれています。
まず複数のスムージー ステートメントを配列に変換します
(24:05) Fruta の最初のステップは、SmoothieArrayBuilder。関数本体に複数を入れますSmoothieに集められる[Smoothie]。
@resultBuilder
enum SmoothieArrayBuilder {
static func buildBlock(_ components: Smoothie...) -> [Smoothie] {
return components
}
}
キーポイント:
enum SmoothieArrayBuilder大文字と小文字の区別はなく、静的メソッドのコンテナとしてのみ使用されます。buildBlockパラメータは次のとおりです。Smoothie...、スムージーを何個でも受け取ることができます。return components可変長パラメータを配列として返します。- このビルダーは、関数本体に 1 行のみの最も単純なリストに適しています。
Smoothie。
(24:39) コンパイラは関数本体を次のような形式に変換します。
// 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, …) */
}
キーポイント:
@SmoothieArrayBuilder関数にマークが付いている場合、関数本体はこのビルダーを使用します。- それぞれ
Smoothie(...)式は最初に一時値に保存されます。 - 最後の電話
SmoothieArrayBuilder.buildBlock。 buildBlock戻り値は関数本体の結果になります。
### サポートif:buildOptional、buildBlockそしてbuildExpression
(27:47) Fruta は以下に基づく必要があります。includingPaid有料レシピを含めるかどうかを決定します。普通ifビルダーの実装が必要ですbuildOptional。
最初はこのように書くだけでは十分ではありませんでした。
@resultBuilder
enum SmoothieArrayBuilder {
static func buildOptional(_ component: [Smoothie]?) -> [Smoothie] {
return component ?? []
}
static func buildBlock(_ components: Smoothie...) -> [Smoothie] {
return components
}
}
キーポイント:
buildOptional処理番号elseのif。- 条件が満たされると、
componentはいifin vivoで収集されたアレイ。 - 条件が満たされない場合、Swift が通過します
nil。 - このバージョンでは型の問題が発生します。
if結果は[Smoothie]、 古いbuildBlockまだまだシングル楽しみですSmoothie。
(31:44) 正しいバージョンが追加されましたbuildExpression。通常のステートメント内の 1 つの単語を次のように変換します。Smoothieにも変換されました[Smoothie]。
@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]
}
}
キーポイント:
buildExpression(_ expression: Smoothie)まずはそれぞれを裸にするSmoothie単一要素の配列にラップされます。buildOptionalこちらも返却[Smoothie]。buildBlock複数受信[Smoothie]。flatMap { $0 }複数の配列を 1 つの配列にフラット化します。
このステップは、DSL の最初の実際のメンテナンスの問題を解決します。Swift のif、追加のメンテナンスは必要ありませんhasFreeRecipeそしてフィルタリングロジック。
### サポートif-elseおよびログステートメント
(32:48)else条件分岐には以下が必要ですbuildEither(first:)そしてbuildEither(second:)。フルッタさんのelseとも呼ばれますlogger.log(...)、この式は次の値を返します。Void、したがって、ビルダーも受け入れる必要がありますVoid。
@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 []
}
}
キーポイント:
buildEither(first:)引き継ぐif分岐結果。buildEither(second:)引き継ぐelse分岐結果。- Fruta はどの分岐が選択されるかを気にしないため、どちらのメソッドもパラメータを直接返します。
buildExpression(_ expression: Void)ログ ステートメントを空の配列に変換します。- 空の配列は
buildBlock結合すると、スムージー リストに要素は追加されません。
(32:53) コンパイラif-else変換方法はこのように理解できます。
// 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) */
}
キーポイント:
includingPaidまだ通常のスイフトの状態です。if支店でSmoothie最初にパスするbuildExpression。- 支店内で再度電話をかける
buildBlock。 -分岐結果についてbuildEitherパッケージ。 - 最外層を再度呼び出します
buildBlock最終結果をマージします。
修飾子メソッドを使用して成分を圧縮する記述メソッド
(36:41) 材料行の定型コードが多すぎます。 Apple は 2 つの修飾子スタイルのメソッドを使用して、「ユニット」と「複数」をチェーン呼び出しに分割します。
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)
}
}
キーポイント:
measured(with:)で定義されていますIngredient優れた。- 受信したものを使用します
UnitVolume数量1を作成しますMeasuredIngredient。 scaled(by:)で定義されていますMeasuredIngredient優れた。- 新しいものを返します
MeasuredIngredient、測定値に入力倍率が乗算されます。 Ingredient.orange.measured(with: .cups).scaled(by: 1.5)レシピ管理者が気にする情報 (オレンジ、カップ、1.5) のみを保持します。
この設計には追加の利点もあります。 Fruta のレシピ ページでは、成分ビューの各行に比率が渡されます。scaled(by:)表示されたら、最初に成分自体を拡大縮小してから、ビュー レイヤーのコードを少なくして、それらを行ビューに渡すことができます。
各スムージー クロージャにビルダーを追加します
(37:32)SmoothieArrayBuilderでのみ動作しますall関数本体。Smoothie(...) { ... }内側の末尾のクロージャは別の関数本体であり、外側のビルダーは自動的にそれに影響を与えません。
// 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) */
}
キーポイント:
- 外層
@SmoothieArrayBuilder集めるSmoothie(...)これらの表現。 Smoothie説明と成分を返すには、末尾のクロージャが必要です。- 外側のビルダーは、この後続クロージャに入りません。
- にいる必要がある
Smoothieイニシャライザのクロージャパラメータで新しいビルダーをマークします。
(39:22) 新しいビルダーが呼び出されます。SmoothieBuilder。最初のステートメントが説明文字列であり、その後に任意の数の説明文字列が続く必要があります。MeasuredIngredient。
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)
}
}
キーポイント:
@SmoothieBuilderクロージャパラメータの前に記述されます。makeIngredients実行後に戻る(String, [MeasuredIngredient])。- イニシャライザはタプルを次のように分割します。
descriptionそしてingredients。 - 最後の行は既存のイニシャライザを呼び出して渡します。
descriptionそしてmeasuredIngredients。 SmoothieBuilder.buildBlockの最初のパラメータはStringしたがって、クロージャの最初の行は説明でなければなりません。components: MeasuredIngredient...後続の材料を集めます。
エラー用の特別なオーバーロードを作成する
(44:55) メンテナが説明を書き忘れた場合、Swift はデフォルトで「最初の成分を文字列に変換できません」と報告します。このエラーは正確ですが、DSL ユーザーにとっては十分に分かりやすいものではありません。
Apple のアプローチは、ビルダーにこの誤った記述を照合させてから、@available(*, unavailable, message:)専門的な情報を提供します。
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()
}
}
キーポイント:
- 初め
buildBlock正当なパスです: 説明文字列と成分。 - 2番目
buildBlock「材料のみ、説明なし」に一致するパスが間違っています。 @available(*, unavailable, message:)このパスを指定するとコンパイル時にエラーが発生します。fatalError()関数本体の型チェックを通過させるだけでは、このメソッドは正常に実行されません。- エラー メッセージには DSL ルールが直接記載されています。最初のステートメントは説明文字列である必要があります。
重要ポイント
-
何をすべきか: DSL をアプリのルーティング テーブルに書き込みます。 実行する価値がある理由: セッションでは、サーバー フレームワークによりユーザーが URL とハンドラーを宣言できるようになり、フレームワークがそれらを登録できると述べました。 Result Builder は、一連のステートメントを収集するのに適しています。 開始方法: 定義
Route入力してから書き込みます@resultBuilder enum RouteBuilder、使用buildBlock(_ components: Route...) -> [Route]ルートを収集します。 -
対処方法: 複雑なフォーム定義を宣言型リストに変更します。 実行する価値がある理由: フォーム フィールドは製品や業務によって調整されることがよくあります。 DSL は、配列括弧、カンマ、および繰り返される初期化パラメータを減らすことができます。 開始方法:
FormFieldビルダーをビルドして使用するbuildOptionalユーザーの役割に応じたフィールドの表示をサポートし、修飾子メソッドを使用して検証ルールを記述します。 -
やるべきこと: ゲーム レベルまたは NPC ダイアログ用に Swift DSL を作成します。 実行する価値がある理由: セッションは、テキスト アドベンチャー ゲームのルーム マップと NPC の対話はデザイナーやライターによって維持されることが多いと指摘しています。 DSL を使用すると、コンテンツ構造のみに集中できます。 開始方法: 定義
Room、DialogLine、Choice、ネストされたビルダーを使用して、ルームと会話のブランチを個別に収集します。 -
何をすべきか: 関数スイッチ設定をコンパイル可能でチェック済みの Swift コードに書き込みます。 実行する価値がある理由: Result Builder は、通常の Swift 条件ステートメントを DSL に組み込んで、一時配列の手動スプライシングを回避できます。 開始方法: 定義
FeatureFlag、成し遂げるbuildExpression単一のフラグを配列にラップして実装するbuildEither環境の分岐をサポートします。 -
何をすべきか: デザイン システム コンポーネントのプレビュー データ DSL を書き込みます。 これを行う価値がある理由: サンプル データは反復的で量が多く、順序を調整する必要があることがよくあります。ビルダーは複数の例を配列に収集でき、修飾子メソッドは共通の属性を圧縮できます。 開始方法: 定義
ComponentExample、使用@ExampleBuilder例を収集します。例に追加.state(...)、.locale(...)このタイプの修飾子。
関連セッション
- Swift の新機能 — 日常のコーディングに影響を与える構文やツールの改善など、Swift 5.5 の言語の更新について説明します。
- Swift の async/await について — Swift が言語機能を使用して非同期コードを簡素化する方法を学びます。
- Swift の構造化同時実行性を調べる — Swift 同時実行モデルがどのようにタスクを編成し、伝播をキャンセルするかを理解します。
- コレクションを使用して Swift パッケージを発見し、キュレートする — チームやコミュニティ向けに Swift パッケージ コレクションを整理する方法を学びます。
- Swift アルゴリズムとコレクション パッケージの紹介 — Swift 公式パッケージのアルゴリズムとコレクション タイプの拡張機能について学びます。
コメント
GitHub Issues · utterances