🔭

SwiftUIのframeのminWidth maxWidth観測隊

2023/04/07に公開

はじめに

この記事はSwiftUIの frame モディファイアの minWidth maxWidth の挙動を観測するものである。


最後にはこれが読めるようになります

frameモディファイアのminWidth maxWidth

frameモディファイアのminWidth maxWidthは以下のように使う。

Rectangle()
    .frame(minWidth: 150, maxWidth: 250)

登場人物

  • 親View : 外側の囲んでいるViewである。親ViewとしてVStackを使用する。widthの設定をする。
  • 子View : 子Viewは2タイプ観測する。
    • 「本来サイズ」(正式名はintrinsic content size)を持つText
    • 持たないRectangle
  • 子Viewのframeモディファイア : 今回の問題児である。

親VStack、子Rectangleのとき

//例
VStack {
    Rectangle()
        .frame(maxWidth: 100) //ここをmaxやminにする
}
.frame(width: 200)

Rectangleは「本来サイズ」がない(*)ので、登場人物は

  • 親ビューのサイズ
  • Rectangleのframeモディファイア

の2つである。結果はこのようになる。 採用された値を()で囲んである。

  • min < (親)
  • 親 < (min)
  • (max) < 親
  • (親) < max
  • min < (max) < 親
  • min < (親) < max
  • 親 < (min) < max

(*)Rectangleに「本来サイズ」はあるかもしれないが今回の調査の観点で見ると「ない」。

スクリーンショットとコード

VStack {
    
    //ものさし
    HStack {
        Rectangle()
            .frame(height: 5)
        Text("200")
        Rectangle()
            .frame(height: 5)
    }
    .frame(width: 200)
    
    //min調査
    VStack {
        Rectangle()
            .frame(minWidth: 100)//min < (親)
        Rectangle()
            .frame(minWidth: 300)//親 < (min)
    }
    .frame(width: 200)
    
    //max調査
    VStack {
        Rectangle()
            .frame(maxWidth: 100)//(max) < 親
        Rectangle()
            .frame(maxWidth: 300)//(親) < max
    }
    .frame(width: 200)
    
    //ものさし
    HStack {
        Rectangle()
            .frame(height: 5)
        Text("200")
        Rectangle()
            .frame(height: 5)
    }
    .frame(width: 200)
     
    //min & max調査
    VStack {
        Rectangle()
            .frame(minWidth: 100, maxWidth: 150)//min < (max) < 親
        Rectangle()
            .frame(minWidth: 150, maxWidth: 250)//min < (親) < max
        Rectangle()
            .frame(minWidth: 250, maxWidth: 300)//親 < (min) < max
    }
    .frame(width: 200)
}
.frame(height: 300)//縦を制限するのは単純に見やすいから

このように 親からの値をmin maxの範囲に収める挙動 である。親の値がmin maxの範囲に無いならminとmaxのうち、はみ出した側の値が採用される。単純でわかりやすい。親から200を渡し、minを250にすると、決定する値は250なのでそのときは親の枠をはみ出るレイアウトになる。

ちなみに気になる人がいるかもしれないので、min maxの指令をしないRectangleの挙動を載せる。

min maxの指令をしないRectangleの挙動

VStack {
    //ものさし
    HStack {
        Rectangle()
            .frame(height: 5)
        Text("200")
        Rectangle()
            .frame(height: 5)
    }
    .frame(width: 200)
    
    Rectangle()//なにもしなければ横幅いっぱい
    VStack {
        Rectangle()//上から200が来たので200
    }
    .frame(width: 200)
}
.frame(height: 100)//縦を制限するのは単純に見やすいから

親VStack、子Textのとき

今度は子をTextにして観測する。

//例
VStack(alignment: .leading) {
    MyText(str: "12345")
        .frame(minWidth: 100.0)
}
.frame(width: 200.0)

Textは「本来サイズ」がある。これは制限なく健やかに表示した時のサイズである。

準備

本来サイズを測りやすくするために、一文字の横幅が10になるようなフォントを探す。

横幅が10になるようなフォントのスクリーンショットとコード
struct MyText: View {
    let str: String
    var body: some View {
        Text(str)
            .font(.system(size: 16, design: .monospaced))
            .lineLimit(1)
    }
}

上のようにすると一文字がだいたい10になる。尚、観測は50ぐらいの余裕を持って行うのでそんなに厳密じゃなくてもよい。

 HStack {
     Rectangle()
         .frame(height: 5)
     Text("200")
     Rectangle()
         .frame(height: 5)
 }
 .frame(width: 200)
 
 VStack(alignment: .leading, spacing: 10) {
     MyText(str: "1234567890123456789")
     MyText(str: "12345678901234567890")
     MyText(str: "123456789012345678901")
     MyDot()
 }
 .frame(width: 200.0)

19文字で200に足らず、
20文字にするときっちり表示して
21文字にすると範囲を超えて点々が表示している
のを確認していただきたい。

小さい点は何?

VStack内に左詰めで表示する。左端がわかりやすいように点を置く。

これは1の左に空白があるときに役に立つ。

では観測を始める。

親サイズと本来サイズで観察

まずはTextに .frame を使わず親サイズと本来サイズの関係の復習である。さきほどの文字サイズ決定のところでも見たが、 決定値を()で表すと

  • (親) < 本来サイズ
  • (本来サイズ) < 親サイズ

である。小さい方が採用される単純なアルゴリズム。
この先も()によって決定値を表現する。

本来サイズとminで観察

本来サイズをmin指令の関係を観測する。

  • 本来サイズ < (min)
  • min < (本来サイズ)

minよりも下のものは許されない挙動。

スクリーンショットとコード

VStack(alignment: .leading, spacing: 10) {
    MyText(str: "123456789012345")
        .frame(minWidth: 200.0)
    MyText(str: "1234567890123456789012345")
        .frame(minWidth: 200.0)
    MyDot()
}

本来サイズとmaxで観察

本来サイズをmax指令の関係を観測する。

  • 本来サイズ < (max)
  • (max) < 本来サイズ

となり、maxより上のものは許されないのだが、 maxより下の場合もmaxまで引き伸ばされる 。maxを「超えた場合に制限するもの」と解釈するとここで間違えることになる。

スクリーンショットとコード

VStack(alignment: .leading, spacing: 10) {
    MyText(str: "123456789012345")
        .frame(maxWidth: 200.0)
    MyText(str: "1234567890123456789012345")
        .frame(maxWidth: 200.0)
    MyDot()
}

本来サイズとminとmaxで観察

  • 本来サイズ < min < (max)
  • min < 本来サイズ < (max)
  • min < (max) < 本来サイズ

こちらも 本来サイズがmaxより小さいときでもmaxまで引き伸ばされる

スクリーンショットとコード

VStack(alignment: .leading, spacing: 10) {
    MyText(str: "12345")
        .frame(minWidth: 100.0, maxWidth: 200.0)
    MyText(str: "123456789012345")
        .frame(minWidth: 100.0, maxWidth: 200.0)
    MyText(str: "1234567890123456789012345")
        .frame(minWidth: 100.0, maxWidth: 200.0)
    MyDot()
}

親サイズと本来サイズとminで観察

  • 本来 < 親 < (min)
  • 親 < 本来 < (min)
  • 親 < (min) < 本来
  • 本来 < (min) < 親
  • min < (本来) < 親
  • min < (親) < 本来

親サイズと本来サイズの小さい方が候補になるが、それがminより小さいならminが採用される。

スクリーンショットとコード

VStack(alignment: .leading, spacing: 10) {
    MyText(str: "12345")
        .frame(minWidth: 200.0)//本来 < 親 < (min)
    MyText(str: "123456789012345")
        .frame(minWidth: 200.0)//親 < 本来 < (min)
    MyText(str: "1234567890123456789012345")
        .frame(minWidth: 200.0)//親 < (min) < 本来
    MyDot()
}
.frame(width: 100.0)

VStack(alignment: .leading, spacing: 10) {
    MyText(str: "12345")
        .frame(minWidth: 100.0)//本来 < (min) < 親
    MyText(str: "123456789012345")
        .frame(minWidth: 100.0)//min < (本来) < 親
    MyText(str: "1234567890123456789012345")
        .frame(minWidth: 100.0)//min < (親) < 本来
    MyDot()
}
.frame(width: 200.0)

親サイズと本来サイズとmaxで観察

  • 本来 < (親) < max
  • (親) < 本来 < max
  • (親) < max < 本来
  • 本来 < (max) < 親
  • (max) < 本来 < 親
  • (max) < 親 < 本来

親サイズとmaxの小さい方が採用。ここでもまた、 本来サイズが一番小さくても本来サイズが採用されずに引き伸ばされる

スクリーンショットとコード

VStack(alignment: .leading, spacing: 10) {
    MyText(str: "12345")
        .frame(maxWidth: 200.0)//本来 < (親) < max
    MyText(str: "123456789012345")
        .frame(maxWidth: 200.0)//(親) < 本来 < max
    MyText(str: "1234567890123456789012345")
        .frame(maxWidth: 200.0)//(親) < max < 本来
    MyDot()
}
.frame(width: 100.0)
VStack(alignment: .leading, spacing: 10) {
    MyText(str: "12345")
        .frame(maxWidth: 100.0)//本来 < (max) < 親
    MyText(str: "123456789012345")
        .frame(maxWidth: 100.0)//(max) < 本来 < 親
    MyText(str: "1234567890123456789012345")
        .frame(maxWidth: 100.0)//(max) < 親 < 本来
    MyDot()
}
.frame(width: 200.0)

親サイズと本来サイズとminとmaxで観察

登場人物が4つになった。4つの大小関係は24通り。ただminとmaxの大小関係は常識的にmin < maxなので12通り。

12通りを2つに分ける。大小で並べた時に

  • 本来サイズと親サイズが隣にある
  • 本来サイズと親サイズが離れている
本来サイズと親サイズが隣にある
  • min < (max) < 本来 < 親
  • min < (max) < 親 < 本来
  • min < 本来 < (親) < max
  • min < (親) < 本来 < max
  • 本来 < 親 < (min) < max
  • 親 < 本来 < (min) < max

本来サイズと親サイズの両方がmin-maxの範囲から外れた時はmin maxのうち、近い方が採用。
本来サイズと親サイズの両方がmin-maxの範囲内なら小さい方だが、本来サイズの方が小さくても「maxによって本来サイズが採用されずに引き伸ばされる」の法則によって親サイズに決定。

スクリーンショットとコード

VStack(alignment: .leading, spacing: 10) {
    MyText(str: "123456789012345")
        .frame(minWidth: 50.0, maxWidth: 100.0)//min < (max) < 本来 < 親
    MyText(str: "123456789012345")
        .frame(minWidth: 100.0, maxWidth: 250.0)//min < 本来 < (親) < max
    MyText(str: "123456789012345")
        .frame(minWidth: 250.0, maxWidth: 300.0)//本来 < 親 < (min) < max
    MyDot()
}
.frame(width: 200.0)
VStack(alignment: .leading, spacing: 10) {
    MyText(str: "12345678901234567890")
        .frame(minWidth: 50.0, maxWidth: 100.0)//min < (max) < 親 < 本来
    MyText(str: "12345678901234567890")
        .frame(minWidth: 100.0, maxWidth: 250.0)//min < (親) < 本来 < max
    MyText(str: "12345678901234567890")
        .frame(minWidth: 250.0, maxWidth: 300.0)//親 < 本来 < (min) < max
    MyDot()
}
.frame(width: 150.0)
本来サイズと親サイズが隣にない

観測は大変であるが諦めるわけにはいかない。なぜなら我々は観測隊だからである。

  • min < 本来 < (max) < 親
  • 本来 < min < (max) < 親
  • 本来 < min < (親) < max
  • min < (親) < max < 本来
  • 親 < (min) < max < 本来
  • 親 < (min) < 本来 < max

ここでも「maxによって本来サイズが採用されずに引き伸ばされる」の法則が暴れている

スクリーンショットとコード

VStack(alignment: .leading, spacing: 10) {
    MyText(str: "1234567890")
        .frame(minWidth: 50.0, maxWidth: 150.0)//min < 本来 < (max) < 親
    MyText(str: "1234567890")
        .frame(minWidth: 150.0, maxWidth: 200.0)//本来 < min < (max) < 親
    MyText(str: "1234567890")
        .frame(minWidth: 200.0, maxWidth: 300.0)//本来 < min < (親) < max
    MyDot()
}
.frame(width: 250.0)
VStack(alignment: .leading, spacing: 10) {
    MyText(str: "1234567890123456789012345")
        .frame(minWidth: 50.0, maxWidth: 150.0)//min < (親) < max < 本来
    MyText(str: "1234567890123456789012345")
        .frame(minWidth: 150.0, maxWidth: 200.0)//親 < (min) < max < 本来
    MyText(str: "1234567890123456789012345")
        .frame(minWidth: 200.0, maxWidth: 300.0)//親 < (min) < 本来 < max
    MyDot()
}
.frame(width: 100.0)

結果をわかりやすく並べる

本来 < (min)
親 < 本来 < (min)
本来 < 親 < (min)
本来 < (min) < 親

min < (本来)
親 < (min) < 本来
min < (親) < 本来
min < (本来) < 親

本来 < (max)
(親) < 本来 < max
本来 < (親) < max
本来 < (max) < 親

(max) < 本来
(親) < max < 本来
(max) < 親 < 本来
(max) < 本来 < 親

本来 < min < (max)
親 < 本来 < (min) < max
本来 < 親 < (min) < max
本来 < min < (親) < max
本来 < min < (max) < 親

min < 本来 < (max)
親 < (min) < 本来 < max
min < (親) < 本来 < max
min < 本来 < (親) < max
min < 本来 < (max) < 親

min < (max) < 本来
親 < (min) < max < 本来
min < (親) < max < 本来
min < (max) < 親 < 本来
min < (max) < 本来 < 親

どこがわかりやすいんだと思うだろうがこの下の結論を読んだ後で改めて見てほしい。

結論

法則が「maxは上の許可を削る、minは下の許可を削る」のみであればよかったのだが、「max指令によって本来サイズが採用されずに引き伸ばされる」の法則によって非常に難解になった。すべてを説明する単純な法則はないか私なりに考えてみたが、今の所の結論はこうである。

本来サイズを持つものでもframeモディファイアのminやmaxが適用されることによって「min-maxの情報を持つもの」になる。多くの場合、本来サイズ情報を失う(適用されない)。そのmin-max情報に親Viewによる制約が考慮されて最終的な値が決定する。

次の図は

  • 本来サイズ
  • frameのminやmax

によってできる「min-maxの情報を持つもの」を緑で表している。それぞれのものは左が小側、右が大側となっている。

minだけ指令して、本来 < minのときは、min = maxという範囲(というか点)になる。

maxだけ指令した時は、そのmax値以下が範囲になる。おそらくminは0だろうが、minが0であることがわかるように上のように表記してみた。

次の段階に進む。次の図は

  • 「min-maxの情報を持つもの」
  • 親サイズ

により決定される値を表している。

最後の調査

さて、親のVStackのwidthを設定しない
min < 本来 < (max)
などのタイプについてさらに調査したい。
これは「min maxの範囲の中で出来るだけ大きくなる」という法則に見えるが、これを囲んでいる画面の大きさが渡っている考えると、さきほどの結論で出した「min maxの範囲の中で出来るだけ親ビューの提案を満たす」法則に当てはまりそうである。
ということで画面の大きさが渡っているかどうかを調査する。maxに画面の幅より大きい値を入れて、max値ではなく画面の大きさが採用されれば画面の大きさが渡っていそうである。

maxにとんでもなく大きな値を入れても画面の左端に点があるので、frameによって作成された枠の幅は画面の大きさである。画面の大きさが渡っていると考えてよさそうである。

そのように考えると、次のものも

本来 < (max)
本来 < min < (max)
min < 本来 < (max)
(それぞれのmaxは画面の幅より小さい)

次のように考えると結論で上げた法則に当てはまる。

本来 < (max) < 画面の横幅
本来 < min < (max) < 画面の横幅
min < 本来 < (max) < 画面の横幅


検索用ワード
minHeight maxHeight

Discussion