🍩

Offset から理解する SwiftUI の Alignment Guide

2023/09/11に公開

Alignment Guide

View 同士の相対的な位置を決めるための基準

VStack で見る Alignment Guide

Alignment Guide は身近なところでは VStack, HStack, ZStack の Initializer に登場する

// VStack
init(
    alignment: HorizontalAlignment = .center,
    spacing: CGFloat? = nil,
    @ViewBuilder content: () -> Content
)

VStack では、子 View を配置する際に各 View の x 軸方向の位置を決めるために HorizontalAlignment を指定する必要がある(default は center)
各 Alignment の結果は以下のようになる

leading center trailing

Stack 内の子 View の終端を揃えようとした場合、以下の 2 つのコードは同じ結果となる

VStack(alignment: .trailing, spacing: 0) {
    Rectangle()
        .frame(width: 200, height: 100)
        .foregroundStyle(.yellow)
    Rectangle()
        .frame(width: 300, height: 100)
        .foregroundStyle(.green)
}
.border(.gray)
VStack(alignment: .trailing, spacing: 0) {
    Rectangle()
        .frame(width: 200, height: 100)
        .foregroundStyle(.yellow)
        .alignmentGuide(.trailing) { _ in 200 } // or .alignmentGuide(.trailing) { d in d[.trailing] }
    Rectangle()
        .frame(width: 300, height: 100)
        .foregroundStyle(.green)
        .alignmentGuide(.trailing) { _ in 300 } // or .alignmentGuide(.trailing) { d in d[.trailing] }
}
.border(.gray)

2 つめのコードでは、 Alignment を調節するための alignmentGuide(_:computeValue:) を利用している
第 1 引数は基準となる Alignment 、第 2 引数は offset を計算するための closure を受け取る

※ Stack に指定する Alignment と、第 1 引数に渡す Alignment が異なる場合、ここで設定した Alignment Guide は無効となる
※ ここでの offset は表示位置をズラすための offset とは異なる(例: offset(x:y:)

func alignmentGuide(
    _ g: VerticalAlignment,
    computeValue: @escaping (ViewDimensions) -> CGFloat
) -> some View

offset を図で表すと以下のようになる

つまり、 offset が 0 の場合は先端、 width / 2 の場合は中央、 width の場合は終端が揃うことになる

0 width / 2 width

VStack の Initializor で HorizontalAlignment を指定したとき、暗黙的に子 View に対して offset が設定されていると考えた方が分かりやすいかもしれない

Alignment Guide の調節

基本的には事前に定義されている以下の Alignment Guide を利用すれば良いが、一部の View のみズラして表示したい場合などは、先ほども登場した alignmentGuide(_:computeValue:) を利用する

第 2 引数では offset を計算するための closure を渡す必要あるが、Closure には ViewDimensions が渡される

var height: CGFloat
var width: CGFloat
subscript(VerticalAlignment) -> CGFloat
subscript(HorizontalAlignment) -> CGFloat
subscript(explicit _: VerticalAlignment) -> CGFloat?
subscript(explicit _: HorizontalAlignment) -> CGFloat?

ViewDimensions から、対象となる View の width, height や VerticalAlignment, HorizontalAlignment に対応する offset を取り出すことができる

.alignmentGuide(.trailing) { d in d.width * 0.5 }
.alignmentGuide(.trailing) { d in d[.trailing] }

HorizontalAlignment を対象とする場合、基本的には d[.leading] = 0, d[.center] = width / 2, d[.trailing] = width となる
VerticalAlignment の場合は、 d[.top] = 0, d[.center] = height / 2, d[.bottom] = height である

例: 3 段のテキストを斜めに並べる

VStack(alignment: .center, spacing: 15) {
    Text("Hello")
        .alignmentGuide(HorizontalAlignment.center) { d in d[.trailing] }
    Text("SwiftUI")
    Text("World")
        .alignmentGuide(HorizontalAlignment.center) { d in d[.leading] }
}
.font(.title)
.border(.gray)

Hello の offset は、明示的に設定した d[.trailing] = width
SwiftUI の offset は、暗黙的に設定されている d[.center] = 2 / width
World の offset は、明示的に設定した d[.leading] = 0

例: テキストを Text Baseline に合わせ画像のみ下げる

HStack(alignment: .bottom) {
    Text("Delicious")
        .font(.caption)
    Image(systemName: "faceid")
        .resizable()
        .frame(width: 40, height: 40)
    Text("Avocado Toast")
}
.lineLimit(1)

Alignment を bottom に合わせているが、左右のテキストサイズが異なるためテキストの下端が揃っていない

HStack(alignment: .lastTextBaseline) {
    Text("Delicious")
        .font(.caption)
    Image(systemName: "faceid")
        .resizable()
        .frame(width: 40, height: 40)
    Text("Avocado Toast")
}
.lineLimit(1)

lastTextBaseline を指定することで、テキストの下端を合わせる

HStack(alignment: .lastTextBaseline) {
    Text("Delicious")
        .font(.caption)
    Image(systemName: "faceid")
        .resizable()
        .frame(width: 40, height: 40)
        .alignmentGuide(.lastTextBaseline) { d in
    	    d[.bottom] * 0.875
        }
    Text("Avocado Toast")
}
.lineLimit(1)

画像の位置を下方向に調整するため offset を d[.bottom] = height * 87.5% に設定

まとめ

offset に応じた View 同士の相対的な配置のされ方を理解できれば、 最初は掴みづらい Alignment Guide も比較的簡単に使える

※ (再掲)ここでの offset は表示位置をズラすための offset とは異なる(例: offset(x:y:)

参考動画

https://developer.apple.com/videos/play/wwdc2019/237/

Discussion