Edited at

SwiftUIの魔法を実現する仕組み (Custom Attributes, Function Builder)


Introduction

WWDC19でSwiftUIが発表されてiOS開発者界隈は騒然となりました。11年あるiOSの歴史のなかでSwiftの発表に次ぐ大きなインパクトのあるリリースだと言っても過言ではないでしょう。内容としては、Flutter, Kotlin Jetpack Composeに次ぐReact-likeなView構成フレームワークなのですが、iOS開発が大幅に効率化することは間違いありません。Flutterの導入なども本格的に検討していたので、本格投入する前に発表があって正直ほっとしています。

hero-lockup-medium_2x.png


不思議構文

さて、発表を見て、よく訓練されたiOSエンジニアの方はひと目で気がついたかも知れませんが、スライドに写ったVStackを作るところで不思議構文が登場します。

VStack(alignment: .leading) {

Text(item.title)
Text(item.subtitle)
.color(.gray)
}

SwiftのClosureは別のthisコンテキストを持ったり(*1)、複数の返り値をもつことがない(*2)のにもかかわらず、VStackに渡されたClosureが期待通りに解釈されて、VStack<TupleView<(Text, Text)>> の値を返しています。

ひょっとすると実行時の評価だけやっているのかなと思って、試しに3を挿入してみると、

VStack(alignment: .leading) {

3
Text(item.title)
Text(item.subtitle)
.color(.gray)
}

きちんとコンパイルエラーになってくれます。不思議ですね。


TL;DR

VStackのinitializerの引数で ViewBuilder というCustom Attributeを用いていて、

VStack(alignment: .leading) {

Text(item.title)
Text(item.subtitle).color(.gray)
}

上のコードは、以下のように変換されます。

struct ContentView : View {

var body: some View {
VStack(alignment: .leading) {
ViewBuilder.buildBlock(
Text(item.title),
Text(item.subtitle).color(.gray)
)
}
}
}

この、ViewBuilderは非公式の機能であるFunction Builderを用いて実現されていて、我々もこの機能を使うことができます。実際にやってみましょう。


Usage of FunctionBuilder


作るもの

blockAdd {

3
4
}

で3 + 4が実行されて、7が返ってくる魔法関数を作ってみます。


CustomAttributeを定義してみる

これで、CustomAttributeが定義できたことになります。SwiftUIではViewBuilderに対応する部分です。

@_functionBuilder public struct BlockAdder {

public static func buildBlock(_ a: Int) -> Int {
return a
}

public static func buildBlock(_ a: Int, _ b: Int) -> Int {
return a + b
}

public static func buildBlock(_ a: Int, _ b: Int, _ c: Int) -> Int {
return a + b + c
}

public static func buildBlock(_ a: Int, _ b: Int, _ c: Int, _ d: Int) -> Int {
return a + b + c + d
}
}


関数を定義

SwiftUIのソースコードでは、VStackのinitializerに対応しています。

func blockAdd(@BlockAdder block: () -> Int) -> Int {

return block()
}


使ってみる

let one = blockAdd {

1
} // 1
let three = blockAdd {
1 // SwiftUIのサンプルではTextに対応するところ
2
} // 3 (1 + 2)
let six = blockAdd { 1; 2; 3 } // 6
let ten = blockAdd { 1; 2; 3; 4 } // 10 (1 + 2 + 3 + 4)

望み通りの結果が得られました。


感想

私の知る限りでは、Swiftで自前でAttirbuteを実装できたのはこれが初めてなのでちょっと感慨深いです。

自由自在にAttributeを作れる機能についても議論されているので、SwiftでもRetrofitのように便利な(easyな)ライブラリが生まれる日が近いのかも知れません。

https://forums.swift.org/t/pitch-introduce-custom-attributes/21335

最後に、フランスの詩人ラ・フォンティーヌが書いた『寓話』の『裁判官と修道士と隠者』にある言葉を引用して筆を置きたいと思います。


すべての道はJavaに通ず - All roads lead to Java.



バージョン情報

Apple Swift version 5.1 (swiftlang-1100.0.38.29 clang-1100.0.20.14)

Target: x86_64-apple-darwin18.6.0


参考

apple/swiftのFunctionBuilderに関するPull Request

https://github.com/apple/swift/pull/25221

ForumのFunctionBuilderに関するdiscussion

https://forums.swift.org/t/pitch-function-builders/25167/10

ViewBuilderのドキュメント

https://developer.apple.com/documentation/swiftui/viewbuilder

故事ことわざ辞典

http://kotowaza-allguide.com/su/subetenomichi.html


注意

画像はこちら(https://developer.apple.com/xcode/swiftui/ )からの転載です。Apple Inc.かIncrements Inc.の方は twitter: _kentino にご連絡ください。苦情・マサカリも上記まで。