[SwiftUI] SwiftUI の layout システムを理解する (領域が不足するケースの調停)

SwiftUI2021

SwiftUI での レイアウトされる際に、スペースが不足するケースを説明します

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Monterey 12.1 RC
  • Xcode 13.2 RC
  • iOS 15.2

SwiftUI でのレイアウト

これまでに、領域が十分あるケースで、push-out/pull タイプのビューがレイアとされる時の挙動をみてきました。

SwiftUI2021[SwiftUI] SwiftUI の layout システムを理解する (コンテナビューに含まれる push-out / pull-in タイプビューの調停) SwiftUI2021[SwiftUI] SwiftUI の layout システムを理解する (push-out タイプビューの調停) SwiftUI2021[SwiftUI] SwiftUI の layout システムを理解する (push-out タイプビュー, pull-in タイプビュー とレイアウトの調整)

今回は、レイアウトする領域が、十分に広くないケースでの振る舞いを確認します。

領域が不足しているケースでのレイアウト

領域位が不足している とは?

以前の記事でも書きましたが、親ビューが与える領域は、子ビューの実際に必要な領域を考慮しません。

SwiftUI2021[SwiftUI] SwiftUI の layout システムを理解する (基礎編: レイアウトシーケンス)

子ビューは、必要ならば親ビューから与えられた領域を考慮して自分の領域を決定し、そこに表示を行うだけです。

親ビューと領域について ”もっと高くしたい” や ”もう少し狭くても良い” 等のやりとりはしません。

ですので、”領域が不足するケース” とは、親ビューから与えられた領域が ”親ビューから提案されたサイズをベースに自分で決定した使用領域が 自分自身を描画するのに十分でないケース” です。

例えば、Text は、領域の幅が不足していると、複数行に変更したり、文字列の一部を ... という表示に変更したりします。

ですが、これは 個別のビューの振る舞いですので、今回の確認範囲とはしません。

今回は、以下のようなケースでの振る舞いを確認していきます。

  • 可変高さを持つ複数の子要素を持つ VStack のレイアウト
  • 固定高さを持つ複数の子要素を持つ VStack のレイアウト
  • 固定高さと可変高さを持つ子要素を持つ VStack のレイアウト

可変高さを持つ要素とは、push-out タイプのビューを指していて、固定高さを持つ要素とは pull-in タイプのビューを指しています。

pull-in タイプのビューとして、Text を .frame で高さを固定したビューを使って確認していきます。

push-out タイプのビューは、Color を使います。

可変高さを持つ複数の子要素を持つ VStack のレイアウト

一番わかりやすいケースから考えます。

VStack に高さが与えられているならば、等分した高さを与えてレイアウトします。

与えられる高さが存在する(つまり 0 より大きい時)は、(表示が収まるかは別として)可変高さを持つ要素としてはレイアウトできます。

これは、以前に確認したケースです。

ですので、領域が不足するということは、layout システム上は、存在しないはずです。

固定高さを持つ複数の子要素を持つ VStack のレイアウト

2個の固定高さ(300)を持つ Text を含む VStack(高さ500)のレイアウトで確認してみます。

簡略化すると以下のようなビューです。


VStack {
  Text("Hello").frame(height:300)
  Text("world").frame(height:300)
}.frame(height: 500)

下のスクリーンショットを見るとわかりますが、VStack に収まり切らないようにレイアウトされています。
(オレンジ色の枠が、VStack の枠です)

# .clipped を付与すると、上下が切られてしまうことも確認できます。

TooTallTexts

実際のコードは以下のものを使っています。


//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2021/12/10
//  © 2021  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack(spacing:0) {
            Text("Hello")
                .frame(height: 300).frame(maxWidth: .infinity)
                .border(.red)
                .overlay {
                    GeometryReader { geom in
                        Text("\(geom.size.width) - \(geom.size.height)")
                    }
                }
            Text("world")
                .frame(height: 300).frame(maxWidth: .infinity)
                .border(.blue)
                .overlay{
                    GeometryReader { geom in
                        Text("\(geom.size.width) - \(geom.size.height)")
                    }
                }
        }
        .frame(height: 500)
        .border(.orange, width: 4)
//        .clipped()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

何が起きているのか?

# 厳密な意味での振る舞いは、開示されていないので、Apple しかわかりません。以下、推測です。

2つの Text はそれぞれ 高さ 300 と指定されています。ですので、VStack としては、それを縦に積み重ねて 600 の高さを持つようなレイアウトにして、上位のビューに回答しているように見えます。

以下は、上記に至るまでの考えです。

Text は、上位ビューからのおすすめ高さを無視して、自分に必要な領域を返します。今回のケースでは、300 を使うと親ビューにも返し、実際 300 の高さを使用します。
このことは、以前に説明しました。
SwiftUI2021[SwiftUI] SwiftUI の layout システムを理解する (基礎編: レイアウトシーケンス)

以下の記事で確認したように、子ビューとして pull-in タイプのビューのみを持つ VStack は、pull-in 的なレイアウトになっていました。
SwiftUI2021[SwiftUI] SwiftUI の layout システムを理解する (コンテナビューに含まれる push-out / pull-in タイプビューの調停)

これらの点を合わせて考えると、VStack も Text と同様の振る舞いをして、自分が必要と考える領域を使用していると推測できます。

具体的な振る舞いとしては VStack は、上位ビューからのおすすめ高さは無視して、下位ビューが必要とする 300 + 300 = 600 の高さを使用するということです。

# .clipped 指定されるとはみ出た部分は削除されますが、それは、親ビュー側の操作です。

固定高さと可変高さを持つ子要素を持つ VStack のレイアウト

1つの固定高さ(600)を持つビューと、push-out タイプのビューで試してみます。


VStack {
  Text("Hello").frame(height:600)
  Color.red
}.frame(height:500)

以下のような結果となり、VStack からはみ出す Text が配置され、Color.red は配置されていないことがわかります。

TextWillThrowAwayColor

実際に動かしたコードは以下です。


//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2021/12/10
//  © 2021  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack(spacing:0) {
            Text("Hello")
                .frame(height: 600).frame(maxWidth: .infinity)
                .border(.red)
                .overlay {
                    GeometryReader { geom in
                        Text("\(geom.size.width) - \(geom.size.height)")
                    }
                }
            Color.red
        }
        .frame(height: 500)
        .border(.orange, width: 4)
    }
}

何が起きているのか?

結果から推測するに以下のようなことになっている気がします。

  1. VStack の子ビューを確認する
  2. 最初の子ビュー Text の高さを確認すると 600、もう一つのビューは、Color で push-out タイプ
  3. VStack は高さ 500 と指定されている
  4. Text を配置するとすでに 500 を超えて 600 になるので、Color は、レイアウトしない

若干の考察

Apple のドキュメントにはざっくりしか記述されていないので、推測ばかりです。

ですが、レイアウトシステムをできるだけシンプルにしようとすると、これまでの推測が 大きく外れてはいない気がします。(気がするだけですが)

まとめ:layout システムを理解する (領域が不足するケースの調停)

layout システムを理解する (領域が不足するケースの調停)
  • 領域が不足すると、push-out タイプのビューがレイアウトされなくなる
  • 領域が不足しても、pull-in タイプのビューは、不足に構わずレイアウトされる
  • 領域が不足しているケースで push-out/pull-in 両方のタイプのビューがあると、まずは、push-out タイプがレイアウトされなくなる

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

SwiftUI 学習におすすめの本

SwiftUI 徹底入門

SwiftUI は、グラフィカルなライブラリということもあり、文字だけのテキストよりは、画像が多く入れられた書籍を読むと理解が進みやすいです。

自分で購入した中でおすすめできるものとしては、以下のものです。

2019 年発表の SwiftUI 1.0 相当を対象にしているので、2020/2021 に追加された一部の機能は、説明されていません。

ですが、SwiftUI 入門書としては、非常によくできていますし、わかりやすいです。 この本で学習した後に、追加分を学習しても良いと思います。

SwiftUIViewsMastery

英語での説明になってしまいますが、以下の本もおすすめです。

1ページに、コードと画面が並んでいるので、非常にわかりやすいです。

View に適用できる modifier もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。

超便利です

SwiftUIViewsMastery

販売元のページは、こちらです。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です