[SwiftUI] Grid の使い方

SwiftUI2021

     
⌛️ 6 min.
SwiftUI での Excel 的なレイアウトの方法を説明します。

環境&対象

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

  • macOS Sonoma Beta 5
  • Xcode 15 Beta 6
  • iOS 17 Beta 4
  • Swift 5.9

Excel 的レイアウトとは

Excel がワープロ代わりに 使われているケースをたまにみます。

行の高さや 列の幅を 配置したいものに合わせて変更するのではなく、一定の大きさを持つ複数のセルを結合して欲しい大きさを作り出します。

ちょうど、方眼紙の罫線にそって サイズを決めて配置していくイメージです。

このようにすることで、文章中の要素を 上辺や左辺で揃えることが簡単にできます。

以下の様な感じ(?) のレイアウトを簡単に作成できます。
・タイトルは、用紙全体の中央に
・項目は、高さ2を持つ
・詳細は、高さ1を持ち、項目よりも少し右にレイアウトする

具体的に作ってみると 以下の様なイメージです。

Excel
MEMO

好みのレイアウトでパッと作るのは簡単です。
ですが、実際に運用し始めると文章の変更によってセルの再結合が必要になったりしし始めて いろいろと不便な点が見えてきます。

SwiftUI で Excel 的レイアウト

SwiftUI でアプリの UI を作る時にも同様に 方眼紙のマス目に沿った配置をしたいケースがあります。

VStack/HStack

最初に、SwiftUI でよく使用されている VStack/HStack を使って Excel 的に配置することを考えてみます。

SwiftUI では VStack を使うと 高さ方向にそって並べることができ、HStack を使うと幅方向に並べることができます。

VStack と HStack は組み合わせて使うこともできますので、例えば、VStack の中に HStack を入れて レイアウトすることも可能です。

うまくいきそうなのですが、問題があります。
VStack の中に入れられている 複数の HStack は、それぞれ 独立して要素を幅方向に並べてしまいます。
ですので、 Excel での配置のように 方眼紙の罫線に沿って並べるというような配置はできません。

VStackHStack_iOS
VStackHStack_macOS

使用コード

//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2023/08/11
//  © 2023  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            HStack {
                Text("Hello")
                    .padding(2)
                    .border(.red)
                Text("World")
                    .padding(2)
                    .border(.red)
            }
            .padding(2)
            .border(.yellow)

            HStack {
                Text("こんにちわ")
                    .padding(2)
                    .border(.red)
                Text("世界")
                    .padding(2)
                    .border(.red)
            }
            .padding(2)
            .border(.yellow)
        }
        .padding(2)
        .border(.green)
}

#Preview {
    ContentView()
}

配置をわかりやすくするために、それぞれに border 指定しています。

方眼紙を作る

方眼紙に沿ってのレイアウトは、SwiftUI では、 Grid を使うことで実現できます。

Apple のドキュメントは、こちら

この Grid を使って、最初に 方眼紙的なビューを作ってみます。

なお Grid は、子要素(行要素)に GridRow を持ちます。

Apple のドキュメントは、こちら

MEMO

Grid と GridRow の関係は、この記事の最初の例に登場した VStack と HStack の関係と同じです。

VStack と HStack は、どちらを親としてもレイアウトできますが、Grid は、横方向に配置される GridRow を常に子要素として持ちます。

Grid/GridRow を使用して 正方形を並べてみます。

具体的には、Grid を使い、内部に 4つの GridRow、GridRow の中には 2つづつの 正方形を入れてみます。

//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2023/08/11
//  © 2023  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    var body: some View {
        Grid {
            GridRow{
                square(.yellow)
                square(.yellow)
            }
            GridRow{
                square(.yellow)
                square(.yellow)
            }
            GridRow{
                square(.yellow)
                square(.yellow)
            }
            GridRow{
                square(.yellow)
                square(.yellow)
            }
        }
        .padding()
    }

    func square(_ color: Color) -> some View {
        Rectangle()
            .fill(color)
            .frame(width: 100, height: 100)
    }
}

#Preview {
    ContentView()
}
2x4_iOS
2x4_macOS

# デフォルトでは 表示要素間に スペース が入っていますが、設定でなくすこともできます。

HelloWorld を配置してみる

Grid がそれっぽい配置であることを確認したので、先ほどの Hello world をレイアウトしてみます。

使用コード

//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2023/08/11
//  © 2023  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    var body: some View {
        Grid {
            GridRow{
                Text("Hello")
                    .padding(2)
                    .border(.red)
                Text("World")
                    .padding(2)
                    .border(.red)
            }
            .padding(2)
            .border(.yellow)
            GridRow{
                Text("こんにちわ")
                    .padding(2)
                    .border(.red)
                Text("世界")
                    .padding(2)
                    .border(.red)
            }
            .padding(2)
            .border(.yellow)
        }
        .padding(2)
        .border(.green)
        .padding()
    }
}

#Preview {
    ContentView()
}

以下の様なレイアウトになります。

HellWorld_Grid_ iOS
HellWorld_Grid_macOS

揃っていない様にも見えますが、SwiftUI の Text の配置がデフォルトでは、center であることを考えると、揃えられていることがわかるかと思います。

HellWorld_Grid_guide

セルの中での配置位置

セルの与えられた領域の中での配置位置を調整する方法も用意されています。

Grid 単位での指定と GridRow 単位 そして Grid の Cell 単位での指定が用意されています。
Grid 単位での指定は、高さ方向・幅方向どちらの配置も制御でき、Grid の init で指定します。

Grid の init

init(
    alignment: Alignment = .center,
    horizontalSpacing: CGFloat? = nil,
    verticalSpacing: CGFloat? = nil,
    @ViewBuilder content: () -> Content
)

1つ目の引数 alignment がその指定です。
Apple のドキュメントは、こちら
デフォルトは center で .top 等様々な指定が用意されています。
Apple のドキュメントは、こちら

GridRow 単位では、高さ方向の配置を、init で指定できます。

GridRow の init

init(
    alignment: VerticalAlignment? = nil,
    @ViewBuilder content: () -> Content
)

Grid のセル要素の配置を個別で指定する方法も用意されています。.gridCellAnchor を使います。Grid のセル単位でも 高さ方向・幅方向いずれもより柔軟に指定できます。
Apple のドキュメントは、こちら

Grid を Column 単位で配置指定

これまでみてきた様に Grid は 各行の要素を 高さ方向に積み重ねて作るレイアウトです。
そのため、行単位での指定は自然にできますが、列単位では難しいです。

ですが Grid で表を作る時には、行ごとに 配置を揃えたい時も多くあります。
「1行目は名前なので 行頭揃え にして、2行目は金額なので 行末揃えに」という感じです。

そのため 列単位で配置が、.gridColumnAlignment を使用することで可能です。
この指定では、幅方向の配置を指定できます。

Apple のドキュメントは、こちら

使ってみる

配置指定の方法がいろいろと出てきたので、使ってみて、どうなるか見てみましょう。

gridCellAnchor を使用した指定

Hello World の例に、1行 金額を表示する行を追加して、文字列は行頭揃え 金額は行末揃えにしてみます。

行頭揃えは、Grid 全体に適用して、金額のセルだけ gridCellAnchor を使用して個別指定しています。

//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2023/08/11
//  © 2023  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    var body: some View {
        Grid(alignment: .leading) {
            GridRow{
                Text("Hello")
                    .padding(2)
                    .border(.red)
                Text("World")
                    .padding(2)
                    .border(.red)
            }
            .padding(2)
            .border(.yellow)
            GridRow{
                Text("こんにちわ")
                    .padding(2)
                    .border(.red)
                Text("世界")
                    .padding(2)
                    .border(.red)
            }
            .padding(2)
            .border(.yellow)
            GridRow{
                Text("¥2,000")
                    .gridCellAnchor(.trailing)
                    .padding(2)
                    .border(.red)
                Text("$500")
                    .gridCellAnchor(.trailing)
                    .padding(2)
                    .border(.red)
            }
            .padding(2)
            .border(.yellow)
        }
        .padding(2)
        .border(.green)
        .padding()
    }
}

#Preview {
    ContentView()
}

以下の様な表示になります。

example1_iOS
example1_macOS

セルの幅は 同一列で、高さは 同一行の最大のセルに合わせられるため、最大幅を持つセルの alignment がわかりにくいですが、それぞれ 行頭揃え・行末揃えになっていることがわかります。

gridColumnAlignmentを使用した指定

次に、最初の例に追加で行を加えて、金額表示してみます。

gridColumnAlignment を使用して、金額表示の行を行末揃えにします。

gridColumnAlingment は行単位での指定なので、該当列のセル全てに付与する必要はありません。

//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2023/08/11
//  © 2023  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    var body: some View {
        Grid(alignment: .leading) {
            GridRow{
                Text("Hello")
                    .padding(2)
                    .border(.red)
                Text("World")
                    .padding(2)
                    .border(.red)
                Text("¥2,000")
                    .gridColumnAlignment(.trailing)
                    .padding(2)
                    .border(.red)
            }
            .padding(2)
            .border(.yellow)
            GridRow{
                Text("こんにちわ")
                    .padding(2)
                    .border(.red)
                Text("世界")
                    .padding(2)
                    .border(.red)
                Text("$500")
                    .padding(2)
                    .border(.red)
            }
            .padding(2)
            .border(.yellow)
        }
        .padding(2)
        .border(.green)
        .padding()
    }
}

#Preview {
    ContentView()
}

以下の様な表示になります。

example2_iOS
example2_macOS

セルをマージする

Excel 的にレイアウトするときには、セルのマージもポイントになります。

Grid レイアウトでも Excel のセルマージ相当にレイアウトすることが可能です。

幅方向のセルをマージする

幅方向のセルをマージするには、gridCellColumns を使用します。

Apple のドキュメントは、こちら

gridCellColumn には Int を指定しますが、指定した数分の 列を占有するようになります。

以下では、”こんにちわ 世界” を1セルに表示する様にしてみました。
(マージされたセルも Grid で指定されている .leading 配置になっています)

//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2023/08/11
//  © 2023  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    var body: some View {
        Grid(alignment: .leading) {
            GridRow{
                Text("Hello")
                    .padding(2)
                    .border(.red)
                Text("World")
                    .padding(2)
                    .border(.red)
                Text("¥2,000")
                    .gridColumnAlignment(.trailing)
                    .padding(2)
                    .border(.red)
            }
            .padding(2)
            .border(.yellow)
            GridRow{
                Text("こんにちわ 世界")
                    .gridCellColumns(2)
                    .padding(2)
                    .border(.red)
                Text("$500")
                    .padding(2)
                    .border(.red)
            }
            .padding(2)
            .border(.yellow)
        }
        .padding(2)
        .border(.green)
        .padding()
    }
}

#Preview {
    ContentView()
}

高さ方向のセルをマージする

用意されていません。
別途工夫が必要です・・・

セルの大きさを制御する

SwiftUI の ビューには、できるだけ広がろうとするビューと 必要以上に広がらないビューがあります。

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

特に、push-out タイプのビューを セルとして配置すると、大きく広がってしまいます。

元々セルは、同じ列、同じ行のセルとサイズを合わせようと調整されますから セルは大きくなりがちなのですが、一部に push-out タイプのビューを配置すると、セルはどんどん(?) 大きくなっていってしまいます。

push-out タイプの代表の Color を配置してみます。

example4_iOS
example4_macOS
//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2023/08/11
//  © 2023  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    var body: some View {
        Grid(alignment: .leading) {
            GridRow{
                Text("Hello")
                    .padding(2)
                    .border(.red)
                Text("World")
                    .padding(2)
                    .border(.red)
                Text("¥2,000")
                    .gridColumnAlignment(.trailing)
                    .padding(2)
                    .border(.red)
                Text("Color")
            }
            .padding(2)
            .border(.yellow)
            GridRow{
                Text("こんにちわ 世界")
                    .gridCellColumns(2)
                    .padding(2)
                    .border(.red)
                Text("$500")
                    .padding(2)
                    .border(.red)
                Color.yellow
            }
            .padding(2)
            .border(.yellow)
        }
        .padding(2)
        .border(.green)
        .padding()
    }
}

#Preview {
    ContentView()
}

Color は、与えられた領域いっぱいに広がろうとするため、与えられた高さを埋める様に大きくなります。

MEMO

このとき、幅も領域いっぱいに広がりそうなものなのですが、実際には 適度な幅に設定されています。
ドキュメントを確認した範囲では その辺りの調整については言及されていませんでした。

push-out ビューを抑制する

ケースによっては、このように貪欲に領域を獲得しようとする振る舞いを制御したくなります。そのために gridCellUnsizedAxes という View Modifier が用意されています。

Apple のドキュメントは、こちら

この View Modifier で指定した軸方向には、push-out することが抑制されます。

さきほどの Color は、高さ方向に push-out していましたので、.gridCellUnsizedAxes に .vertical 指定することで、高さ方向に push-out することを抑制できます。

ALTTEXT
ALTTEXT

以下は使用したコードです。

//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2023/08/11
//  © 2023  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    var body: some View {
        Grid(alignment: .leading) {
            GridRow{
                Text("Hello")
                    .padding(2)
                    .border(.red)
                Text("World")
                    .padding(2)
                    .border(.red)
                Text("¥2,000")
                    .gridColumnAlignment(.trailing)
                    .padding(2)
                    .border(.red)
                Text("Color")
            }
            .padding(2)
            GridRow{
                Text("こんにちわ 世界")
                    .gridCellColumns(2)
                    .padding(2)
                    .border(.red)
                Text("$500")
                    .padding(2)
                    .border(.red)
                Color.yellow
                    .gridCellUnsizedAxes(.vertical)
            }
            .padding(2)
        }
        .padding(2)
        .border(.green)
        .padding()
    }
}

#Preview {
    ContentView()
}
//
//  ContentView.swift
//
//  Created by : Tomoaki Yagishita on 2023/08/11
//  © 2023  SmallDeskSoftware
//

import SwiftUI

struct ContentView: View {
    var body: some View {
        Grid(alignment: .leading) {
            GridRow{
                Text("Hello")
                    .padding(2)
                    .border(.red)
                Text("World")
                    .padding(2)
                    .border(.red)
                Text("¥2,000")
                    .gridColumnAlignment(.trailing)
                    .padding(2)
                    .border(.red)
                Text("Color")
            }
            .padding(2)
            GridRow{
                Text("こんにちわ 世界")
                    .gridCellColumns(2)
                    .padding(2)
                    .border(.red)
                Text("$500")
                    .padding(2)
                    .border(.red)
                Color.yellow
                    .gridCellUnsizedAxes(.vertical)
            }
            .padding(2)
        }
        .padding(2)
        .border(.green)
        .padding()
    }
}

#Preview {
    ContentView()
}

まとめ

Grid レイアウトの使い方を説明しました。

Grid レイアウトの使い方
  • Grid を使うと、行単位で整列したレイアウトを作りやすい
  • Grid で 列単位で alignment を揃えることもできる
  • Grid では、幅方向に 複数列を占めるセルを作ることもできる
  • Grid のセルに push-out ビューを配置しても 制御することもできる

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

SwiftUI おすすめ本

SwiftUI を理解するには、以下の本がおすすめです。

SwiftUI ViewMatery

SwiftUI で開発していくときに、ViewやLayoutのための適切なmodifierを探すのが大変です。
英語での説明になってしまいますが、以下の”SwiftUI Views Mastery Bundle”という本がビジュアル的に確認して探せるので、便利です。

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

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

超便利です

SwiftUIViewsMastery

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

SwiftUI 徹底入門

# SwiftUI は、毎年大きく改善されていますので、少し古くなってしまいましたが、いまでも 定番本です。

Swift学習におすすめの本

詳解Swift

Swift の学習には、詳解 Swift という書籍が、おすすめです。

著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。

最新版を購入するのがおすすめです。

現時点では、上記の Swift 5 に対応した第5版が最新版です。

コメントを残す

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