SwiftUIの*Stack{ }やList{ }の謎を解く記事【波括弧は何?】
はじめに
最近,ふと思い立ちSwiftの勉強を始めました.
そこでSwiftの多機能性に逆に悩まされています.
今回の題材はSwiftUIでおなじみの VStack { } 系統.
この波括弧は何!?という私の疑問を解決する記事です.
もうすでに答えをわかっている人には「そんなこともわからんのかよ...」と呆れられそうですが,頑張ります.
問題のコード
以下のコードを見てください.
1 2 3 4 5 6 7 | var body: some View { HStack { Text(landmark.park) Spacer() Text(landmark.state) } } |
SwiftUIのチュートリアルをやれば似たようなコードを必ず見るはずです.
別にやっていることは分かります.
「水平方向にテキストとスペースを並べているんだな」というのは分かるんです.
そして, body がComputed propatyとして定義されていることも分かります.
でも,この HStack って何者なんでしょう??
関数ではなさそうです.
実際に定義を辿ってみました.
1 2 3 | @frozen public struct HStack<Content> : View where Content : View { // 省略 } |
どうやら構造体らしい.
でも構造体って初期化するならば, HStack( ) みたいな感じかと思っていたが, HStack { } という使い方をしている,なぜだ...
ヒントはクロージャ?
この HStack,イニシャライザの引数を見てみると,クロージャを含んでいる.
1 2 3 | @inlinable public init(alignment: VerticalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content) |
これがヒントなのかもしれないと思い,簡単な構造体をつくってみた.
自作のクロージャを引数にとる構造体
1 2 3 4 5 6 7 8 9 10 11 12 13 | // 適当な大元となるプロトコル protocol base{ var a: String { get set } } // イニシャライザに簡単なクロージャをとる構造体 struct hoge: base{ var a: String init(a: () -> String) { self.a = a() } } |
何の意味もないコードだが動作を確認するだけなので問題ない.
まずは初期化して実行
クロージャも超簡単なものを渡す.
1 2 3 4 5 | var x: base { hoge(a: {"Hello."}) } print(x.a) // => Hello. |
問題ないし,特に疑問もない.
HStack { }のようにしてみる
1 2 3 4 5 6 7 | var x: base { hoge{ "Hello." } } print(x.a) // => Hello. |
おお!
これだ,同じような表記を実現することができた.
つまり, HStack { } の波括弧は引数に渡すクロージャを表しているらしい.
ここまでで,基本はわかった気がする.
しかし疑問がまだある.
クロージャ内に適当に積み上げられたオブジェクト
1 2 3 4 5 6 7 | var body: some View { HStack { Text(landmark.park) Spacer() Text(landmark.state) } } |
これ,一つの波括弧 { } 内にカンマ , もなく,オブジェクトが積み上げられてるけどどういうことなの?
見様見真似でやってみても...
1 2 3 4 5 6 7 8 | var x: base { hoge{ "Hello, " "Swift." // => Error } } print(x.a) |
もちろんダメ.
だってクロージャ内で複数文書くときは, return が必要なのだから.
またも定義を辿る.
すると,怪しげなものが.
@ViewBuilder
イニシャライザの @ViewBuilder content: () -> Content の部分.
この属性 (attribute) が関係しているに違いない,と調べてみると.
ピンポイントで求めてた記事を見つけました.
どうやら,この @ViewBuilder が付いたクロージャは以下のように解釈されるらしい.
1 2 3 4 5 6 7 | var body: some View { HStack { Text(landmark.park) Spacer() Text(landmark.state) } } |
が,こう????
1 2 3 4 5 6 7 8 9 | var body: some View { HStack { ViewBuilder.buildBlock( Text(landmark.park), Spacer(), Text(landmark.state) ) } } |
だから,実質クロージャ内は一つのオブジェクトしかないのでOKということ.
何だそりゃ.
便利なんだろうけど,細かく仕様を知ろうとする人にとっては,やや気持ち悪い.
C++でいうところの,マクロ使いまくって構文が崩壊仕掛けている感じ.(?)
作ってみたい
だけど,仕組みがわかった以上,自分で同じような実装がしてみたい.
そして,先の記事では親切にも,その作り方まで書いてくれている.(神様!)
CustomAttribute
早速作ってみました.
1 2 3 4 5 6 7 8 9 | // CutomAttributeを作る! @_functionBuilder public struct ConcatStr{ public static func buildBlock(_ s1: String) -> String{ return s1 } public static func buildBlock(_ s1: String, _ s2: String) -> String{ return s1 + s2 } } |
意外と脳筋な実装になってしまうのですが,最大2つまで文字列を積み上げられるようにしました.
ちなみにメソッド名は buildBlock() と言う名前でなければダメっぽいです.
あとは引数のラベルは省略したいので, _ がついています.
早速実行してみる!
まずは,構造体のイニシャライザに作成したattributeを加筆します.
1 2 3 4 5 6 7 8 | // イニシャライザに簡単なクロージャをとる構造体 struct hoge: base{ var a: String init(@ConcatStr a: () -> String) { self.a = a() } } |
そしたら,待ちに待った実行!
1 2 3 4 5 6 7 8 | var x: base { hoge{ "Hello, " "Swift." } } print(x.a) // => Hello, Swift |
やったー!嬉しいー!
これで,本題の謎は解けた気がします.
この,不思議構文はなかなか手強かったですね...
良い勉強になりました.
余談:@ViewBuilderの定義 (一部)
1 2 3 4 5 6 7 8 9 | extension ViewBuilder { public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9> (_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View, C7 : View, C8 : View, C9 : View } |
本家も脳筋な実装してました.
最大9つスタックできるらしいです.
おわりに
いやー,謎が解けると気持ちが良いです.
私,こういう便利なフレームワークの定義を読み解くのが結構好きで,Pythonのフレームワークとかはよく読んでいたのですが,SwiftUIは手強かったですね.
勉強し初めて数日というのもあるし,結構細かい定義の話までしている記事って少ないんですよね.
みんな,「SwiftUIの使い方に特化している」というか.
あまり細かい定義のは話は気にならないのかな,理解できたらコーディングしやすくなると思うんだけどな.
みなさんはどう思います?