🙏

【SwiftUI】Viewをなるべく中央にキープする

2024/02/25に公開

きっかけ

HStackの中央にViewをキープしつつ、左右の要素が増えた場合は押し出される挙動をするレイアウトを作りたいと思ったのがきっかけです。

左右に余裕あり
右の要素による押し出し

以前も実装しようと思ったことがあったのですが、中央からずれてしまったり、押し出す挙動ができなく妥協していました。

今回改めて考えてみて、意図通りの挙動をするレイアウトが組めたので、忘れないよう記事にしておこうと思います。

イメージとしてはUINavigationBarのTitleと左右のButtonのようなレイアウトです。

うまく動いたコード

早速ですが、完成系です。

 HStack {
    HStack {
        // 任意のButton
        Spacer(minLength: 0)
    }
    .frame(maxWidth: .infinity, maxHeight: .infinity)
    .background(.blue.opacity(0.2))
    
    Text("タイトル")
        .lineLimit(1)
        .layoutPriority(1)
    
    HStack {
        Spacer(minLength: 0)
        // 任意のButton
    }
    .frame(maxWidth: .infinity, maxHeight: .infinity)
    .background(.blue.opacity(0.2))
}
.frame(height: 50)

HStack内にTextとHStackを配置しています。

HStackに対してmaxWidth: .infinityを指定するとSpacerと同じような役割を果たすのでタイトルを中央に寄せることができます。

親のHStackの1/3を超える幅になるとTextの文字が省略されてしまうので、Textには.layoutPriority(1)を入れて省略されにくいようにします。
省略を防ぐ方法としてfixedSizeもありますが、左右の要素が増えたときに文字が省略されてくれないので今回は使えませんでした。

以前試したコード(うまく動かない)

以前、Spacerで中央に寄せて、overlayでViewを置く方法も考えましたが、overlayを使った場合は押し出す挙動が実現できませんでした。

HStack {
    Spacer()
        .overlay {
            HStack {
                ForEach(0..<leftButtonCount
                    Color.blue.frame(width:
                }
                Spacer(minLength: 0)
            }
        }
        .frame(maxHeight: .infinity)
        .background(.orange.opacity(0.2))
    
    Text("タイトル")
        .lineLimit(1)
        .layoutPriority(1)
    
    Spacer()
        .overlay {
            HStack {
                Spacer(minLength: 0)
                ForEach(0..<rightButtonCoun
                    Color.blue.frame(width:
                }
            }
        }
        .frame(maxHeight: .infinity)
        .background(.orange.opacity(0.2))
}
.frame(height: 50)
}

挙動を確認

2つのコードを並べて挙動の確認をしてみます

ContentView.swift
struct ContentView: View {
    @State private var rightButtonCount = 0
    @State private var leftButtonCount = 0
    
    var body: some View {
        VStack(spacing: 100) {
            OKView
            VStack {
                Stepper("右の要素:\(rightButtonCount)", value: $rightButtonCount, in: 0...10)
                Stepper("左の要素:\(leftButtonCount)", value: $leftButtonCount, in: 0...10)
            }
            NGView
        }
        .buttonStyle(.borderedProminent)
    }
    var OKView: some View {
        HStack {
            HStack {
                ForEach(0..<leftButtonCount, id: \.self) { _ in
                    Color.blue.frame(width: 40, height: 40)
                }
                Spacer(minLength: 0)
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(.blue.opacity(0.2))
            
            Text("タイトル")
                .lineLimit(1)
                .layoutPriority(1)
            
            HStack {
                Spacer(minLength: 0)
                ForEach(0..<rightButtonCount, id: \.self) { _ in
                    Color.blue.frame(width: 40, height: 40)
                }
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(.blue.opacity(0.2))
        }
        .frame(height: 50)
    }
    var NGView: some View {
        HStack {
            Spacer()
                .overlay {
                    HStack {
                        ForEach(0..<leftButtonCount, id: \.self) { _ in
                            Color.blue.frame(width: 40, height: 40)
                        }
                        Spacer(minLength: 0)
                    }
                }
                .frame(maxHeight: .infinity)
                .background(.orange.opacity(0.2))
            
            Text("タイトル")
                .lineLimit(1)
                .layoutPriority(1)
            
            Spacer()
                .overlay {
                    HStack {
                        Spacer(minLength: 0)
                        ForEach(0..<rightButtonCount, id: \.self) { _ in
                            Color.blue.frame(width: 40, height: 40)
                        }
                    }
                }
                .frame(maxHeight: .infinity)
                .background(.orange.opacity(0.2))
        }
        .frame(height: 50)
    }
}

(上)うまく動いたコード(下)以前試したコード

要件を満たす挙動ができました🙌

  • Textはなるべく中央をキープする
  • 左右の要素が増えたらTextが押し出されるようにずれる
  • 幅いっぱいになってしまったらTextが省略される

また、Textを省略させない工夫としては以下のことができそうです

minWidthを指定する
    Text("タイトル")
+       .frame(minWidth: 50)
        .lineLimit(1)
        .layoutPriority(1)
ViewThatFitsを使う
+    ViewThatFits {
        Text("長い長いタイトル")
        Text("タイトル")
+    }
    .lineLimit(1)
    .layoutPriority(1)

まとめ

Textに.layoutPriorityを指定しないと文字が省略される原因がわかっていませんが、思ったとおりのレイアウトを組むことができました。
なにかご存じの方いらっしゃいましたらコメントください🙇‍♂

参考になると幸いです。

Discussion