見出し画像

SwiftUIのAlignment Guideについて調べてみた

Alignment Guideとは?

Alignmentとは要素の配置を指定する構造体で、HorizontalAlignmentとVerticalAlignmentを含む。そしてAlignment Guideは、基準のAlignmentからの位置(オフセット)を調整できるViewModifierで、コンテナビュー内でのレイアウトを指定できる。

Alignment Guideの基本的な使い方

Alignment Guideの定義は下記の通りで、第1引数に基準となるAlignment(.centerや.leadingなど)を指定し、第2引数にオフセットとなる値を計算して返却するクロージャーを渡す。なお、オフセットの計算において、クロージャーの引数であるViewDimensionsを使って、Viewのwidthやheight、ViewのAlignment Guide位置を取得できる。

// HorizontalAlignmentからの位置を調整する
func alignmentGuide(
    _ g: HorizontalAlignment,
    computeValue: @escaping (ViewDimensions) -> CGFloat
) -> some View

// VerticalAlignmentからの位置を調整する
func alignmentGuide(
    _ g: VerticalAlignment,
    computeValue: @escaping (ViewDimensions) -> CGFloat
) -> some View

下記コードではHotizontalAlignment.centerからのオフセットを指定しており、それぞれのTagが指定された分だけ位置が調整されていることがわかる。

struct BasicSampleView: View {
    var body: some View {
            VStack(alignment: HorizontalAlignment.center) {
                Color.red.frame(width: 1)
                Tag("#AAA")
                    .alignmentGuide(HorizontalAlignment.center) { context in
                        // .centerからのオフセットをwidthの2倍とする
                        return context.width * 2
                    }
                Tag("#BBBBBBBBBB")
                    .alignmentGuide(HorizontalAlignment.center) { context in
                        // .centerからのオフセットをTagのtrailingに合わせる
                        context[HorizontalAlignment.trailing]
                    }
                Tag("#CCCCC")
                    .alignmentGuide(HorizontalAlignment.center) { context in
                        // .centerからのオフセットをTagのcenterに合わせる
                        context[HorizontalAlignment.center]
                    }
                Tag("#DDDDDDDDDDDDDDD")
                    .alignmentGuide(HorizontalAlignment.center) { context in
                        // .centerからのオフセットを0とする
                        return 0
                    }
                Tag("#EEEEEEEEEE")
                    .alignmentGuide(HorizontalAlignment.center) { context in
                        // .centerからのオフセットをwidthの-0.5倍とする
                        return -context.width * 0.5
                    }
                Color.red.frame(width: 1)
            }
            .border(Color.gray)
            .navigationTitle("Basic Sample")
        }
}

Alignment Guideとoffsetの違い

alignmentGuide(_:computeValue:)以外にもViewの位置を調整するoffset(x:y:)があるが、公式ドキュメントのDiscussionにあるように、offsetはあくまでも表示内容をずらすのであって、View本来のサイズは変更されない。

Use offset(x:y:) to shift the displayed contents by the amount specified in the x and y parameters. The original dimensions of the view aren’t changed by offsetting the contents

下記コードではalignmentGuideとoffsetそれぞれで位置を調整しており、alignmentGuideの場合はコンテナビューも合わせて変化しているのに対して、offsetの場合は初期状態のままであることがわかる。

struct AlignmentGuideVsOffset: View {
    var body: some View {
        VStack(spacing: 40) {
            VStack(alignment: HorizontalAlignment.center) {
                Tag("#AAA")
                    .alignmentGuide(HorizontalAlignment.center) { context in
                        return context.width * 2
                    }
                Tag("#BBBBBBBBBB")
                    .alignmentGuide(HorizontalAlignment.center) { context in
                        context[HorizontalAlignment.trailing]
                    }
                Tag("#CCCCC")
                    .alignmentGuide(HorizontalAlignment.center) { context in
                        context[HorizontalAlignment.center]
                    }
            }
            .border(Color.gray)
            
            VStack(alignment: HorizontalAlignment.center) {
                Tag("#AAA")
                    .offset(x: -50, y: 0)
                Tag("#BBBBBBBBBB")
                    .offset(x: -30, y: 0)
                Tag("#CCCCC")
                    .offset(x: 40, y: 0)
            }
            .border(Color.gray)
        }
        .navigationTitle("Alignment Guide vs. Offset")
    }
}

Alignment Guideの応用例

Alignment Guideを応用すると「タグを配置する際に自動的に改行させる」ようなレイアウトを組むことができる。HStackだとタグが詰まってしまうが、この記事にあるようにZStackとAlignment Guideを組み合わせることで実現できる。ただし、クロージャー内で複雑な計算が必要となってしまう。
補足:iOS 16から使えるLayoutプロトコルを使えば、複雑なレイアウトを組みやすくなり、タグの自動改行もより簡単になる(こちらを参照)。

struct TagBreakView: View {
    @State var tagList = ["#AAA", "#BBBBBBBBBB", "#CCCCC", "#DDDDDDDDDDDDDDD", "#EEEEEEEEEE"]
    
    var body: some View {
        GeometryReader { geometry in
            generateTags(geometry)
        }
        .padding()
    }
    
    private func generateTags(_ geometry: GeometryProxy) -> some View {
        var leading = CGFloat.zero
        var top = CGFloat.zero
        
        return ZStack(alignment: .topLeading) {
          ForEach(tagList, id: \\.self) { tag in
              Tag(tag)
                  .padding([.horizontal, .vertical], 4)
                  .alignmentGuide(.leading, computeValue: { context in
                      if abs(leading - context.width) > geometry.size.width {
                          // 改行の場合はleadingをリセットする
                          leading = 0
                          // topも積算する
                          top -= context.height
                      }
                      
                      // 改行判定後に返却値を代入
                      let result = leading
                      
                      if tag == tagList.last {
                          // 複数回計算されるためリセットする
                          leading = 0
                      } else {
                          // leadingを積算する (次の基準とするため返却値に積算させない)
                          leading -= context.width
                      }
                      return result
                  })
                  .alignmentGuide(.top, computeValue: { _ in
                      let result = top
                      if tag == tagList.last {
                          // 複数回計算されるためリセットする
                          top = 0
                      }
                      return result
                  })
          }
        }
        .border(Color.gray)
        .navigationTitle("Tag Break")
    }
}

サンプルコード

GitHubリポジトリ

参考


この記事が気に入ったらサポートをしてみませんか?