kumamotone’s blog

iOS/Android アプリエンジニアです https://twitter.com/kumamo_tone

「SwiftUI を理解するために必要な Swift 5.1 の新機能 (some View編)」の書き起こし

先日、Bonfire iOS #6 というイベントで、「SwiftUI を理解するために必要な Swift 5.1 の新機能 (some View編)」という15分のトークをやりました。

Bonfire iOSは自分で立ち上げて毎回企画してきたイベントで、今回は後輩に運営の旗振りをお願いしたところ、自分も発表する流れになり、不安を感じつつもなんとか発表にこぎつけることができました。

ありがたい反応もいただいています。

資料は Speaker Deck にありますが、資料だけだと分かりづらい部分もあるかと思うので、今回は実験的に書き起こしを用意してみました。

当日来られなかった方や、発表の復習に使いたい方などに使っていただければ幸いです。

(ここから書き起こし)

f:id:kumamotone:20190809075419p:plain

それでは発表を始めさせていただきます。

f:id:kumamotone:20190809063412p:plain

今日は SwiftUI のコードを読み書きする上で、知っておくと良い、Swift 5.1 の新機能についてお話ししようと思います。

具体的には、ここに書いてある3つについてです。

この発表のモチベーションとしては、自分がこの手の言語機能の説明とか読むと、全然理解できなくて、自分理解力ないなーって落ち込むことが多いんですが、

分解して解決すれば結構すんなりわかることもあって、主には、そういう自分のような一人で勉強して落ち込みやすいタイプなどに対して、理解の一助になるお話になればと思います。

f:id:kumamotone:20190809063439p:plain

最初に、テキストを上から2つ並べるViewを思い浮かべてください。

f:id:kumamotone:20190809063545p:plain

これをSwiftUIで書くと、こうなります。

titleにHello, subtitleに Worldが入っています。

VStackというスタックに、普通のテキストと、グレーのテキストを追加することで実現できます。

このstructは、

f:id:kumamotone:20190809063614p:plain

Viewというプロトコルに適合しています。

Viewプロトコルは、SwiftUIで定義されている、var bodyという、get-only な computed propertyをただひとつ持つ、Swiftのプロトコルです。

f:id:kumamotone:20190809063637p:plain

とても単純な例ですが、今までのSwiftの文法で考えると、不自然な点があることに気づくでしょうか。

f:id:kumamotone:20190809063651p:plain

まず、someという見慣れないキーワードがプロトコルの前についています。

f:id:kumamotone:20190809063706p:plain

また、Vstackはいわゆるトレイリングクロージャで、中でTextという型のインスタンス生成が行われているように見えますが、これは何をしているのでしょうか?

なぜこのようにインスタンスの生成を行うだけで、Viewを定義したことになるのでしょうか。

f:id:kumamotone:20190809063720p:plain

また、勘のいい人は、Computed property なのに return が無いことに気づいたかもしれません。

f:id:kumamotone:20190809063737p:plain

このように、とても単純な例ですが、今までのSwiftの文法から考えると、説明できない部分がいくつかあります。

f:id:kumamotone:20190809063750p:plain

これは、AppleXcodeの中でいい感じにしてくれる魔法なのでしょうか。

f:id:kumamotone:20190809063803p:plain

もちろん、魔法ではありません。

f:id:kumamotone:20190809063920p:plain

これらはSwift5.1に入ったこの3つの機能で説明することができます。

これらはおそらくSwiftUIのために追加された機能ではあるのですが、SwiftUIに限定されるような話ではありません。

この発表では、先程のコードがなぜSwiftのコードとして解釈できるのか、わかるようになることを目指したいと思います。

f:id:kumamotone:20190809064115p:plain

では早速、順に見ていきましょう。

f:id:kumamotone:20190809064129p:plain

1つ目は、Implicit return from single expressions です。

f:id:kumamotone:20190809064146p:plain

これはすごく簡単で、式がひとつしかないとき、

関数やComputed Properyでreturnを省略できるようになったというものです。

今までもクロージャでは式が一つしかないとき、returnを省略できたと思うんですが、

f:id:kumamotone:20190809064208p:plain

それが今回、関数や、Computed Propertyでもできるようになりました。

このコードは、Swift 5.1未満では、 widthの前に return がないとコンパイルエラーになります。

f:id:kumamotone:20190809064241p:plain
というわけで、さきほどのコードをもう一度見てみましょう。

Vstackはsingle expressionなのでreturnが省略されています。

つまりここに、

f:id:kumamotone:20190809064255p:plain
return を書いたとしても、コンパイルできますし、同じ挙動になります。

f:id:kumamotone:20190809064309p:plain

1つ目の説明は以上です。これはこの後も出てくるので一番最初に説明しました。

ここからはちょっと難しくなります。

f:id:kumamotone:20190809064323p:plain

次にお話するのはFunction buildersについてです。

f:id:kumamotone:20190809064335p:plain

Function Builder は、改行区切りの要素たちから、変数宣言と関数呼び出しを生成してくれる機能です。

具体的には、@_functionBuilderアトリビュートをつけた関数が、function builderになります。

f:id:kumamotone:20190809064350p:plain

たとえば、 TupleBuilder というものが、FunctionBuilderとして定義されているとします。

この場合、@TupleBuilderのついた関数に、1,2,3とInt型の要素を上から改行区切りで順に記述すれば、

コンパイラが let a = 1, let b = 2, let c = 3, そしてその3つの値のタプルを返すreturn に変換してくれます。

f:id:kumamotone:20190809064406p:plain

とりあえず単純なFunction Builderを例にしてみましたが、今回のコードではどこで使われているのでしょうか。

Xcodeで、VStackをcommand +クリックして、定義を見てみます。

f:id:kumamotone:20190809064430p:plain

VStackの定義はこのようになっています。
第1引数と第2引数はalignmentやspacingが指定できますが、デフォルト引数が指定されています。
大事なのは第3引数で、あるViewに適合した型を返す関数を引数にとっています。
ここに、@ViewBuilderアトリビュートがついています。

f:id:kumamotone:20190809064446p:plain

この@ViewBuilderとは一体なんでしょうか?さらに定義を見てみましょう。
ViewBuilderは、どうやらViewに適合した型を引数として取り、同じ型を返すbuildBlockという関数を持っているようです。

f:id:kumamotone:20190809064505p:plain

さらに下を見ていくと、extensionの形で、buildBlockの、引数2個のバージョンが書いてあります。

f:id:kumamotone:20190809064535p:plain

先程の例で使われていた実装はこれです。

ViewBuilderは、2個の要素が書かれている場合、それを変数宣言と、この2個バージョンのbuildBlockのreturnに変換します。

これが、VStackの引数の正体でした。

f:id:kumamotone:20190809064553p:plain

ちなみにこのViewBuilderのbuildBlockの実装は、3個のバージョン、

f:id:kumamotone:20190809064605p:plain

4個のバージョン…

f:id:kumamotone:20190809070615p:plain

10個のバージョンまであります。
つまり、10個までViewを並べて書けるということです。

f:id:kumamotone:20190809070633p:plain
じゃあ11個以上は要素を並べて書けないのかというと、その通りで、

個数が多くなる場合はGroupというクラスを使ってまとめたりする必要があります。Viewをいっぱい書いてコンパイルエラーになったときには思い出してみてください。

f:id:kumamotone:20190809070905p:plain

なにはともあれ、このVstackのtrailing closure、つまり、Vstackのイニシャライザの第3引数のViewBuilderの関数は、Function Builderによって

f:id:kumamotone:20190809070931p:plain

このように解釈されます。

f:id:kumamotone:20190809071012p:plain

このときの返り値の型は、TupleView<(Text,Text)>になります。

f:id:kumamotone:20190809071025p:plain

そしてさらに、外側の返り値の型は、

VStack の型パラメータが TupleView<(Text, Text)> なので、最終的な body の返り値の型は、 Vstack<TupleView<(Text, Text)>> になります。

このように、既存のSwiftの文法を使って、引数の組み合わせで書くこともできますが、
Function Builder があることで、改行区切りで簡潔に記述ができるようになっています。

f:id:kumamotone:20190809071046p:plain

@_functionBuilderは、普通にコード上で利用できて、意外と簡単にFunction Builderを自分でも作ることができます。これはInt型の配列に対応したFunction Builderです。

アンダーバーがついているアノテーションは、仕様が決まりきっていないとか、基本的に使われることを想定していないものだと思います。

そのため、まだ形などは変わるかもしれませんが、しかしこれを利用することでかなり簡潔に処理が書けるようになるので、今後はこれを利用したライブラリなども増えてくるのではないかと思います。

f:id:kumamotone:20190809071102p:plain

最後に、Opaque Result Typeについてお話します。 f:id:kumamotone:20190809071123p:plain

Opaque Result Type、これは簡単に言ってしまうと、ジェネリクスのようなものです。

f:id:kumamotone:20190809071140p:plain

some Protocol のように型名宣言されているものがOpaque Result Typeで、some View は、意味としては 「具体的な型は公開しないけど、Viewプロトコルに適合する何らかの型」であることを表します。

これだけだとわかりにくいですが、たとえば、

f:id:kumamotone:20190809071156p:plain

この場合の some Viewの中身の返り値は、Function Builder によってつくられた、VStack<TupleView<(Text,Text)> 型でした。これが実際の型です。

しかし外側から見たときは、具体的な型は公開されておらず、Viewプロトコルに適合する何らかの型として振る舞うことができます。

f:id:kumamotone:20190809071217p:plain

もちろん、some View ではなく、具体的な型を指定してもOKです。

f:id:kumamotone:20190809071249p:plain

ちなみにもはやこの形なら、returnも省略されていないですし、function builderも使っていないので、Swift5.1未満の文法でも意味が通りますね。

f:id:kumamotone:20190809071304p:plain

これで文法上の意味を把握するという目的は達成できたのですが、じゃあなんでOpaque Result Typeがあると嬉しいかというのを最後にお話します。

まず第一にすっきりするからです。

f:id:kumamotone:20190809073025p:plain

スタックが一つのうちはいいですが、どんどんネストしていくので、

普通に書くとやってられないほど長くなっていきます。うれしいのはそれだけではありません。

f:id:kumamotone:20190809073043p:plain

コンパイル時に何らかの型であることが確定するため、実行時にパフォーマンスロスがないというのがOpaque Result Typeのポイントになります。 

f:id:kumamotone:20190809073102p:plain

先ほどもお話したように、Opaque Result Type ジェネリクスのようなものとみなすことができます。内側から決まるジェネリクスということもできると思います。

たとえばmap関数は関数を記述するときには特定の型を指定していませんが、呼び出し側がIntIntを渡しているので、その関数の引数と返り値がIntとIntであることが、コンパイル時に確定します。

同じように、some Viewも、関数を記述するときにはsome Viewと、特定の型を指定していませんが、コンパイル時には、具体的な型を確定させることができます。

f:id:kumamotone:20190809073116p:plain

コンパイル時に具体的な型が確定しているのは、従来のプロトコルを指定する形とはお大きく違うところで、たとえば、bodyの型をViewと指定すると、コンパイルエラーになります。

Viewプロトコルで、さらにassociatedtypeを持っているからです。

f:id:kumamotone:20190809073132p:plain

これを解決するために、型消去などの方法を使うこともできます。

型消去は、たとえばViewプロトコルに準拠した型を用意して、それをラッパーとして扱うことで、そのプロトコルに準拠した型を格納する場所を作る方法です。

型消去によって、見かけ上some Viewと同じことはできますが、呼び出し時にオーバーヘッドが生じてしまうため、パフォーマンス的に良くないという問題がありました。

Opaque Result Type であれば、コンパイル時には何かしらの具体型に落ちているため、実行パフォーマンスを保ち、実際の型を隠蔽しながら、すっきりとした記述ができるようになります。

f:id:kumamotone:20190809073142p:plain

Opaque Result Type の説明は以上です。というわけで、すべての説明が終わりました。

f:id:kumamotone:20190809073157p:plain

話をまとめながらおさらいしてみましょう。

f:id:kumamotone:20190809073211p:plain

まずこのコードではreturnが省略されていて、

f:id:kumamotone:20190809073222p:plain

VStackの引数はFunction Builder によって、ViewBuilder.buildBlock(Text(),text()) と解釈されて、

実際には、VStack<TupleView<(Text, Tet)>>>型のオブジェクトを返しているのでした。

f:id:kumamotone:20190809073234p:plain

そして、some ViewViewに適合するなんらかの型であるということを表していて、

これは関数の内側からコンパイル時に具体型が確定するので、パフォーマンスロスもなく、実際の型を隠蔽することができるのでした。

f:id:kumamotone:20190809073244p:plain

いかがでしたでしょうか。

f:id:kumamotone:20190809073417p:plain
今日はSwiftUIbodyプロパティを読むのに必要な、3つのSwiftUIの機能を紹介しました。これらの機能によって、SwiftUIのシンプルな記述が可能になっています。

実際結構ややこしいトピックなので、なかなか馴染みのない場合は理解が時間がかかるかもしれませんが、この発表がとっかかりになれば幸いです。

f:id:kumamotone:20190809073259p:plain

参考文献のSwift Evolutionのプロポーザルはこちらになります。

f:id:kumamotone:20190809073346p:plain

参考にさせていただいたネットの記事などはこちらになります。

SpeakerDeckのほうからもリンクを貼ってあるのでご参照ください。

f:id:kumamotone:20190809073335p:plain

自分からの発表は以上になります。ご清聴ありがとうございました。