SwiftUIのViewは自分のサイズを自分で決めます

```text
もと（英語）→
https://netsplit.com/swiftui/views-choose-their-own-sizes/

外国人なので日本語がおかしいところがあれば是非教えてください。
```

SwiftUIの紹介記事などでだいたい最初に書かれるのは「ビューは自分で自分のサイズを決め、一回決められたサイズは上書きできません」とのことですが、こんな簡単そうなことでも以外と複雑な闇を隠しています。なぜならその簡単そうな宣言が我らの知っているレイアウトについての全ての常識を変えるのです。

SwiftUIはUIKitやAppKitの考え方とは違いますしメリットとデメリットもありますので時間をかけてちゃんと勉強する価値はあると思います。なのでこの記事ではたくさんのレイアウトの土台になるビューの２つ（ImageとText）を見ながら宣言の深い意味を考えてみましょう。

## Images

まず画像を表示しましょう：

```swift

struct ContentView : View {
    var body: some View {
        Image("barbarian")
    }
}
```

ソース画像のサイズ（解像度）は `101px x 92px` なので表示される画像も同じサイズになって、画面の真ん中に表示されます。

![frame.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126962/5cb3828c-541e-c5d5-892b-94ba954a5577.png)


見やすいため記事のスクショは全部ボーダー付きにしました。
画像のボーダーは全部「赤色」
デバイスのボーダーは「グレー」
フレームのボーダーは「緑色」
テキストのボーダーは「黄色」
他の記事でSwiftUIのボーダーについて書きますがとりあえず今回はビューのバウンドを見やすくするために使用し、コードからは抜きます。

`.frame`modifierを使うと画像をフレームの中に入れることができます。

```swift

struct ContentView : View {
    var body: some View {
        Image("barbarian")
            .frame(width: 200, height: 200)
    }
}
```

![frame2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126962/b4cf55bd-bbca-6c1d-ae6c-6e1d58ca4e25.png)


UIKit, AppKitなどだとこれは意外な結果ですよね？

画像を拡大するのじゃなくて、画像は同じサイズそのままフレームの真ん中にドンと置いてあるだけです。

結局のところ、`.frame`は使用されたビューのプロパティーを変更せず指定されたサイズで新しく "frame" というビューを作成し使用されたビューがその新しいフレームのチャイルドとして追加されます。

### ポイント！！`.frame`は使用されたビューのプロパティーを変更せず指定されたサイズで新しく "frame" というビューを作成し使用されたビューがその新しいフレームのチャイルドとして追加されます。

これがSwiftUIの基礎的なところなのでちゃんと理解をした方がいいんじゃないかなと思います。 `.frame`みたいなmodifierは新しくビューを作成し元のビューが新しいビューにチャイルドとして追加されます。

Imageのサイズが画像アセットだけを元に選ばれます。親ビューがそれを上書きできません。

シンプルなデモとして小さすぎるフレームに画像をぶち込んでみましょう。

```swift

struct ContentView : View {
    var body: some View {
        Image("barbarian")
            .frame(width: 50, height: 200)
    }
}
```

多分これだと画像が必ずリサイズされる！かと思うのは普通ですが、SwiftUIだと画像がフレームをオーバーフローするだけです。

![overflow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126962/35676f59-1e11-7708-ca27-2fbfedcb5b3d.png)


重ねられるビューを作る時、これは使えますね。
ですが、`.clipped`や`.cornerRadius`みたいな、画像を切り取るmodifierを使うとフレームのバウンドを元に画像を切り取ることは可能です。が、長くなるのでこの記事では詳しくは書かないことにします。

あとでまた画像に戻りますがここで他に確認するべきところがありますので一旦`Text`をみてみましょう。

## Text

さっきのレイアウトを画像じゃなくてキャラクターの名前の文字列にしてみましょう。

```swift

struct ContentView : View {
    var body: some View {
        Text("Nogitsune Takeshi")
    }
}
```

![text_frame.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126962/cd2161a4-c571-ea70-b98d-8aa111849af1.png)


画像と同じように画面の真ん中に文章が表示されています。Textも文字がちゃんと入るサイズを自分で決めました。そして`.frame`も同じように使うことはできます。

```swift

struct ContentView : View {
    var body: some View {
        Text("Nogitsune Takeshi")
            .frame(width: 200, height: 200)
    }
}
```


`.frame`がもう１つの新しいビューを作ることを知っているとTextが前と同じく真ん中に置いてあることに驚きはないと思います。

![text_frame_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126962/baf9385e-5343-ccf7-f536-2c77dec5deb0.png)


Textはサイズ選びに設定は色々ありますよ。例えば`.font`でフォントを選べます。ヒーローの名前なので`.title`にしましょう。

```swift

struct ContentView : View {
    var body: some View {
        Text("Nogitsune Takeshi")
            .font(.title)
    }
}
```

フォントが大きくなったので前よりテキストが大きくなっています。

![title_text.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126962/2d0755eb-b842-6a84-f0f0-3aa82a250e9c.png)


画像の対応と完全に同じにするため大きくなったTextを小さすぎるフレームにぶち込んでみます。

```swift

struct ContentView : View {
    var body: some View {
        Text("Nogitsune Takeshi")
            .font(.title)
            .frame(width: 200, height: 200)
    }
}
```

似たコードで画像のサイズを全く変更せずにフレームをオーバーフローしてしまったが、Textだと`.font`でサイズの変更できましたしこれの結果は違うのかな？！

![title_text_multiline.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126962/290f24f7-3949-b88f-3fe9-c183b015fa4a.png)


オーバーフローせず、ちゃんとフィットするサイズを選ぶことはできたらしい。この場合だとコンテンツを2行に分け、横幅の広さを抑えて縦幅を大きくしました。

これは「親ビューは決まったサイズを上書きできない」とのことを矛盾しない。ただまだ勉強していないもう１つの原則を表す→ ビューが自分のサイズを決める前に親から「提案」をもらいます。

### ポイント！！ビューが自分のサイズを決める前に親から「提案」をもらいます。

ちなみに「親が子に渡す提案サイズ」と「親が決める自分（親）のサイズ」は別物です。大体、親は子ビューを元に自分のサイズを選びますので。

`Flexible Frames`は複雑なので、他の記事でもう少し詳しく書きます。

## 提案サイズについて

前の例で、サイズを決めてビューが設置された手順の理解がSwiftUIの理解の鍵になります。Textが自分のサイズを決めて、そのサイズを上書き不可能。だがTextは親から渡されたサイズをもとに自分のサイズを柔軟に選べます。

Frameを`200 x 200`のサイズを指定したのでTextを設置する準備が終えた後にTextに提案サイズとして自分のサイズを渡し、その後フレームが、チャイルドになっているTextに自分のサイズを決めろと頼みます。

Frameがない例でデバイスのバウンドがTextに提案サイズを渡しました。

Frameがある例で提案された横幅（width）が足りなかったが縦幅（height）に十分のスペースがあったのでコンテンツを2行に分けたら収まることができます。

Textは親が渡した提案サイズを元に自分のコンテンツの最適な表示方法を決めてそのように表示してるのです！

普段は決められたサイズが提案サイズより小さいのがいいです、コンテンツが丁度入るぐらいの大きさ。んで、親から渡された提案サイズが大ぎるとしても自分がそのサイズに合わせて大きくなること基本的にないです。

それに、上の例のように提案サイズのwidthをそのまま選ぶじゃなくて、2行になった文字列が入るの、提案より小さいサイズになりました。Frameよりは小さいのでTextをFrameの真ん中に設置されることになりました。

ImageがFrameより大きい場合のようにTextがFrameより縦が大きい時も同じようなことが起きます。

```swift

struct ContentView : View {
    var body: some View {
        Text("Nogitsune Takeshi")
            .frame(width: 200, height: 10)
    }
}
```

ImageがFrameをオーバーフローするしTextもオーバーフローしちゃいます。

![text_frame_overflow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126962/3d3ce5f6-9871-9c9a-ed1f-420f72f17ad8.png)


### テキストの切り捨て

サイズを柔軟に選ぶのに数行に分けるのはTextのたった１つの選択ではありません。文字と文字の間のスペースを小さくすることもでき、コンテンツを切り捨てることもできます。こうやって切り捨て機能を確認できます →

```swift

struct ContentView : View {
    var body: some View {
        Text("Nogitsune Takeshi")
            .font(.title)
            .lineLimit(1)
            .frame(width: 200, height: 200)
    }
}
```

`.lineLimit`を使ったことによって前の無限行数ではなく1行に収まるように命令しましたので、提案サイズが足りないときはTextが違うやり方でコンテンツを抑えてサイズを選ばないといけない。1行で文字列の横幅をフィットすることができず、数行使うこともできないのでTextが文字列を切り捨てて自分の横幅を減らすようにしてます。

![title_text_frame.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126962/ec86ddd4-410c-55f3-34e7-674244307249.png)


もう一度、選ばれた横幅はコンテンツが丁度入るくらいのサイズで親ビュー（Frame）の真ん中にポンと置かれています。

縦幅が足りない時でも切り捨てが起きることがあります。

```swift

struct ContentView : View {
    var body: some View {
        Text("Nogitsune Takeshi")
            .font(.title)
            .frame(width: 200, height: 50)
    }
}
```

行数が限られてないとしても提案サイズの縦幅が1行しか入らないのでこの例でコンテンツが提案サイズに合わせてます。

![text_frame_truncation.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126962/2b844424-4888-3f2d-f2a7-d04b7c23f658.png)


### リサイズ可能な画像

ビューが自分のサイズを選ぶときに柔軟に選べることはわかった上でもう一度Imageを見てみましょう。

普段Imageは画像ファイルの解像度のサイズになりますが、リサイズすることもできます。

```swift

struct ContentView : View {
    var body: some View {
        Image("barbarian")
            .resizable()
            .frame(width: 200, height: 150)
    }
}
```

リサイズ可能なImage（リサイザブル）は親ビューからもらった提案サイズそのまま選んでできるだけスペースを埋むようにします。

![resizable_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126962/6ee24583-4e7c-6e6a-56e4-414bfa0f2361.png)


デフォルトでImageは画像を伸ばしてスペースを埋むけど`.resizable(resizingMode: .tile)`を使うと画像をリピートすることができます。

```swift

struct ContentView : View {
    var body: some View {
        Image("barbarian")
            .resizable(resizingMode: .tile)
            .frame(width: 200, height: 150)
    }
}
```

ImageのサイズはまだFrameからもらった提案サイズのままなのですが、単に画像を複数回描画しているだけに注意。

![tile_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126962/d88de42d-9b89-e8e6-6326-35314cf67e4d.png)


アプリつくっているとき、画像を伸ばすのも複数描画のも違う単純に「変に伸ばさずこのサイズで一番大きいサイズで画像を表示したい」だけの時が多いと思いますが、その時は`.aspectRatio`という便利なmodifierもあります。`.aspectRatio`を`.resizable`と一緒に使うことでアスペクト比変わらずリサイズをすることが可能になります。

### Modifierの順番

複数なmodifierを使うときは順番は重要です。各modifierがそのmodifierが使われたビューに適応されるのでSwiftUIのmodifierは逆の順番に書かれるのです。

### ポイント！！SwiftUIのmodifierは逆の順番に書かれるのです。

ソース画像のアスペクト比で正しくframeのサイズに合わせたImageを作るのに以下の順番でmodifierを書く必要があります。

```swift

struct ContentView : View {
    var body: some View {
        Image("barbarian")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: 200, height: 150)
    }
}
```

このコードでは`.resizable`はImageに追加されました。ってことはImageは`.resizable`のチャイルドになります。`.frame`と同じように。

そのあとは第２のmodifier、`.aspectRatio`を追加することによってaspectRatioは「リサイザブル画像」に適応されて、「リサイザブル画像にアスペクト比を適応する」という流れなので、modifierチェーンの最後に追加します。

そして最後に「アスペクト比を使うリサイザブル画像」をframeに打ち込みたいので最後に`.frame`を追加。大事なポイントはチェーンの最後にframeは来てますがビューの構成ではframeは一番最初のビューになります。

そしてそれで正しく表示できます→

![aspectratio_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126962/511504e0-b645-2c00-63d5-9a07ee0f332a.png)


ソース画像と親ビューの提案サイズを参考にして、`Image`が親が渡したheightを使おうとしましたが、元画像のheightが足りないので元画像のサイズを大きくしてheightを選び、アスペクト比が変わらないためのwidthを計算して選んで、サイズを決定しました。

親の横幅が大きすぎて子ビューが全部使わないので真ん中に置くことになりました。

こういったルールを理解した上で複数ビューをstackで合体することやflexible frameの奇妙なケースなどを確認できるでしょう！

Imagery used in previews by Kaiseto, original images and derived here licensed under Creative Commons 3.0 BY-NC-SA.