15
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at
もと(英語)→
https://netsplit.com/swiftui/views-choose-their-own-sizes/

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

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

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

Images

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


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

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

frame.png

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

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


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

frame2.png

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

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

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

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

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

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

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


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

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

overflow.png

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

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

Text

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


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

text_frame.png

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


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

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

text_frame_2.png

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


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

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

title_text.png

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


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

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

title_text_multiline.png

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

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

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

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

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より縦が大きい時も同じようなことが起きます。


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

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

text_frame_overflow.png

テキストの切り捨て

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


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

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

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


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

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

text_frame_truncation.png

リサイズ可能な画像

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

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


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

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

resizable_image.png

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


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

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

tile_image.png

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

Modifierの順番

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

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

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


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

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

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

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

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

aspectratio_image.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.

15
10
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?