🦤

【SwiftUI】Viewに表れる謎の記法を理解する

2023/05/26に公開

概要

最近Swift・SwiftUI・Xcodeの勉強を始めたのですが,最初のチュートリアルに出てくる基本の文法が全く理解出来ず,調査したまとめです.

既にSwiftを学んでおられる方にとっては基本中の基本かと思いますが,所謂"おまじない"として詳細な説明をスルーされがちな部分かと思いますので,同じところで躓いている方はご参考にしてください.

なお,1つ1つ説明していると長くなるため,このコードの正体は何かに重点を置き,詳細な説明は省きます(詳しい内容は参考文献をご参照ください).

問題のコード

SwiftUI Tutorials - Creating and Combining Viewsより引用

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack(alignment: .leading) {
            Text("Turtle Rock")
                .font(.title)
            Text("Joshua Tree National Park")
                .font(.subheadline)
        }
    }
}

一見するとこのコードはアプリケーションの画面表示を定義する上で非常に簡潔かつ直感的にわかりやすいコードであり,深い理解をせずとも使い方やコードの意味するものがわかる素晴らしい言語だと思います.

その一方,独自の書き方キーワード説明無しの記法の省略が入り乱れており,初学者の段階でこのコードを完全に説明するのはかなり難易度が高いとも感じます.

今回は,このコードを分解しつつ,それぞれの意味するところを解説します.

コードの解説

struct ContentView: View {}:Viewプロトコルへの準拠

SwiftUIでは,Viewを準拠した構造体を定義することで,アプリケーションにおける1つの画面を作成することが出来ます.

最もシンプルな構造体の定義は以下のような記述で行えます.今回もこの通りContentViewを定義しています.

struct 構造体名 {
...
}

勘違いしやすい部分はその後の: Viewの部分で,少しオブジェクト指向を齧った程度の私は直感的にViewクラスの継承かと思ったのですがこれは誤りで,正しくはViewプロトコルに準拠です.

クラスの継承ととプロトコルの準拠はどちらも似た概念ですが,クラスの継承はクラスでしか出来ず,プロトコルの準拠はクラスでも構造体でも出来るという違いがあります.

クラスと構造体の主な違いは以下の通りです.

クラス 構造体
参照型 値型
クラスの継承
プロトコルの準拠

ここでは長くなるためクラスとプロトコルの詳細な説明は致しませんが,非常に似た概念ですので,しっかりと理解・区別することが大切です.

var body: some View : Opaque Result Types

var bodyは変数定義,正確にはstructの内部であるためContentViewのプロパティですね.ここまでは何ら問題無いでしょう.

最初に問題となるのはキーワードsomeです.私は他の言語では見たこと無い(Swift独自?)ですがこれはOpaque Result Typesであることを表しています.

Opaque Result Typesとはパフォーマンスを維持したままプロトコルに準拠する方法です.
例えば,今回の場合,単にViewプロトコルに準拠していることを表すには以下のように記述することも出来ます(実際にはエラーが出ます).

var body: View

しかし,この方法はパフォーマンス面,特にメモリの確保等でロスが大きいとされ,多用することは望ましくありません.

var body: some View

someをつけることでパフォーマンス面のロスを減らしつつ,プロトコルに準拠した型を表せるようになります.

この部分の勉強には主に以下の記事を参考にさせていただきました.Opaque Result Typesだけで1つの記事に出来るくらいボリュームがあるので,詳細はぜひ以下の記事をご参照ください.

var body: some View {} : 省略された計算型プロパティ

var body: some Viewの意味はわかりましたが,その直後に{}が付いています.これはsetterとreturnが省略され,かつgetの表記も省略された形の計算型プロパティです.

まず,計算型プロパティとは値を代入/参照するタイミングで,値を直接保存せずその都度getsetに定義された計算を行うという仕組みです.
詳しくはコチラ:Swift5でgetterとsetter(計算型プロパティ)についてまとめた

計算型プロパティの基本的な記法は以下の通りですが,Swiftにおける計算型プロパティは様々な省略形が存在します.

var プロパティ名:{
    get{
        ...
        return ...
    }
    set(仮引数){
        ...
    }
}

まずはsetの省略です.単に値を参照出来るだけのプロパティを作成する場合,setを省略することが出来ます.

var プロパティ名:{
    get{
        ...
        return ...
    }
}

次に,returnの省略です.get内に記述される内容が戻り値だけの場合,returnを省略出来ます.(この辺りの話はクロージャの省略形が話のベースになっていますので,興味のある方はそちらも調べてみてください.)

var プロパティ名:{
    get{
        ...
    }
}

最後に,getの表記の省略です.計算型プロパティにgetしか含まれない場合,その記述すら省略出来ます.
以下のコードは1つ前のコードと同義です.

var プロパティ名:{
    ...
}

これがvar body: some View {}の正体であり,set,return,getが省略された形の計算型プロパティです.returnの省略等はクロージャの定義でも頻出するため,最初から慣れておくと良いでしょう.

VStack(alignment: .leading) {}:引数としてのクロージャ

最後に,VStack(alignment: .leading) {}です.

VStackはgetter内部の記述であるため,先程の話からContentView構造体のbodyプロパティを参照した時にVstackが返される,ということがわかります.

また,VStack(alignment: .leading)までは良いですね.VStackクラスに対し,イニシャライザの引数に値を与えて初期化しています.(ちなみに,.leadingも省略形です.本来はHorizontalAlignment.leadingですが,引数alignmentがHorizontalAlignment型しか受け付けないため,HorizontalAlignmentを省略できます.)

問題はこの後で,また唐突に出てくる{}と,その中に雑に積み上げられたTextです.

まず,唐突に出てくる{}ですが,これはクロージャを引数として与えるという処理です.
先程からクロージャという名前も説明無しに使ってしまっていましたが,クロージャはある処理をひとまとまりにして再利用可能にしたもので,関数のようなものです(実際には関係が逆で,関数はクロージャの一種です).

このクロージャを引数として渡す場合に,通常のイニシャライザの引数を()で与えた後,続けて{}で与える事ができます.

クラス名(イニシャライザの引数){クロージャ引数}

実際に,Vstackのイニシャライザの定義を見ると,3つ目の引数にクロージャを取っていることがわかります.

@inlinable public init(alignment: VerticalAlignment = .center,
                       spacing: CGFloat? = nil,
                       @ViewBuilder content: () -> Content)

VStack(alignment: .leading) {}:@ViewBuilder

さて,これで最後です.VStackの最後についている{}はクロージャを与えている,ということはわかりました.
では,実際にどのようなクロージャを与えているのか見てみると,Textを2つ与えています.

VStack(alignment: .leading) {
    Text("Turtle Rock")
	.font(.title)
    Text("Joshua Tree National Park")
	.font(.subheadline)
}

しかし,これは通常の処理ではあり得ない書き方です.与えられるクロージャは1つだけなのに対し,ここにはTextが2つ定義されており,何らかの結合処理等もされていません.

実は,これはText自体に秘密があるのではなく,VStack側の引数定義がこれを可能にしています.
先程挙げた引数定義はこのようになっていました.

@ViewBuilder content: () -> Content

この@ViewBuilderアトリビュート(Attributes) というもので,コンパイル時に特定の部分の解釈を変える作用を持つものです.
今回の場合,Vstackは以下のように解釈されます.

VStack(alignment: .leading) {
    ViewBuilder.buildBlock(
	Text("Turtle Rock").font(.title),
	Text("Joshua Tree National Park").font(.subheadline)
    )
}

これにより,TextはbuildBlockに対する複数の引数として解釈され,buildBlockの結果がVStackに渡されるため,正常に動作する,というわけです.

ちなみに,ContentView構造体のbodyプロパティにも@ViewBuilderアトリビュートが付いているため,同様の記述が可能です.

おわりに

最近,ふとmacos用のアプリケーションを作りたくなり,SwiftとXcodeの勉強を始めました.
PythonやCといった基本的な言語は問題無く使用できるため,良くも悪くも,あまり勉強していない言語もググってしまえば基本的なプログラミングは出来てしまい,恥ずかしながらSwiftもきちんと勉強したことはありませんでした.

そのため,今回しっかりとAppleの設計思想に則り言語仕様をある程度理解した上で開発を進めようと思い,公式チュートリアルを途中まで進めたのですが細かい部分の文法が全く理解出来ず...

逆に,今回ご紹介した内容がわかるようになれば,少なくともSwiftの設計方針がなんとなくわかるようになると思いますし,今後似たような問題にぶつかったとしてもその原因を予測しやすくなるとは思います.

まだまだ私も勉強途中のため今回まとめた内容にも間違いがあるとは思いますが,その時はご指摘いただけると幸いです.

参考文献

Discussion