他プログラミング言語を普段使う人のための日本語版SwiftUIチュートリアル
はじめに
この記事は,普段PythonやC++,Javaなどを普段使っているSwiftとは縁もゆかりもない方達の中で「Swiftでアプリ作ってみたいけどいまいち乗り気にならん」という人にむけた記事です.
しかし,私もその1人でずっとSwiftを後回しにし続けここまできました.(筆者はC++やPythonをよく使う人)
そこで,なんとか学びきるために「記事を書きながら学ぼう」と思い立ち記事を書いている次第です.
したがって,これは単なるチュートリアルの翻訳ではなく,あくまで他言語をすでにある程度習得している中級者向けのSwiftUI (iOSアプリ開発) チュートリアルであることをご了承ください.
ちなみにこの記事ではSwiftの構文単体の解説はしませんが,SwiftとSwiftUI両方の解説を盛り込みます.
公式チュートリアルを進めながら,「ここはこういう意味だ」というツッコミを細かく入れていく,それだけの記事です.
別に,この記事と一緒にチュートリアルを進めなくても良いです,流し見するだけでも多分面白いです.笑
SwiftとSwiftUI
SwiftはAppleが開発した言語で,過去に使われていたObjective-Cの後継となる言語を指します.
一方でSwiftUIは,Swiftでアプリ開発を容易にするためのインターフェース (フレームワーク) です.
コンセプトとしては,「よりコードとデザインの調和性をあげる」というものらしいです.
「THE オブジェクト指向的なアイデア」って感じです.
ちなみに今まで使われていたStoryBoardについては私は触ったことがないのでここでは割愛します.
公式チュートリアル
先ほど言ったように,本記事では公式チュートリアルに倣って進めます.
といっても,チュートリアルはいくつかあるので,全ては紹介しきれません.
そこで,本記事ではもっともよく使われているHandling User Inputをもとに進めていきます.
随所,他のチュートリアルやドキュメントからも情報を補足していきます.
Handling User Input
このチュートリアルでは,位置情報や写真などの情報を含むランドマークリストを管理するアプリを作成します.
機能としては,お気に入りに登録したり,簡単なフィルタ機能を持たせたものです.
これについては,一度公式ページを流し見してみると良いでしょう.
他言語学者のためのSwiftUI解説
準備
さて早速始めましょう.
まずはコードを落としてください.
やはり,新しい言語を学ぶ時はすでにあるコードを読み解く方が早いです.
そうしたらXcodeを立ち上げて,Open anotor projectから「HandlingUserInput > StartingPoint > Landmarks > HandlingUserInput.xcodeproj」を開きます.
一度実行してみる
そうしたら取り急ぎ実行してみて,今どのような状態かを確認します.
ここまで,とりあえず出来上がっています.
このあと,お気に入り機能やフィルタ機能を追加していくのですが,その前にコードを少しだけ読み解いていきます.
Models > Landmark.swiftを読む
まずはモデルから見ていきましょう.
ちなみにモデルとは,ざっくりとデータベースのテーブル情報をクラスや構造体で示したものです.
さあ,早速実際のモデル定義とコードを見てみましょう.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | // landmark.swift struct Landmark: Hashable, Codable, Identifiable { var id: Int var name: String fileprivate var imageName: String fileprivate var coordinates: Coordinates var state: String var park: String var category: Category var isFavorite: Bool var locationCoordinate: CLLocationCoordinate2D { CLLocationCoordinate2D( latitude: coordinates.latitude, longitude: coordinates.longitude) } enum Category: String, CaseIterable, Codable, Hashable { case featured = "Featured" case lakes = "Lakes" case rivers = "Rivers" case mountains = "Mountains" } } extension Landmark { var image: Image { ImageStore.shared.image(name: imageName) } } struct Coordinates: Hashable, Codable { var latitude: Double var longitude: Double } |
比較してみると,コードの書き方はちょっとPythonライクな構文ですね.
ひとつひとつ見ていきましょう.
1 | struct Landmark: Hashable, Codable, Identifiable {} |
ここではモデルを構造体として定義しています.
特に継承したりしないので,構造体という形を取っていますが,後ろに Hashable などの謎の宣言があります.
いずれもプロトコル ( protcol ) と呼ばれるもの (というか一種の型) で,PHPなどではインターフェースと呼ばれているものと使い方は同じであるようです.
Swiftではクラスベースというよりプロトコルベースの開発が基本なようです.
ここでそれぞれのプロトコルについてまとめてみます.
- Hashable
ハッシュ値を生成してくれるプロトコル. - Codable
JSONを任意のデータ型に変換してくれるプロトコル.今回の場合,「Resources > landmarkData.json」から変換される. - Identifiable
データを識別可能にするためのプロトコル.必ずその構造体やクラスにはidが必要らしい.
ここまででもSwift初見では,なかなか情報過多です.
次に,変数定義について.
Swiftでは,PythonのType Hints (型アノテーション) のように後置で型を宣言します.
無くても型推論をしてくれるそうですが,Swiftでは型の扱いはPyhonより厳しいようなので,基本的につけた方が良いでしょう.
fileprivate はアクセス修飾子の一種で,同ファイル内のみアクセス可能な修飾子です.
もちろん, public や private もありますが,別ファイルから呼び出したり,継承,オーバーライドする場合は open をつけます.
すでにかなり長くなってきていますが,最後にこの部分.
1 2 3 4 5 | extension Landmark { var image: Image { ImageStore.shared.image(name: imageName) } } |
extention は構造体やクラスに定義を追加できます.
なぜ,最初から含めないのか?というツッコミはありますが,まあ使い方は基本的には前述の通りです.
このコードでは, image という変数を新たに定義していますが,先ほどの変数定義とは何やら様子が異なります.
どうやらSwiftにはプロパティ (メンバ変数) の定義の仕方が大きく2つあるようです.
- ストアドプロパティ (Stored Propaty)
var num: Int = 10 のように値を保持するプロパティを指す.いわば普通のプロパティ. - コンピューテッドプロパティ (Computed Propaty)123456789101112131415161718var contry: String {get {if state == "New York" {return "America"}else {return "Canada"}}set {if newValue == "America" {state = "New York"}else {state = "Ottawa"}}}
のように,一種のクロージャのような,決まった値を保持しないプロパティ. get {} は必須で set {} は任意. get {} のみであれば省略可能で, return も省略化 (?).セッターは set(hoge) {} のように引数も与えることができる.
所感としては簡易クロージャって感じがするけど,クロージャはストアドプロパティに属し,スコープ ( { } ) 内は一度しか呼ばれない.
したがって,この image はコンピューテッドプロパティと呼ばれる,定義の仕方を取っています.
ここで,ストアドプロパティとして
1 | var image: Image = ImageStore.shared.image(name: imageName) |
のように定義してしまうと, imageName が初期化されていないので怒られます.
このように,別のプロパティを引数とする関数の戻り値を使って,プロパティを定義する場合は,コンピューテッドプロパティを使うようです (ややこしい).
これで大体解説できたかな.
長いですね,次いきましょう.
LandmarkRow.swift
UIを構成するコードの中で最小単位のものから見ていきたいので,次は各データを行方向にどのように表示するかを管理しているLandmarkRow.swiftを見ていきます.
まずはこの部分.
1 2 3 4 5 6 7 8 9 10 11 12 13 | struct LandmarkRow: View { var landmark: Landmark var body: some View { HStack { landmark.image .resizable() .frame(width: 50, height: 50) Text(landmark.name) Spacer() } } } |
コードを見ると「ああ,ビューに関するコードで画像やらなんやら表示させてるんだな」となんとなく分かりますね.
さて, View プロパティはSwiftUIで定義されているその名の通りビューに関するプロトコルです.
プロパティは2つありますが,大事なのは body です.
ここで,型に some というキーワードがついています.
これはOpaque Type (不透明型) と呼ばれているもので,同じ型のインスタンスでも some を付与すれば等価ではなく別の抽象的な型として扱われるようになります.
まったく違うビューでも同じ型 ( View )として扱わず,別のビューとして扱われるようになります.
そして, body はコンピューテッドプロパティとして定義されていますが, HStack {} はHorizontal Stack,つまり水平方向にスタックするときに使われる構造体です.
また,その中に積まれているものを読み解くとこんな感じです.
Spacer() は動的にスペースを確保してくれる便利なツールです.
Spacer().frame(width: 20) のように固定長の余白も挿入可能です.
お気に入りのマークを追加してみる
さて,ここでやっとチュートリアルの内容に入りますが,この HStack {} 内にお気に入りの星マークを表示してみましょう.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | struct LandmarkRow: View { var landmark: Landmark var body: some View { HStack { landmark.image .resizable() .frame(width: 50, height: 50) Text(landmark.name) Spacer() if landmark.isFavorite { Image(systemName: "star.fill") .imageScale(.medium) .foregroundColor(.yellow) } } |
ここでお気に入りになっているものには,右寄せで黄色の星マークが表示されました.
Spacer() が動的に大きさを変えていることも確認できますね.
またここで, Image(systemName: "star.fill") とありますが,Swiftでは予め便利なアイコンが用意されており, Image(systemName: "xxx") という形で利用できます.
使えるものはSF Symbolsというアプリで確認できます.
めちゃくちゃ種類があります.
フロントエンジニアさんであれば,アプリ版FontAwesomeと言えば分かりやすいですかね.
次にこの部分.
1 2 3 4 5 6 7 8 9 | struct LandmarkRow_Previews: PreviewProvider { static var previews: some View { Group { LandmarkRow(landmark: landmarkData[0]) LandmarkRow(landmark: landmarkData[1]) } .previewLayout(.fixed(width: 300, height: 70)) } } |
PreviewProvider は,ただのプレビュー表示をしてくれる,いわばテストプロトコル.
実機アプリには関与しない部分.
LandmarkDetail.swift
次は,リスト (列方向) に関するコードを見る前に,詳細ページについて見ていきます.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | struct LandmarkDetail: View { var landmark: Landmark var body: some View { VStack { MapView(coordinate: landmark.locationCoordinate) .edgesIgnoringSafeArea(.top) .frame(height: 300) CircleImage(image: landmark.image) .offset(x: 0, y: -130) .padding(.bottom, -130) VStack(alignment: .leading) { Text(landmark.name) .font(.title) HStack(alignment: .top) { Text(landmark.park) .font(.subheadline) Spacer() Text(landmark.state) .font(.subheadline) } } .padding() Spacer() } .navigationBarTitle(Text(landmark.name), displayMode: .inline) } } |
先のコードとよく似ているが,こちらは VStack {} で垂直方向に積み上げるタイプ.
ただ,その中に公園名と州の名前が HStack {} で定義されているように,ネストが可能です.
これについては,Section 6 Compose the Detail Viewを見た方が早いかもしれません.
LandmarkList.swift
次は,Rowデータのスタックについて.
1 2 3 4 5 6 7 8 9 10 11 12 13 | struct LandmarkList: View { var body: some View { NavigationView { List(landmarkData) { landmark in NavigationLink(destination: LandmarkDetail(landmark: landmark)) { LandmarkRow(landmark: landmark) } } .navigationBarTitle(Text("Landmarks")) } } } |
これも,先ほどの行方向のコードと大枠は変わりありません.
ただ,スタックするリストの書き方が少し特殊で, landmarkData をリスト化して,各データを landmark という名前の一時変数でその中身の処理を記述しています.
NavigationLink(destination: xxx) { XXX } はHTMLで言う <a href="xxx">XXX</a> と一緒で,そのリンク範囲をカッコ内に記述します.
今回はそれぞれ,先ほど定義したビューが入っていますね.
そして,詳細ビューでもあったように .navigationBarTitle() はそのビューのタイトルを指します.
これは,画面遷移の際に,前ページに戻る際のテキストに使われたりします.
フィルタリング
さて,ここでまたチュートリアルの内容に復帰します.
ここでは,お気に入りのものだけを抽出して表示するフィルタを追加してみましょう.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | struct LandmarkList: View { @State var showFavoritesOnly = true var body: some View { NavigationView { List{ ForEach(landmarkData){ landmark in if !self.showFavoritesOnly || landmark.isFavorite { NavigationLink(destination: LandmarkDetail(landmark: landmark)) { LandmarkRow(landmark: landmark) } } } } .navigationBarTitle(Text("Landmarks")) } } } |
プレビューを表示させてみるとこんな感じになります.
とりあえず,お気に入りだけフィルタリングはできました.
ここで,いくつか初見だと気になるポイントを整理しておきます.
まずは @State ,これを付与したプロパティは変更されるたびにビューの再描画が行われるようになる (らしい).
これにより画面遷移の実装がかなり楽になったとのこと.
次に, List {} と ForEach {} .
最初みた時, List(landmarkData) {} だけでよくない?と思ったが,これだと全てのデータがスタックされてしまい,お気に入りじゃないデータも空欄として描画されてしまう.
したがって,リスト化するデータを ForEach {} でまずは捌くといった形を取っている.
謎がある程度解けたところで,フィルタ機能を完成させましょう.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | struct LandmarkList: View { @State var showFavoritesOnly = true var body: some View { NavigationView { List{ Toggle(isOn: $showFavoritesOnly) { Text("Favorites only") } ForEach(landmarkData){ landmark in if !self.showFavoritesOnly || landmark.isFavorite { NavigationLink(destination: LandmarkDetail(landmark: landmark)) { LandmarkRow(landmark: landmark) } } } } .navigationBarTitle(Text("Landmarks")) } } } |
トグルを追記します.
そうすると,以下のようにお気に入り表示の切り替え機能が実装できました.
やはりSwiftUIは,かなり使いやすさに重視しています.
Toggle(isOn: $showFavoritesOnly) はコードだけで何をしているのかがすぐに分かりますね.
$ 接頭辞は, @State 変数 (もっと細かくいえば State<Bool> ) を便利に扱うために使うものらしく,詳細な定義は分かりませんが,どうやら @State が自動生成した,投影プロパティ (projected propaty)と呼ばれるものらしいです.
ここまでで,チュートリアルSection 3まで終わりました.
あとは,ユーザごとにお気に入りを変更できるようにすれば,このアプリはひとまず完成になります.
監視可能オブジェクト
さて,今まで扱ってきた landmarkData や,お気に入り表示のための showFavoritesOnly といった変数は,それぞれバラバラで扱ってきました.
これから各 landmarkDataに対して,お気に入りにしたり,解除したり,といった機能を追加していくのですが,何かしらのユーザアクションに対してビューの再描画を完璧に行う必要があります.
showFavoritesOnly は @State で「変更されたら再描画」が行われていましたが,そういった変数が複数ある場合,SwiftUIでは監視可能オブジェクト (Observable Object) というものでまとめてしまうことができます.
監視可能オブジェクトとは,先ほど言ったように,ビューの描画に影響する可能性があるオブジェクトを監視対象に置くことで,変更後に正しくビューの再描画を行うためのものです.
今回の場合,先の landmarkData と showFavoritesOnly がそれにあたります.
それらを UserData という名前としてまとめ,監視対象としていきます.
まずは,Modelsディレクトリ内に UserData.swift を作成してください.
そして大枠を書きます.
1 2 3 4 5 6 7 | import SwiftUI import Combine final class UserData: ObservableObject { } |
ObservableObject プロトコルを使用するために, import Combine が必要です.
なぜ,今まで構造体を使ってきたのに, final class なのかというと,継承されることがないから,でしょうか (真意は分かりません).
そしてここに,監視対象とする変数を定義していきます.
1 2 3 4 | final class UserData: ObservableObject { @Published var showFavoritesOnly = false @Published var landmarks = landmarkData } |
ここでも,新しく @Published という宣言がでてきました.
これは,データの変更を公開する変数に付与するものです.
そしたら,リストビューを修正します.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | struct LandmarkList: View { @EnvironmentObject var userData: UserData var body: some View { NavigationView { List{ Toggle(isOn: $userData.showFavoritesOnly) { Text("Favorites only") } ForEach(userData.landmarks){ landmark in if !self.userData.showFavoritesOnly || landmark.isFavorite { NavigationLink(destination: LandmarkDetail(landmark: landmark)) { LandmarkRow(landmark: landmark) } } } } .navigationBarTitle(Text("Landmarks")) } } } struct LandmarkList_Previews: PreviewProvider { static var previews: some View { ForEach(["iPhone SE", "iPhone XS Max"], id: \.self) { deviceName in LandmarkList() .previewDevice(PreviewDevice(rawValue: deviceName)) .previewDisplayName(deviceName) .environmentObject(UserData()) } } } |
ObservableObject は, @EnvironmentObject 属性のプロパティとして定義します.
その後,関係する箇所を書き直していきますが, @State 変数と同じように, $ 接頭辞をつけて扱うことができます.
なお,この時点ではまだ以下のようなエラーが出ますので,焦らず読み進めてください.
1 | Thread 1: Fatal error: No ObservableObject of type UserData found. A View.environmentObject(_:) for UserData may be missing as an ancestor of this view. |
この時点で気になる箇所はいくつかありますが,ひとまず動作するポイントまで進めます.
次に修正するファイルは SceneDelegate.swift です.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). // Use a UIHostingController as window root view controller if let windowScene = scene as? UIWindowScene { let window = UIWindow(windowScene: windowScene) window.rootViewController = UIHostingController( rootView: LandmarkList() .environmentObject(UserData()) ) // window.rootViewController = UIHostingController(rootView: LandmarkList()) self.window = window window.makeKeyAndVisible() } } |
ここでは, environmentObject(_:) 修飾子 (原文はmodifierと書いてあったが日本語ではなんと呼ぶのが適切なんだろうか) を LandmarkList() に追加しています.
何をしているかと言うと, ObservableObject は呼び出しの際,初期化することもなくコンストラクタもないため,普通に使うとランタイムエラーとなる.
が,環境オブジェクトとしてmodifierとして仮の初期値を与え,特別扱いできる (?らしい.ここは正直よくわからない).
ここまでで,以下のように適切に動作するようになります.
ここまででの疑問
ここで,一旦区切ります.
私が現時点で疑問に思っていることは, self の有無です.
例えば以下のコード.
1 2 3 4 5 6 7 | ForEach(userData.landmarks){ landmark in if !self.userData.showFavoritesOnly || landmark.isFavorite { NavigationLink(destination: LandmarkDetail(landmark: landmark)) { LandmarkRow(landmark: landmark) } } } |
ForEach() の中では self ついていないけれど, if 分の条件式ではついていますね.
ちなみに,前者は self. があっても動作しますが,後者は無いとエラーになります.
なぜ,こうなるか調べてみると,「クロージャ内でのプロパティ参照は明示的に self. で参照する必要がある」とのこと.
個人的には,全て明示的に self. をつけたくなる人だけど,なんか中途半端だね.
詳細ビューとの同期
最後に,詳細ビューでお気に入りの追加・解除機能をつけてチュートリアルは終わりです.
先ほど作成した ObservableObject と, EnvironmentObject(_: ) のおかげで,ビューを跨いだデータの同期と描画が簡単に行えます.
以下のように修正.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | struct LandmarkDetail: View { @EnvironmentObject var userData: UserData var landmark: Landmark var landmarkIndex: Int { userData.landmarks.firstIndex(where: { $0.id == landmark.id })! } var body: some View { VStack { MapView(coordinate: landmark.locationCoordinate) .edgesIgnoringSafeArea(.top) .frame(height: 300) CircleImage(image: landmark.image) .offset(x: 0, y: -130) .padding(.bottom, -130) VStack(alignment: .leading) { Text(landmark.name) .font(.title) HStack(alignment: .top) { Text(landmark.park) .font(.subheadline) Spacer() Text(landmark.state) .font(.subheadline) } } .padding() Spacer() } .navigationBarTitle(Text(landmark.name), displayMode: .inline) } } struct LandmarkDetail_Previews: PreviewProvider { static var previews: some View { LandmarkDetail(landmark: landmarkData[0]) .environmentObject(UserData()) } } |
基本,環境オブジェクトの定義に関するものですが,一部初めて見るようなコードがありますね.
1 2 3 | var landmarkIndex: Int { userData.landmarks.firstIndex(where: { $0.id == landmark.id })! } |
これです.
これは,お気に入りの追加・解除において,その切り替えを行う際に使う,今参照しているランドマーク配列のインデックスを取得しています.
まず, firstIndex() は配列に対して,指定した条件に合致した最初のデータのインデックスを返す関数です.
そしてその中身 (条件式) ですが, (where: { $0.id == landmark.id }) となっていますね.
完全に初見殺しなコードだと思います.
まず, where という引数に何かを渡していますが,この「何か」が初見だと分かりづらいです.
(というか,ここで関数やクロージャ,構造体,クラスへの引数の指定の仕方を知る)
正体はクロージャで, $0 は第一引数を表しています.
詳しい定義は分かりませんが,おそらく firstIndex()では配列の中身を一つずつ取り出し,それをそのまま引数として受け取ったクロージャに渡しているのだと思います.
というかそもそも,なんで最初から参照型のクラスを使わないんだろう,という疑問が残ります.
まあ,値型の構造体で Landmark を定義しちゃったんだから仕方ないね.
さて,詳細ビューにお気に入りボタンを追加してみましょう.
お気に入りボタンは,ランドマーク名の右隣に置きたいので,まずはランドマーク名を VStack {} に入れて,さらにそこにボタンを水平方向に並べます.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | struct LandmarkDetail: View { @EnvironmentObject var userData: UserData var landmark: Landmark var landmarkIndex: Int { userData.landmarks.firstIndex(where: { $0.id == landmark.id })! } var body: some View { VStack { MapView(coordinate: landmark.locationCoordinate) .edgesIgnoringSafeArea(.top) .frame(height: 300) CircleImage(image: landmark.image) .offset(x: 0, y: -130) .padding(.bottom, -130) VStack(alignment: .leading) { HStack { Text(landmark.name) .font(.title) Button(action: { self.userData.landmarks[self.landmarkIndex].isFavorite.toggle() }) { if self.userData.landmarks[self.landmarkIndex].isFavorite { Image(systemName: "star.fill") .foregroundColor(Color.yellow) } else { Image(systemName: "star") .foregroundColor(Color.gray) } } } HStack(alignment: .top) { Text(landmark.park) .font(.subheadline) Spacer() Text(landmark.state) .font(.subheadline) } } .padding() Spacer() } .navigationBarTitle(Text(landmark.name), displayMode: .inline) } } |
これで本アプリは完成です.
お疲れ様でした.
最後に,全体を通して気になったことをまとめて終わります.
おわりに:Swiftに対する所感
各所突っ込みながら進めると,チュートリアルと言えど長いですね.
ここまでお付き合いしてくれているみなさん,本当にありがとうございます. (多分最後まで読んでくれている人あまりいないんじゃないかな...)
最後に,Swiftに対する所感と,ちょっとしたまとめを書きます.
まず所感ですが,Swift (というかSwiftUI) は多機能すぎて,細かくひとつひとつ追うにはかなり時間がかかるためある程度妥協が必要だな,と感じました.
まあ,私自身この記事を書きながら初めてSwift自体にも触っているわけですし,もしSwift自体の知識がもう少しあれば,もっと良い感じに説明できたかもしれません...無念.
そしてその他にも,コードを読んだ感想としては,構造体と関数とクロージャの呼び出し方が分かりづらいな,とも感じました.
というより, { } の意味を見失いやすい,と言った感じでしょうか.
誰かこの気持ちわかってくれないかなあ...
特に, VStack {}, HStack {} 系.
定義としては,構造体なんだけれども,なんで { } を使った呼び出しができるのかがまだ理解できていない.
特別な構造体なんだろうか...
それはまたいつか解決したい.