もと(英語)→
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
なので表示される画像も同じサイズになって、画面の真ん中に表示されます。
見やすいため記事のスクショは全部ボーダー付きにしました。
画像のボーダーは全部「赤色」
デバイスのボーダーは「グレー」
フレームのボーダーは「緑色」
テキストのボーダーは「黄色」
他の記事でSwiftUIのボーダーについて書きますがとりあえず今回はビューのバウンドを見やすくするために使用し、コードからは抜きます。
.frame
modifierを使うと画像をフレームの中に入れることができます。
struct ContentView : View {
var body: some View {
Image("barbarian")
.frame(width: 200, height: 200)
}
}
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だと画像がフレームをオーバーフローするだけです。
重ねられるビューを作る時、これは使えますね。
ですが、.clipped
や.cornerRadius
みたいな、画像を切り取るmodifierを使うとフレームのバウンドを元に画像を切り取ることは可能です。が、長くなるのでこの記事では詳しくは書かないことにします。
あとでまた画像に戻りますがここで他に確認するべきところがありますので一旦Text
をみてみましょう。
Text
さっきのレイアウトを画像じゃなくてキャラクターの名前の文字列にしてみましょう。
struct ContentView : View {
var body: some View {
Text("Nogitsune Takeshi")
}
}
画像と同じように画面の真ん中に文章が表示されています。Textも文字がちゃんと入るサイズを自分で決めました。そして.frame
も同じように使うことはできます。
struct ContentView : View {
var body: some View {
Text("Nogitsune Takeshi")
.frame(width: 200, height: 200)
}
}
.frame
がもう1つの新しいビューを作ることを知っているとTextが前と同じく真ん中に置いてあることに驚きはないと思います。
Textはサイズ選びに設定は色々ありますよ。例えば.font
でフォントを選べます。ヒーローの名前なので.title
にしましょう。
struct ContentView : View {
var body: some View {
Text("Nogitsune Takeshi")
.font(.title)
}
}
フォントが大きくなったので前よりテキストが大きくなっています。
画像の対応と完全に同じにするため大きくなったTextを小さすぎるフレームにぶち込んでみます。
struct ContentView : View {
var body: some View {
Text("Nogitsune Takeshi")
.font(.title)
.frame(width: 200, height: 200)
}
}
似たコードで画像のサイズを全く変更せずにフレームをオーバーフローしてしまったが、Textだと.font
でサイズの変更できましたしこれの結果は違うのかな?!
オーバーフローせず、ちゃんとフィットするサイズを選ぶことはできたらしい。この場合だとコンテンツを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のたった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が文字列を切り捨てて自分の横幅を減らすようにしてます。
もう一度、選ばれた横幅はコンテンツが丁度入るくらいのサイズで親ビュー(Frame)の真ん中にポンと置かれています。
縦幅が足りない時でも切り捨てが起きることがあります。
struct ContentView : View {
var body: some View {
Text("Nogitsune Takeshi")
.font(.title)
.frame(width: 200, height: 50)
}
}
行数が限られてないとしても提案サイズの縦幅が1行しか入らないのでこの例でコンテンツが提案サイズに合わせてます。
リサイズ可能な画像
ビューが自分のサイズを選ぶときに柔軟に選べることはわかった上でもう一度Imageを見てみましょう。
普段Imageは画像ファイルの解像度のサイズになりますが、リサイズすることもできます。
struct ContentView : View {
var body: some View {
Image("barbarian")
.resizable()
.frame(width: 200, height: 150)
}
}
リサイズ可能なImage(リサイザブル)は親ビューからもらった提案サイズそのまま選んでできるだけスペースを埋むようにします。
デフォルトでImageは画像を伸ばしてスペースを埋むけど.resizable(resizingMode: .tile)
を使うと画像をリピートすることができます。
struct ContentView : View {
var body: some View {
Image("barbarian")
.resizable(resizingMode: .tile)
.frame(width: 200, height: 150)
}
}
ImageのサイズはまだFrameからもらった提案サイズのままなのですが、単に画像を複数回描画しているだけに注意。
アプリつくっているとき、画像を伸ばすのも複数描画のも違う単純に「変に伸ばさずこのサイズで一番大きいサイズで画像を表示したい」だけの時が多いと思いますが、その時は.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は一番最初のビューになります。
そしてそれで正しく表示できます→
ソース画像と親ビューの提案サイズを参考にして、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.