Swift・SwiftUIのソースコードを読み解くのに難儀した話
macOS/iOSアプリを開発したくてSwiftを学び始めたのですが、SwiftUIの最初のサンプルコードを読み解くのに難儀して心が折れそうになった話です。C / C++ / C# / Objective-C / Java / JavaScript / Python 等々のプログラミング経験があるのに、Swiftの言語仕様を読み進めてもSwiftUIで書かれたサンプルコードを読み解けなかったので、どうやって学んだら良いのか悩みました。
Swiftの言語仕様は、これまで使ってきたプログラミング言語には無い特徴が多く、習得に時間がかかる印象を持ちました。おそらく、App Storeで配布されているSwift Playgroundsを使って、SwiftUIの使い方とSwiftの構文を必要なところから学習するほうが効率的ではないかと思います。
本文章を作成している時点での各ツールのバージョンは以下のとおりです。
- Swift 6.2.3
- Xcode 26.3
最初のサンプルコードで挫折しそうになる
Section titled “最初のサンプルコードで挫折しそうになる”以下のサンプルコードはSwiftUIのトップに掲載されています。
import SwiftUI
struct AlbumDetail: View { var album: Album
var body: some View { List(album.songs) { song in HStack { Image(album.cover) VStack(alignment: .leading) { Text(song.title) Text(song.artist.name) .foregroundStyle(.secondary) } } } }}以下は初めて見たときの感想です。末尾の🔴は(概ね)正解、❌️は不正解または分からなかった部分です。コンストラクタやコンポーネントなどの用語はSwift/SwiftUIでは不適切ですが、これまで使ってきたプログラミング言語やフレームワークの用語を使って感想を書いています。
- 1行目:
importはSwiftUIモジュールをソースコード内にインポートしている。 🔴 - 3行目:
structでViewから派生した構造体を定義しているようだ。 ❌️ - 4行目:
varで変数宣言しており、Pythonの型アノテーションと同じ書式で変数のデータ型を宣言している。 🔴 - 6行目: 変数宣言の後ろにある
someは何を表している? さらに、{ }で書かれているブロックはどういう構文なのか? ❌️ - 7行目:
Listのコンストラクタを呼び出しているようだが、その後ろの{ }で書かれているブロックはどういう構文なのか? さらにinはどういう演算子なのか? ❌️ - 8行目:
HStackは子コンポーネントを水平に配置するコンポーネントのようだが、コンストラクタの引数( )が無い上に末尾の{ }はどういう構文なのか? ❌️ - 9行目:
Imageは画像コンポーネントのコンストラクタを呼び出しているようだが、次の文との間にカンマなどの記号が無く、これは一体どういう構文なのか? ❌️ - 10行目:
VStackの引数.leadingとは何なのか? ドット(.)の前に型名や変数名などの識別子が無い。 ❌️ - 11, 12行目:
Textコンポーネントを2つ定義してVStackで垂直に配置するようだが、9行目と同様に構文が分からない。 ❌️ - 13行目: これはメソッドを呼び出して12行目の
Textのインスタンスに対してフォアグラウンドのスタイルを設定している。 🔴
サンプルコードを読み解いていく
Section titled “サンプルコードを読み解いていく”14行目以降の波括弧を除くと13行程度のサンプルコードですが、これを読み解くのにSwift及びSwiftUIならではの構文を学ぶ必要がありました。
インポート宣言
Section titled “インポート宣言”import SwiftUIインポート宣言は、Pythonのimport同様、現在のファイルの外部で宣言されているシンボルにアクセスできます。以下のようにモジュール内のシンボルごとにインポートすることもできます。
import protocol SwiftUI.Viewimport struct SwiftUI.HStackアプリから分離したモジュールは ライブラリ で作ることができます。
試しにライブラリでモジュールを作ってみました。
- Xcodeのメニューから、File -> New -> *Package…*を選んでパネルを開き、Libraryを選択する。
- Save Asパネルでモジュール名を入力し、アプリのプロジェクトを選択して、Createボタンでライブラリを作成する。
- これだけでは
import宣言するとNo such moduleエラーになるので、File -> Add Package Dependencies… でパネルを開き、Add Local… ボタンを押してモジュールのフォルダを選択して、Add Package ボタンで依存関係に追加する。
アプリのプロジェクト内にサブフォルダーを作成してソースコードを追加した場合、importを書かなくても別ファイルのシンボル(エンティティ)にアクセスできます。同名の関数などが複数ファイルに存在する場合は、privateなどのアクセス制御構文を使ってシンボルを公開したり隠蔽したりできます。
変数と定数、格納プロパティ
Section titled “変数と定数、格納プロパティ” var album: AlbumJavaScriptのconstやlet(var)のように、Swiftでもletとvarで定数と変数を宣言します。
サンプルコードのvar albumはAlbumDetail構造体の中で宣言しているので、これは格納プロパティを表しています。格納プロパティは、構造体とクラスのインスタンスの一部として値を保存するプロパティです。
struct Point { var x = 0 var y = 0}
var p = Point()p.x = 100p.y = 200Swiftの変数や定数は型があります。Pythonの型アノテーションのように、定数名や変数名の後ろに:を置いて型注釈を書くことができますし、Swiftの型推論によって型注釈を省くこともできます。
var p1: Point = Point()var p2 = Point()print(type(of: p1)) // Pointprint(type(of: p2)) // Pointサンプルコードではalbumプロパティに初期値が入っていないので、AlbumDetail構造体のイニシャライザで確実に初期値をセットする必要があります。
なお、letで構造体型の定数を宣言すると、構造体の中のvarで宣言されている格納プロパティは変更不可能になります。(MotorCycle構造体)
let moto = MotorCycle()print(moto.fuelTankCapacity) // OKmoto.fuelLevel = 10 // エラー: varプロパティは変更不可能moto.fillUp() // エラー: mutating修飾子の付いたメソッドも呼び出せない計算プロパティ
Section titled “計算プロパティ” var body: some View { List(album.songs) { /* 中略 */ } }Swiftのプロパティには格納プロパティの他に計算プロパティがあります。計算プロパティはプロパティの宣言の後ろに{ }を記述して、値の取得と設定を実装します。
以下は計算プロパティの例です。型注釈の後ろにある{ }の中で取得用のgetと設定用のsetを実装します。getだけ実装した読み取り専用プロパティではgetを省略できます。
struct Square { var sideLength = 0.0 var area: Double { // 計算プロパティの定義 get { // 値を取得する`get` sideLength * sideLength // 単一式は`return`を省略できる } set { // 値を設定する`set` sideLength = newValue.squareRoot() } } var description: String { // 読み取り専用プロパティ(set無し)は`get`を省略できる "辺の長さ=\(sideLength) 面積=\(area)" }}bodyプロパティをgetとreturnの省略なしで記述すると、以下のようになります。
var body: some View { get { return List(album.songs) { /* 中略 */ } } }struct AlbumDetail: View { /* 中略 */}Swiftの構造体はC++の構造体のような継承がありません(C#の構造体に近いイメージ)ので、Viewから派生しているという解釈は誤りです。Swiftでは、継承はクラスで使うことができます。
プロトコルはC#やJavaのインターフェース相当です。プロトコルは機能を実装しません。プロパティ要件やメソッド要件などを定義します。サンプルコードでは、Viewプロトコルに準拠したAlbumDetail構造体を定義しています。
SwiftUIでは、参照型のクラスを使わずに値型の構造体でプロトコルを実装している点が特徴的です。他の言語・フレームワークでは、フレームワークが提供するクラスを継承して実装する方式が多いと思います。
プロトコルと構造体の例
Section titled “プロトコルと構造体の例”プロトコルと構造体の例をPlaygroundで書いてみました。
MotorVehicleプロトコル、MotorCycle構造体、Car構造体の例
構造体や列挙型でプロパティを変更するメソッドはmutatingキーワードが必要です。構造体や列挙型をletで定数宣言すると、mutatingメソッドの呼び出しはエラーになります。
// プロトコルの定義protocol MotorVehicle { var description: String { get } // 説明文 var fuelEfficiency: Double { get } // 燃料効率 (km/ℓ) var fuelTankCapacity: Double { get } // 燃料タンクの容量 var fuelLevel: Double { get set } // 燃料残量 mutating func drive(distance: Double) -> Bool}
// プロトコルを拡張してメソッドを追加するextension MotorVehicle { mutating func fillUp() { fuelLevel = fuelTankCapacity } @discardableResult // 呼び出し元で戻り値を使用しない場合もある関数に与える属性(コンパイラ警告を回避) mutating func drive(distance: Double) -> Bool { let fuel = distance / fuelEfficiency if (fuel > fuelLevel) { print("燃料が\(fuel - fuelLevel)ℓ足りません。") return false } else { fuelLevel -= fuel print("燃料の残量は\(fuelLevel)ℓです。") return true } }}
// オートバイ構造体の定義struct MotorCycle: MotorVehicle { var description = "オートバイX" var fuelEfficiency = 40.0 var fuelTankCapacity = 20.0 var fuelLevel = 0.0}
// 自動車構造体も同様に定義できるstruct Car: MotorVehicle { var description = "自動車Y" var fuelEfficiency = 15.0 var fuelTankCapacity = 45.0 var fuelLevel = 0.0
// 以下はCar固有の格納プロパティ var doorIsOpen = false}
// テストvar moto: MotorCycle = MotorCycle() // `let`で定義すると、以降のmutatingメソッドの呼び出しはコンパイルエラーになるmoto.drive(distance: 100)moto.fillUp()moto.drive(distance: 100)Opaque型とBoxプロトコル型
Section titled “Opaque型とBoxプロトコル型” var body: some View { /* 中略 */ }SwiftのプロトコルはC#やJavaのインターフェース相当の概念ですが、some プロトコルと記述すると、プロトコルに準拠した特定の1つの型に固定されます(Opaque型)。次のgetMotorVehicle関数はCar構造体の型だけ返すことができます。
enum VehicleType { case car case truck// case motorCycle}
func getMotorVehicle(_ vehicleType: VehicleType) -> some MotorVehicle { switch vehicleType { case .car: return Car() case .truck: return Car(description: "トラックZ", fuelEfficiency: 18.0, fuelTankCapacity: 60.0)/* case .motorCycle: // 上でCarを返しているため、MotorCycleは型の不一致エラーになる return MotorCycle() */ }}
// テストvar vehicle = getMotorVehicle(.car) // 型推論により列挙型の型名は省略できるvehicle = getMotorVehicle(.truck)ちなみに、vehicle変数に型注釈を書くとエラーになります。
var vehicle: some MotorVehicle = getMotorVehicle(.car)このように型注釈を書くと、以下のエラーが発生します。変数宣言のsome MotorVehicleと、getMotorVehicle関数の戻り値としてのsome MotorVehicleは別物として扱われるようです。
cannot assign value of type 'some MotorVehicle' (result of 'getMotorVehicle') to type 'some MotorVehicle' (type of 'vehicle')any プロトコルと記述すると、プロトコルに準拠した任意の型を扱えるようになります(Boxプロトコル型)。次のgetMotorVehicle関数はCar型とMotorCycle型を返します。
enum VehicleType { case car case truck case motorCycle}
func getMotorVehicle(_ vehicleType: VehicleType) -> any MotorVehicle { switch vehicleType { case .car: return Car() case .truck: return Car(description: "トラックZ", fuelEfficiency: 18.0, fuelTankCapacity: 60.0) case .motorCycle: return MotorCycle() }}
// テストvar vehicle: any MotorVehicle = getMotorVehicle(.car) // Car型vehicle = getMotorVehicle(.motorCycle) // MotorCycle型anyはプロトコルに準拠した任意の型を扱えるようにするため、boxingの処理が加わり、実行時のパフォーマンスコストが増加します。
それに対して、サンプルコードのvar body: some View { ... }は、Viewプロトコルに準拠した特定の型が決まることによって、anyのような実行時のパフォーマンスコストを増加させないようにしていると解釈すると良さそうです。
[蛇足] someを無くすとどうなるか
Section titled “[蛇足] someを無くすとどうなるか”サンプルコードのvar body: some Viewのsomeを消したりanyに変更したりすると、コンパイラは以下のエラーを発生します。
Type 'AlbumDetail' does not conform to protocol 'View'❗️Unable to infer associated type 'Body' for protocol 'View' (SwiftUI.View.Body)bodyプロパティはViewプロトコルの中で以下のように定義されています。
@ViewBuilder @MainActor @preconcurrencyvar body: Self.Body { get }Body型はViewプロトコルの中で以下のように定義されています。
associatedtype Body : Viewassociatedtypeはプロトコルの関連型を宣言します。BodyはViewプロトコルの中で使用される型のプレースホルダ名で、Viewプロトコルに準拠する構造体がbodyプロパティを実装すると、Bodyの型が特定されます。
つまり、bodyプロパティはViewプロトコルに準拠した特定の型に定まる必要があります。そのため、型の特定できないBoxプロトコル型(any View)ではコンパイルエラーになります。
ちなみに、var body: some Viewの型注釈そのものを消すとコンパイルエラーになります。これは計算プロパティを宣言するときに型注釈は省略できないことを表しています。
Computed property must have an explicit typeイニシャライザ
Section titled “イニシャライザ” List(album.songs) { /* 中略 */ }C++やC#、Javaのコンストラクタに相当するものは、Swiftではイニシャライザになります。イニシャライザは構造体・クラス・列挙型で定義できます。
サンプルコードの7行目はList構造体のイニシャライザを呼び出しています。Pythonと同様にnew演算子が不要です。末尾の{ }は次節で説明する末尾クロージャです。
以下はイニシャライザを定義した構造体の例です。この例では、構造体や引数などの名前に漢字を使っていますが、Unicode文字も含めてほとんどの種類の文字を使うことができます。
struct 摂氏: Equatable { var 摂氏温度: Double init(華氏 温度: Double) { // 引数は`引数ラベル パラメータ名: 型名`で書く 摂氏温度 = (温度 - 32.0) / 1.8 } init(ケルビン 温度: Double) { // 引数の型や数が同じでも引数ラベルが異なるとオーバーロード可能 self.init(温度 - 273.15) // イニシャライザの委譲 } init(_ 摂氏温度: Double) { // 引数ラベルを省略するときは`_`を書く self.摂氏温度 = 摂氏温度 } static func == (lhs: 摂氏, rhs: 摂氏) -> Bool { // `==`演算子の実装 return lhs.摂氏温度 == rhs.摂氏温度 }}
// テストlet 水の沸点 = 摂氏(華氏: 212.0)assert(水の沸点.摂氏温度 == 100.0)let 水の凝固点 = 摂氏(ケルビン: 273.15)assert(水の凝固点 == 摂氏(0.0))initを定義しなければ、構造体ではメンバワイズイニシャライザが自動生成されます。AlbumDetail構造体の場合、以下のようなメンバワイズイニシャライザが生成されます。
struct ContentView: View { var body: some View { AlbumDetail(album: Album()) }}なお、クラスにinitを定義しない場合は、引数の無いデフォルトイニシャライザが自動生成されます。
Swiftのイニシャライザは機能が豊富で、指定イニシャライザ(一般的なクラスのコンストラクタ相当)、convenienceイニシャライザ(同じクラスの別のイニシャライザを呼び出すオーバーロード)、失敗可能イニシャライザ(nilを返して失敗する)、必須イニシャライザ(サブクラスは実装しなければならない)などがあります。失敗可能イニシャライザは構造体や列挙型でも定義できるので、SwiftUIでは使う場面がありそうです。
末尾クロージャ
Section titled “末尾クロージャ” List(album.songs) { song in /* 中略 */ }7行目はList構造体のインスタンスを作成していますが、List(album.songs)の後ろにある{ }は初見では全く理解できなかった構文でした。8行目のHStack、10行目のVStackの後ろにある{ }も同様です。
これはSwiftの言語仕様を読み進めていくうちに末尾クロージャという構文であることが分かりました。SwiftのクロージャはC#のラムダ式やJavaScriptのアロー関数式のような無名関数(匿名関数)に相当しますが、その書式は{ (引数) -> 戻り値の型名 in 処理の本文 }になります。関数の引数としてクロージャを渡す場合は、型推論により(引数) -> 戻り値の型名の部分が簡略化されて{ 引数 in 処理の本文 }になります。さらに、引数が無い場合や省略引数名($0, $1, $2など)を使う場合は{ 処理の本文 }だけになります。
例えば、配列の各要素をn倍にするmultiply関数は、JavaScriptでは以下のように書けます。
function multiply(array, n) { return array.map(x => n * x);}console.log(multiply([1, 2, 3], 5)) // [ 5, 10, 15 ]これをSwiftで同じように書くと以下のようになります。
func multiply(_ array: [Int], _ n: Int) -> [Int] { return array.map({ x in n * x }) // 単一式は`return`を省略できる}print(multiply([1, 2, 3], 5)) // [5, 10, 15]Swiftの末尾クロージャは、以下のような構文です。
- 関数の最後の引数として渡されるクロージャ式は、関数呼び出しの
( )の後ろに書く ことができる - 末尾クロージャを使って関数呼び出しの
( )が空になるときは( )を省く ことができる
末尾クロージャの構文と省略引数名を使って、さらにmultiply関数のreturnを省略すると、上のコードは以下のようになります。
func multiply(_ array: [Int], _ n: Int) -> [Int] { return array.map({ x in n * x }) array.map { n * $0 }}print(multiply([1, 2, 3], 5)) // [5, 10, 15]つまり、SwiftUIのサンプルコードの7行目, 8行目, 10行目は以下のように解釈できます。
- 7行目は、
List構造体のイニシャライザを呼び出しており、引数にalbum.songsと末尾クロージャ{ song in 処理の本文 }を渡している。 - 8行目は、
HStack構造体のイニシャライザを呼び出しており、( )が無いので末尾クロージャだけ引数として渡している。 - 10行目は、
VStack構造体のイニシャライザを呼び出しており、引数にalignment: .leadingと末尾クロージャを渡している。
末尾クロージャの処理の本文については次節で説明します。
DSLによる宣言型プログラミング(@ViewBuilder)
Section titled “DSLによる宣言型プログラミング(@ViewBuilder)”サンプルコードの8行目以降を簡略的に書くと、以下のようになります。
HStack { Image(album.cover) VStack(alignment: .leading) { /* 中略 */ }}VStack(alignment: .leading) { Text(song.title) Text(song.artist.name).foregroundStyle(.secondary)}HStackは水平配置、VStackは垂直配置を表しており、それぞれImageやTextを並べて表示するということは経験的に察しましたが、構造体をカンマなどの記号なしで箇条書きに並べる構文は、AIに @ViewBuilderを使ったDSL と教えてもらうまで理解不能でした。
HStackのイニシャライザの仕様は以下のとおりです。VStackも同様です。
init( alignment: VerticalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content)最後のcontent引数が戻り値Contentの関数型になっており、これがHStackやVStackの後ろにある末尾クロージャに対応します。content引数の前にある@ViewBuilderが、クロージャのブロック内を宣言的な構文で記述できるようにする仕組み(リザルトビルダ)になります。
@ViewBuilderは、クロージャのブロック内を以下のような処理相当に構文変換を行います。
VStack(alignment: .leading) { let text1 = Text(song.title) let text2 = Text(song.artist.name).foregroundStyle(.secondary) return ViewBuilder.buildBlock(text1, text2)}text2に関する蛇足
text2はforegroundStyleメソッドの戻り値が入りますが、これはTextにスタイルを適用したView準拠のインスタンスです。Viewプロトコルに定義されている他のメソッドの戻り値も同様になっており、そのおかげでメソッドを連鎖的に呼び出すメソッドチェーンを書けるようになっています。
ViewBuilder.buildBlockの定義は以下のようになっており、TupleView構造体のインスタンスを返します。
static func buildBlock<each Content>(_ content: repeat each Content) -> TupleView<(repeat each Content)> where repeat each Content : Viewつまり、HStackやVStackは末尾クロージャでTupleView構造体のインスタンスを取得しているということがようやく理解できました。
試しに、XcodeでHello, world!を表示するだけのアプリを新規作成して@ViewBuilderを使わずにSwiftの構文で書き直してみました。
import SwiftUI
struct ContentView: View { var body: some View { VStack { Image(systemName: "globe") .imageScale(.large) .foregroundStyle(.tint) Text("Hello, world!") } .padding() }}Swiftの構文で書き直したコードは以下のとおりです。計算プロパティも省略無しで書き直してみました。
import SwiftUI
struct ContentView: View { var body: some View { get { return VStack(content: { let image = Image(systemName: "globe") .imageScale(.large) .foregroundStyle(.tint) let text = Text("Hello, world!") return TupleView((image, text)) }) .padding() } }}SwiftUIの宣言型構文は、UI構築の煩雑なコードを簡潔に記述できるようにしていることが分かります。
なお、AlbumDetail構造体のbodyプロパティも@ViewBuilderが付いていることから、この計算プロパティはDSLで構文変換されます。この場合のViewBuilder.buildBlockは以下のオーバーロードが適用されて、Listのインスタンスを返していると考えられます。
static func buildBlock<Content>(_ content: Content) -> Content where Content : View[蛇足] リザルトビルダで新しいDSLを作ってみる
Section titled “[蛇足] リザルトビルダで新しいDSLを作ってみる”リザルトビルダの知識を習得できたので、簡単な独自リザルトビルダを作ってみました。@SumBuilderは整数(Int型)の並びを入力して、その和で結果を構築します。
@resultBuilderstruct SumBuilder { static func buildBlock(_ elements: Int...) -> Int { elements.reduce(0, +) }}
// 関数の引数として@SumBuilderを使うfunc sum(@SumBuilder sumBuilder: () -> Int) -> Int { return sumBuilder()}
var x = sum { 1 2 3 4 5 sum { 6; 7; 8; 9; 10 } // 文はセミコロンで結ぶことができる}print(x) // 55
// 計算プロパティ(get)に@SumBuilderを使うstruct A { @SumBuilder var x: Int { 1 4 7 }}var a = A()print(a.x) // 12型推論による省略
Section titled “型推論による省略” VStack(alignment: .leading) { /* 中略 */}ドット(.)から始まる識別子はVisual BasicのWithステートメントによる構文で見たことがありますが、Swiftではドットの前に置かれるはずの型名や変数名などの識別子がソースコードのどこにも見当たらないので困惑しました。
Swiftでは型推論によって型名が省略できるようになっています。例えば、以下のようなコードの場合、
enum Direction { case north, south, east, west}
func showDirection(_ direction: Direction) { switch direction { case Direction.north: // 列挙型の型名を明示している print("北") case Direction.south: // 列挙型の型名を明示している print("南") case Direction.east: // 列挙型の型名を明示している print("東") case Direction.west: // 列挙型の型名を明示している print("西") }}
showDirection(Direction.north)direction引数の型がDirection型であると分かるため、型名は省略できるようになっています。
enum Direction { case north, south, east, west}
func showDirection(_ direction: Direction) { switch direction { case .north: // 型推論によって列挙型の型名を省略できる print("北") case .south: // 型推論によって列挙型の型名を省略できる print("南") case .east: // 型推論によって列挙型の型名を省略できる print("東") case .west: // 型推論によって列挙型の型名を省略できる print("西") }}
showDirection(.north)上記のように引数の型名がソースコード上に書かれていればどういう型の値であるかを読み取れるのですが、SwiftUIのサンプルコードではそれが見当たらないので、.leadingが何の値なのかを知るためには、まずVStackのalignmentの型を調べる必要があります。
init( alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content)ようやく.leadingがHorizontalAlignment構造体の定数であることが分かりました。
同様に13行目のforegroundStyle(.secondary)を調べると、.secondaryはHierarchicalShapeStyle構造体の定数でした。
Xcodeでコーディングしているときは引数に設定できる値の候補と説明が表示されるので分かりますが、ソースコードを読み返すときには引数の型を調べたりするので、不便に感じます。
サンプルコードを省略無しで書き直してみる
Section titled “サンプルコードを省略無しで書き直してみる”returnやプロパティのgetの省略、型名の省略、@ViewBuilderによる宣言的な構文、末尾クロージャの構文を使わないでサンプルコードを書き直してみました。動作確認用に、Album構造体を簡易的に実装しています。
import SwiftUI
// Album構造体は必要最低限だけ実装したstruct Album { struct Artist { var name: String } struct Song: Identifiable { let id = UUID() let title: String let artist: Artist } let cover = "globe.americas.fill" let songs: [Song] = [ Song(title: "曲名1", artist: Artist(name: "アーティスト1")), Song(title: "曲名2", artist: Artist(name: "アーティスト2")), Song(title: "曲名3", artist: Artist(name: "アーティスト3")), ]}
// SwiftUIのAlbumDetail構造体を省略無しで書き直してみたstruct AlbumDetail: View { var album: Album
var body: some View { get { return List(album.songs, rowContent: { song in return HStack(content: { return TupleView(( Image(systemName: album.cover), // シンボル画像に変更 VStack(alignment: HorizontalAlignment.leading, content: { return TupleView(( Text(song.title), Text(song.artist.name) .foregroundStyle(HierarchicalShapeStyle.secondary) )) }) )) }) }) } }}
// AlbumDetail構造体をメインのビューに埋め込んで表示するstruct ContentView: View { var body: some View { AlbumDetail(album: Album()) }}
#Preview { ContentView()}このように書き直すと、Swiftの言語構文をあまり知らなくてもAlbumDetail構造体の各行が何をしているのかが分かるようになりましたが、括弧記号の対応付けが複雑で、何度か入力ミスがありました。
構文を理解すれば、Swiftの省略できる記述方法と宣言的な構文は読みやすく、コーディングやメンテナンスがやりやすいと思います。
SwiftUIのサンプルコードを読み解くために、以下の知識が必要でした。十数行程度のサンプルコードでしたが、学ぶことは多かったです。
- SwiftUIは、プロトコルに準拠した構造体で実装する。
- あるプロトコルに準拠している構造体が特定の型で定まるときは
someを使う。bodyプロパティの型はBodyというViewプロトコルの関連型のため、someを使う必要がある。
- Swiftは記述を省略できる部分が多い。
- 計算プロパティは、読み取り専用であれば
getを省略できる。 - 関数やクロージャ、計算プロパティの
getは、単一式のときはreturnを省略できる。 - 変数・定数宣言時の型注釈や、列挙型の型名など、型推論できるときは省略できる。
- 末尾クロージャを使うと、関数の引数の記述が簡易化できる。空の
()は省略できる。
- 計算プロパティは、読み取り専用であれば
- SwiftUIは
@ViewBuilderを使って、bodyプロパティやHStack・VStackなどのイニシャライザ引数に宣言型構文を使うことができる。