先日、Bonfire iOS #6 というイベントで、「SwiftUI を理解するために必要な Swift 5.1 の新機能 (some View編)」という15分のトークをやりました。
「SwiftUI を理解するために必要な Swift 5.1 の新機能 (some View編)」の発表資料です! #yjbonfire https://t.co/eWXYU36YI6
— Monkuma 👾 (@kumamo_tone) 2019年8月7日
Bonfire iOSは自分で立ち上げて毎回企画してきたイベントで、今回は後輩に運営の旗振りをお願いしたところ、自分も発表する流れになり、不安を感じつつもなんとか発表にこぎつけることができました。
ありがたい反応もいただいています。
めちゃめちゃわかりやすい
— かしはら (@kashihararara) August 7, 2019
ありがたみ https://t.co/WV8LE3y1h8
謎構文を理解できて助かりました🙏SwiftUI を理解するために必要な Swift 5.1 の新機能 (some View編) - Speaker Deck https://t.co/rqRdg4MEAD
— おつくつん (@otukutun) August 8, 2019
資料は Speaker Deck にありますが、資料だけだと分かりづらい部分もあるかと思うので、今回は実験的に書き起こしを用意してみました。
当日来られなかった方や、発表の復習に使いたい方などに使っていただければ幸いです。
(ここから書き起こし)
それでは発表を始めさせていただきます。
今日は SwiftUI のコードを読み書きする上で、知っておくと良い、Swift 5.1 の新機能についてお話ししようと思います。
具体的には、ここに書いてある3つについてです。
この発表のモチベーションとしては、自分がこの手の言語機能の説明とか読むと、全然理解できなくて、自分理解力ないなーって落ち込むことが多いんですが、
分解して解決すれば結構すんなりわかることもあって、主には、そういう自分のような一人で勉強して落ち込みやすいタイプなどに対して、理解の一助になるお話になればと思います。
最初に、テキストを上から2つ並べるViewを思い浮かべてください。
これをSwiftUIで書くと、こうなります。
titleにHello, subtitleに Worldが入っています。
VStackというスタックに、普通のテキストと、グレーのテキストを追加することで実現できます。
このstructは、
Viewというプロトコルに適合しています。
Viewプロトコルは、SwiftUIで定義されている、var bodyという、get-only な computed propertyをただひとつ持つ、Swiftのプロトコルです。
とても単純な例ですが、今までのSwiftの文法で考えると、不自然な点があることに気づくでしょうか。
まず、someという見慣れないキーワードがプロトコルの前についています。
また、Vstackはいわゆるトレイリングクロージャで、中でTextという型のインスタンス生成が行われているように見えますが、これは何をしているのでしょうか?
なぜこのようにインスタンスの生成を行うだけで、Viewを定義したことになるのでしょうか。
また、勘のいい人は、Computed property なのに return が無いことに気づいたかもしれません。
このように、とても単純な例ですが、今までのSwiftの文法から考えると、説明できない部分がいくつかあります。
これは、AppleがXcodeの中でいい感じにしてくれる魔法なのでしょうか。
もちろん、魔法ではありません。
これらはSwift5.1に入ったこの3つの機能で説明することができます。
これらはおそらくSwiftUIのために追加された機能ではあるのですが、SwiftUIに限定されるような話ではありません。
この発表では、先程のコードがなぜSwiftのコードとして解釈できるのか、わかるようになることを目指したいと思います。
では早速、順に見ていきましょう。
1つ目は、Implicit return from single expressions です。
これはすごく簡単で、式がひとつしかないとき、
関数やComputed Properyでreturnを省略できるようになったというものです。
今までもクロージャでは式が一つしかないとき、returnを省略できたと思うんですが、
それが今回、関数や、Computed Propertyでもできるようになりました。
このコードは、Swift 5.1未満では、 widthの前に return がないとコンパイルエラーになります。
というわけで、さきほどのコードをもう一度見てみましょう。
Vstackはsingle expressionなのでreturnが省略されています。
つまりここに、
return を書いたとしても、コンパイルできますし、同じ挙動になります。
1つ目の説明は以上です。これはこの後も出てくるので一番最初に説明しました。
ここからはちょっと難しくなります。
次にお話するのはFunction buildersについてです。
Function Builder は、改行区切りの要素たちから、変数宣言と関数呼び出しを生成してくれる機能です。
具体的には、@_functionBuilderアトリビュートをつけた関数が、function builderになります。
たとえば、 TupleBuilder というものが、FunctionBuilderとして定義されているとします。
この場合、@TupleBuilderのついた関数に、1,2,3とInt型の要素を上から改行区切りで順に記述すれば、
コンパイラが let a = 1, let b = 2, let c = 3, そしてその3つの値のタプルを返すreturn に変換してくれます。
とりあえず単純なFunction Builderを例にしてみましたが、今回のコードではどこで使われているのでしょうか。
Xcodeで、VStackをcommand +クリックして、定義を見てみます。
VStackの定義はこのようになっています。
第1引数と第2引数はalignmentやspacingが指定できますが、デフォルト引数が指定されています。
大事なのは第3引数で、あるViewに適合した型を返す関数を引数にとっています。
ここに、@ViewBuilderアトリビュートがついています。
この@ViewBuilderとは一体なんでしょうか?さらに定義を見てみましょう。
ViewBuilderは、どうやらViewに適合した型を引数として取り、同じ型を返すbuildBlockという関数を持っているようです。
さらに下を見ていくと、extensionの形で、buildBlockの、引数2個のバージョンが書いてあります。
先程の例で使われていた実装はこれです。
ViewBuilderは、2個の要素が書かれている場合、それを変数宣言と、この2個バージョンのbuildBlockのreturnに変換します。
これが、VStackの引数の正体でした。
ちなみにこのViewBuilderのbuildBlockの実装は、3個のバージョン、
4個のバージョン…
10個のバージョンまであります。
つまり、10個までViewを並べて書けるということです。
じゃあ11個以上は要素を並べて書けないのかというと、その通りで、
個数が多くなる場合はGroupというクラスを使ってまとめたりする必要があります。Viewをいっぱい書いてコンパイルエラーになったときには思い出してみてください。
なにはともあれ、このVstackのtrailing closure、つまり、Vstackのイニシャライザの第3引数のViewBuilderの関数は、Function Builderによって
このように解釈されます。
このときの返り値の型は、TupleView<(Text,Text)>になります。
そしてさらに、外側の返り値の型は、
VStack の型パラメータが TupleView<(Text, Text)> なので、最終的な body の返り値の型は、 Vstack<TupleView<(Text, Text)>> になります。
このように、既存のSwiftの文法を使って、引数の組み合わせで書くこともできますが、
Function Builder があることで、改行区切りで簡潔に記述ができるようになっています。
@_functionBuilderは、普通にコード上で利用できて、意外と簡単にFunction Builderを自分でも作ることができます。これはInt型の配列に対応したFunction Builderです。
アンダーバーがついているアノテーションは、仕様が決まりきっていないとか、基本的に使われることを想定していないものだと思います。
そのため、まだ形などは変わるかもしれませんが、しかしこれを利用することでかなり簡潔に処理が書けるようになるので、今後はこれを利用したライブラリなども増えてくるのではないかと思います。
最後に、Opaque Result Typeについてお話します。
Opaque Result Type、これは簡単に言ってしまうと、ジェネリクスのようなものです。
some Protocol のように型名宣言されているものがOpaque Result Typeで、some View は、意味としては 「具体的な型は公開しないけど、Viewプロトコルに適合する何らかの型」であることを表します。
これだけだとわかりにくいですが、たとえば、
この場合の some Viewの中身の返り値は、Function Builder によってつくられた、VStack<TupleView<(Text,Text)> 型でした。これが実際の型です。
しかし外側から見たときは、具体的な型は公開されておらず、Viewプロトコルに適合する何らかの型として振る舞うことができます。
もちろん、some View ではなく、具体的な型を指定してもOKです。
ちなみにもはやこの形なら、returnも省略されていないですし、function builderも使っていないので、Swift5.1未満の文法でも意味が通りますね。
これで文法上の意味を把握するという目的は達成できたのですが、じゃあなんでOpaque Result Typeがあると嬉しいかというのを最後にお話します。
まず第一にすっきりするからです。
スタックが一つのうちはいいですが、どんどんネストしていくので、
普通に書くとやってられないほど長くなっていきます。うれしいのはそれだけではありません。
コンパイル時に何らかの型であることが確定するため、実行時にパフォーマンスロスがないというのがOpaque Result Typeのポイントになります。
先ほどもお話したように、Opaque Result Type はジェネリクスのようなものとみなすことができます。内側から決まるジェネリクスということもできると思います。
たとえばmap関数は関数を記述するときには特定の型を指定していませんが、呼び出し側がIntとIntを渡しているので、その関数の引数と返り値がIntとIntであることが、コンパイル時に確定します。
同じように、some Viewも、関数を記述するときにはsome Viewと、特定の型を指定していませんが、コンパイル時には、具体的な型を確定させることができます。
コンパイル時に具体的な型が確定しているのは、従来のプロトコルを指定する形とはお大きく違うところで、たとえば、bodyの型をViewと指定すると、コンパイルエラーになります。
Viewはプロトコルで、さらにassociatedtypeを持っているからです。
これを解決するために、型消去などの方法を使うこともできます。
型消去は、たとえばViewプロトコルに準拠した型を用意して、それをラッパーとして扱うことで、そのプロトコルに準拠した型を格納する場所を作る方法です。
型消去によって、見かけ上some Viewと同じことはできますが、呼び出し時にオーバーヘッドが生じてしまうため、パフォーマンス的に良くないという問題がありました。
Opaque Result Type であれば、コンパイル時には何かしらの具体型に落ちているため、実行パフォーマンスを保ち、実際の型を隠蔽しながら、すっきりとした記述ができるようになります。
Opaque Result Type の説明は以上です。というわけで、すべての説明が終わりました。
話をまとめながらおさらいしてみましょう。
まずこのコードではreturnが省略されていて、
VStackの引数はFunction Builder によって、ViewBuilder.buildBlock(Text(),text()) と解釈されて、
実際には、VStack<TupleView<(Text, Tet)>>>型のオブジェクトを返しているのでした。
そして、some ViewはViewに適合するなんらかの型であるということを表していて、
これは関数の内側からコンパイル時に具体型が確定するので、パフォーマンスロスもなく、実際の型を隠蔽することができるのでした。
いかがでしたでしょうか。
今日はSwiftUIのbodyプロパティを読むのに必要な、3つのSwiftUIの機能を紹介しました。これらの機能によって、SwiftUIのシンプルな記述が可能になっています。
実際結構ややこしいトピックなので、なかなか馴染みのない場合は理解が時間がかかるかもしれませんが、この発表がとっかかりになれば幸いです。
参考文献のSwift Evolutionのプロポーザルはこちらになります。
参考にさせていただいたネットの記事などはこちらになります。
SpeakerDeckのほうからもリンクを貼ってあるのでご参照ください。
自分からの発表は以上になります。ご清聴ありがとうございました。