この投稿は何?
SwiftUIのViewBuilderについて、基本的な仕組みを解説します。
Swiftを基礎から学ぶには
自著、工学社より発売中の「まるごと分かるSwiftプログラミング」をお勧めします。変数、関数、フロー制御構文、データ構造はもちろん、構造体からクロージャ、エクステンション、プロトコル、クロージャまでを基礎からわかりやすく解説しています。
また、Swiftプログラミングを基礎から動画で学びたい方には、Udemyコース「今日からはじめるプログラミング」をお勧めします。
実行環境
macOS 11.4
Xcode 12.5.1
Swift 5.4
ハンズオン
ここでは、挨拶ラベルを生成するプログラムを考えます。
例えば、Ravi Patelさんに向けには下のような挨拶ラベルを生成します。
***Hello RAVI PATEL!**
挨拶ラベルの生成ルールは、以下の通りです。
- 名前の前後には、任意の数だけアスタリスク記号
*を配置する - 名前の直前に
Helloをつける - 名前は大文字にする
- 名前の後には感嘆符
!を配置する -
Helloと名前の間には、半角スペースを配置する
手続き的なアプローチで実装する
まず、Drawableプロトコルを宣言します。
protocol Drawable {
func draw() -> String
}
このプロトコルに適合する型は、draw()メソッドの実装が要求されます。
次に、「名前を示すテキスト」や「星印を示すアスタリスク記号」、スペースなどは以下のようにモデル化します。
どの構造体も、Drawableプロトコルに適合しているので、draw()メソッドを呼び出すと、最適化された形式でコンソールに出力できることが保証されます。
struct Text: Drawable {
var content: String
init(_ content: String) { self.content = content }
func draw() -> String { return content }
}
struct Space: Drawable {
func draw() -> String { return " " }
}
struct Stars: Drawable {
var length: Int
func draw() -> String { return String(repeating: "*", count: length) }
}
struct AllCaps: Drawable {
var content: Drawable
func draw() -> String { return content.draw().uppercased() }
}
これらの構造体は、それぞれが「挨拶ラベルを構成する要素」です。
次に、「挨拶ラベルの要素」を連結して「一連の文字列」にする構造体を定義します。この型もDrawableプロトコルに適合することで、draw()メソッドの呼び出しが保証されます。
struct Line: Drawable {
var elements: [Drawable]
func draw() -> String {
return elements.map { $0.draw() }.joined(separator: "")
}
}
それでは、Ravi Patelさんへの挨拶ラベルを生成します。
let name = "Ravi Patel"
let manualDrawing = Line(elements: [
Stars(length: 3),
Text("Hello"),
Space(),
AllCaps(content: Text(name + "!")),
Stars(length: 2),
])
Line型イニシャライザのパラメータは、「Drawableプロトコルに適合した型のインスタンス」を配列の要素として受け取ります。
作成したLine型インスタンスのdraw()メソッドを呼び出すと、挨拶ラベルを出力できます。
print(manualDrawing.draw())
// Prints "***Hello RAVI PATEL!**"
条件分岐を実装する
ここで、特定の誰か向けでない場合にも挨拶ラベルを作成できるようにします。
nameがnilだった場合は、***Hello WORLD!**という挨拶ラベルを作成します。
let name: String? = nil
let manualDrawing = Line(elements: [
Stars(length: 3),
Text("Hello"),
Space(),
AllCaps(content: Text((name ?? "World") + "!")),
Stars(length: 2),
])
print(manualDrawing.draw())
// Prints "***Hello WORLD!**"
このコードは問題なく動作しますが、AllCapsの後、括弧の入れ子が深くなっているせいで、コードが読みやすくありません。
「nameがnilならば、"World"にする」というロジックをインライン記述するために、??演算子を使用する必要があります。
ただし、より複雑なロジックに対応することは難しいでしょう。
挨拶ラベルを作成するために、switch構文やforループ構文を含める方法もありません。
宣言的なアプローチ
リザルトビルダーを利用すると、このようなコードを「通常のSwiftコード」のように書き換えることができます。
リザルトビルダーは、構造体に@resultBuilder属性をマークすることで定義できます。
@resultBuilder
struct DrawingBuilder {
// implementation here...
}
@resultBuilder属性をマークした構造体では、buildBlockという名前のタイプメソッドの実装が要求されます。
@resultBuilder
struct DrawingBuilder {
static func buildBlock(_ components: Drawable...) -> Drawable {
return Line(elements: components)
}
}
パラメータに「作成した@DrawingBuilder属性」をマークします。
func draw(@DrawingBuilder content: () -> Drawable) -> Drawable {
return content()
}
この関数の呼び出し側で記述するクロージャ式は、宣言的なアプローチで実装できます。
let name = "Ravi Patel"
let greeting = draw {
Stars(length: 3)
Text("Hello")
Space()
AllCaps(content: Text(name))
Stars(length: 2)
}
print(greeting.draw())
// Prints "***Hello RAVI PATEL!**"
if構文による条件分岐
リザルトビルダー属性でマークした型の定義に、以下に挙げる2つのタイプメソッドを実装します。
-
buildEither(first:)メソッド -
buildEither(second:)メソッド
どちらのメソッドも、受け取ったパラメータをそのまま返すだけで、宣言的なコード内でif構文が記述できるようになります。
@resultBuilder
struct DrawingBuilder {
static func buildBlock(_ components: Drawable...) -> Drawable {
return Line(elements: components)
}
static func buildEither(first component: Drawable) -> Drawable {
return component
}
static func buildEither(second component: Drawable) -> Drawable {
return component
}
}
宣言的なコードの中に、if構文を含めることができるようになりました。
let name: String? = nil
let greeting = draw {
Stars(length: 3)
Text("Hello")
Space()
if let name = name {
AllCaps(content: Text(name))
} else {
let world = "world!"
AllCaps(content: Text(world))
}
Stars(length: 2)
}
print(greeting.draw())
// Prints "***Hello WORLD!**"
draw()メソッドの呼び出し側ではコードを宣言的に記述できるので、手続き的なアプローチよりも読みやすくなります。
その上、より込み入った構文も記述できるメリットも享受できます。