SwiftUIを使用しているときに
複数のViewの配置を設定する場合
alignmentを使用することが多くあると思います。
しかし
コンテナやViewやframeメソッドなど
色々な場所に指定できるため
どれをどう使っていいのかがはっきりとしていませんでした。
そこで
今回はalignmentついてどうすればどう動くのかについて
見ていきたいと思います。
Alignment Guideとは?
同じコンテナに含まれるViewとの相対的な位置を決めます。
垂直(Vertical)の配置と水平(Horizontal)の配置があります。
※ コンテナとはVStackやGroupなどのViewを含むことができるViewのこと
図でイメージしてみる
まずはイメージをつかむために
図で見てみます。
例えば水平の関係を図で示します。
ViewAを基点(0)として考えると
ViewAは
ViewBから20離れた位置にあり
ViewCから10離れた位置にあります。
垂直の場合も同じようになります。
この2つから言えることは
垂直のコンテナ(VStack)には**水平の配置(horizontal alignment)が必要
水平のコンテナ(HStack)には垂直の配置(vertical alignment)**が必要
ということです。
こう考えるとVStack(alignment:)とHStack(alignment:)のalignmentが
何を示しているのかがわかりやすくなると思います。
ZStackの場合は両方が必要になります。
コードから理解する
ここからはコードから詳細を見ていきたいと思います。
まず下記のコードの各値が何を示し
どういう意味を持つのかを考えます。
struct ContentView: View {
var body: some View {
VStack(alignment: .leading) {
Text("Hello World....")
.alignmentGuide(.leading, computeValue: { d in (d[explicit: .leading] ?? 0) })
.alignmentGuide(.leading, computeValue: { d in d[.leading] })
.multilineTextAlignment(.leading)
}.frame(alignment: .leading)
}
}
複数のalignmentが登場していますが
それぞれ意味が異なります。
Container Alignment
2つの目的があります。
1.
Viewの
どのalignmentGuide(alignment:)が無視され
どのalignmentGuide(alignment:)が有効になるか
を決める
明示的にalignmentが指定されていないコンテナ内のViewに
暗黙的にalignmentを設定する。
Alignment Guide
Container Alignmentの値と
Alignment Guideが一致している場合(上記は同じ.leadingなので一致している)
値は有効になりますが異なっている場合は無視されます。
Implicit Alignment
guide(今回は.leading)に対するデフォルトの数値(CGFloat)。
このsubscriptはReadOnlyです。
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct ViewDimensions {
/// Accesses the value of the given guide.
public subscript(guide: HorizontalAlignment) -> CGFloat { get }
/// Accesses the value of the given guide.
public subscript(guide: VerticalAlignment) -> CGFloat { get }
}
※
ViewDimensions
HorizontalAlignment
VerticalAlignment
については後ほど紹介します。
Explicit Alignment
guide(今回は.leading)に対する値で同じですが
コード上で明示的に指定された値です。
設定していなければnilになります。
このsubscriptもReadOnlyです。
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct ViewDimensions {
/// Returns the explicit value of the given alignment guide in this view, or
/// `nil` if no such value exists.
public subscript(explicit guide: HorizontalAlignment) -> CGFloat? { get }
/// Returns the explicit value of the given alignment guide in this view, or
/// `nil` if no such value exists.
public subscript(explicit guide: VerticalAlignment) -> CGFloat? { get }
}
Frame Alignment
frame(alignment:)を呼び出したコンテナに含まれる全てのViewが
どのように配置されるのかをひとまとめに指定します。
Text Alignment
複数行のTextに対して各行がどのように配置されるのかを指定します。
全てのコンテナに含まれるViewが
どのように配置されるのかをひとまとめに指定します。
これらに関しては後ほどより詳細に紹介します。
ImplicitとExplicitの違い
一番重要なポイントとして
コンテナ内の全てのViewはalignmentを持っています。
.alignmentGuide()を呼び出した場合は明示的(explicit)な値が
特定していなければ暗黙的(implicit)な値が設定されます。
implicitの値はコンテナの値から提供されます。
VStack(alignment: .leading)など
もしコンテナに何も指定しない場合は
**デフォルト値の.center**が設定されます。
ViewDimensions
alignmentはcomputedValueクロージャの中でCGFloatで指定します。
この値は任意の値ですが
単純に数値を指定するだけでは何をどう指定すれば良いのかが難しく感じます。
そこでViewDimensionsが活用できます。
.alignmentGuideは下記のような定義になっています。
func alignmentGuide(_ g: HorizontalAlignment,
computeValue: @escaping (ViewDimensions) -> CGFloat) -> some View
func alignmentGuide(_ g: VerticalAlignment,
computeValue: @escaping (ViewDimensions) -> CGFloat) -> some View
computedValueクロージャの引数でViewDimensionsというstructを受け取っています。
この中にはViewに関する有用な情報が入っています。
public struct ViewDimensions {
public var width: CGFloat { get }
public var height: CGFloat { get }
public subscript(guide: HorizontalAlignment) -> CGFloat { get }
public subscript(guide: VerticalAlignment) -> CGFloat { get }
public subscript(explicit guide: HorizontalAlignment) -> CGFloat? { get }
public subscript(explicit guide: VerticalAlignment) -> CGFloat? { get }
}
高さと幅とsubscriptから各guideに対する値を取得できます。
subscriptから取得できる値がわかりづらいので
下記のコードを使ってより詳しく見てみます。
Text("Hello")
.alignmentGuide(.leading, computeValue: { d in
return d[.leading]
+ d.width / 3.0 - (d[explicit: .top] ?? 0)
})
HorizontalAlignment
ViewDimensionがHorizontalAlignmentを指定した場合
下記の位置に対する値が取得できます。
extension HorizontalAlignment {
public static let leading: HorizontalAlignment
public static let center: HorizontalAlignment
public static let trailing: HorizontalAlignment
}
d[.leading]
これはimplicitな値を取得することができます。
通常は
.leadingが0
.centerが2/width
.trailingがwidth
になります。
d[explicit: .leading]
これは明示的にコード上で指定した値を取得します。
どういう時に必要かを考えると
例えばZStackを使っている時に
.leadingの位置を.topの位置から
相対的に決めたい場合などに活用できます。
設定していない場合はnilが返ってきます。
VerticalAlignment
VerticalAlignmentを指定した場合
下記の位置に対する値が取得できます。
extension VerticalAlignment {
public static let top: VerticalAlignment
public static let center: VerticalAlignment
public static let bottom: VerticalAlignment
public static let firstTextBaseline: VerticalAlignment
public static let lastTextBaseline: VerticalAlignment
}
テキストのベースラインを基にした値も取得できます。
d[.top]
これはimplicitな値を取得することができます。
通常は
.topが0
.centerが2/height
.bottomがheight
になります。
Alignment型
ZStackを使用している場合
HorizontalとVerticalの2つを指定する必要があります。
Alignment型は両方指定できるstructです。
public struct Alignment : Equatable {
public var horizontal: HorizontalAlignment
public var vertical: VerticalAlignment
@inlinable public init(horizontal: HorizontalAlignment, vertical: VerticalAlignment)
public static let center: Alignment
public static let leading: Alignment
public static let trailing: Alignment
public static let top: Alignment
public static let bottom: Alignment
public static let topLeading: Alignment
public static let topTrailing: Alignment
public static let bottomLeading: Alignment
public static let bottomTrailing: Alignment
}
下記のように使用します。
ZStack(alignment: Alignment(horizontal: .leading, vertical: .top)) { ... }
また簡単に設定できるように
定数も定義されています。
ZStack(alignment: .topLeading) { ... }
Container Alignment
上記でContainer Alignmentの2つの目的を書きました。
1.
Viewの
どのalignmentGuide(alignment:)が無視され
どのalignmentGuide(alignment:)が有効になるか
を決める
明示的にalignmentが指定されていないコンテナ内のViewに
暗黙的にalignmentを設定する。
これを下記のコードを参照して見ていきます。
struct Implicit: View {
@State private var alignment: HorizontalAlignment = .leading
var body: some View {
VStack {
Spacer()
VStack(alignment: alignment) {
LabelView(title: "A", color: .green)
.alignmentGuide(.leading, computeValue: { _ in 30 } )
.alignmentGuide(HorizontalAlignment.center, computeValue: { _ in 30 } )
.alignmentGuide(.trailing, computeValue: { _ in 90 } )
LabelView(title: "B", color: .red)
.alignmentGuide(.leading, computeValue: { _ in 90 } )
.alignmentGuide(HorizontalAlignment.center, computeValue: { _ in 30 } )
.alignmentGuide(.trailing, computeValue: { _ in 30 } )
LabelView(title: "C", color: .blue)
}
Spacer()
HStack {
Button("leading") { withAnimation(.easeInOut(duration: 2)) { self.alignment = .leading }}
Button("center") { withAnimation(.easeInOut(duration: 2)) { self.alignment = .center }}
Button("trailing") { withAnimation(.easeInOut(duration: 2)) { self.alignment = .trailing }}
}
}
}
}
struct Implicit_Previews: PreviewProvider {
static var previews: some View {
Implicit()
}
}
struct LabelView: View {
let title: String
let color: Color
var body: some View {
Text(title)
.font(.title)
.padding(10)
.frame(width: 200, height: 40)
.background(RoundedRectangle(cornerRadius: 8)
.fill(LinearGradient(gradient: Gradient(colors: [color, .black]), startPoint: UnitPoint(x: 0, y: 0), endPoint: UnitPoint(x: 2, y: 1))))
}
}
CはalignmentGuide(alignment:)を使用していないため
implicitな値が設定され
.leadingが0
.centerが2/width
.trailingがwidth
になります。
下記のような動きになります。
.leadingの場合
Cの左端が0になり
AはCより30左に
BはCより90左に
配置されています。
.centerの場合
![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3231393936352f31386636636533362d306535652d643135382d626237302d3632616262316434376336332e706e67.png]
(https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219965/683c48a5-3531-dddb-fc79-b0e67615bda6.png)
Cの中心が0になり
AもBもCより30中心が右にずれています。
.trailingの場合
Cの右端が0になり
AはCより90左に
BはCより30左に
配置されています。
Frame Alignment
これまで見てきたalignmentは
Viewの相対関係と基にした位置を扱ってきましたが
レイアウトシステムはこの後にコンテナ全体の配置を決めます。
frame(alignment:)がこの役割を担っています。
指定していない場合はデフォルト値の.centerになります。
しかし
設定したとしても効果がない場合があります。
それは
コンテナ内のViewでコンテナのスペースが埋まっている場合です。
この時コンテナは動けません。
Multiline Text Alignment
これはとてもシンプルです。
下記のコードを例にします。
struct TextAlignment: View {
var body: some View {
Text("Hello!\nNice to meet you!\nHow are you?")
.multilineTextAlignment(.leading)
}
}
struct TextAlignment_Previews: PreviewProvider {
static var previews: some View {
TextAlignment()
}
}
.leadingの場合
.centerの場合
.trailingの場合
Custom Alignment
これまでは標準のalignmentについて見てきましたが
独自のalignmentを作成することもできます。
下記のコードから考えてみます。
extension HorizontalAlignment {
private enum WeirdAlignment: AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d.height
}
}
static let weirdAlignment = HorizontalAlignment(WeirdAlignment.self)
}
独自のalignmentを実装する場合
2つのものが必要になります。
- 水平(hotrizontal)か垂直(vertical)かを決める
- implicitなalignment用のデフォルト値を提供する
下記のコードを使います。
struct CustomAlignment: View {
var body: some View {
VStack(alignment: .weirdAlignment, spacing: 10) {
Rectangle()
.fill(Color.primary)
.frame(width: 1)
.alignmentGuide(.weirdAlignment, computeValue: { d in d[.trailing] })
ColorLabel(label: "Monday", color: .red, height: 50)
ColorLabel(label: "Tuesday", color: .orange, height: 70)
ColorLabel(label: "Wednesday", color: .yellow, height: 90)
ColorLabel(label: "Thursday", color: .green, height: 40)
ColorLabel(label: "Friday", color: .blue, height: 70)
ColorLabel(label: "Saturday", color: .purple, height: 40)
ColorLabel(label: "Sunday", color: .pink, height: 40)
Rectangle()
.fill(Color.primary)
.frame(width: 1)
.alignmentGuide(.weirdAlignment, computeValue: { d in d[.leading] })
}
}
}
struct CustomAlignment_Previews: PreviewProvider {
static var previews: some View {
CustomAlignment()
}
}
struct ColorLabel: View {
let label: String
let color: Color
let height: CGFloat
var body: some View {
Text(label).font(.title).foregroundColor(.primary).frame(height: height).padding(.horizontal, 20)
.background(RoundedRectangle(cornerRadius: 8).fill(color))
}
}
こうすると高さに合わせて
全てのViewの間の幅が変わります。
VStackのspacingが10の場合
VStackのspacingが40の場合
Custom Alignmentはいつ使う?
異なる階層構造にあるView同士を揃えたい場合に
有効活用できます。
下記のコードを見ていきます。
extension VerticalAlignment {
private enum MyAlignment : AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return d[.bottom]
}
}
static let myAlignment = VerticalAlignment(MyAlignment.self)
}
struct DifferentViewHierarchy: View {
@State private var selectedIdx = 3
let days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
var body: some View {
HStack(alignment: .myAlignment) {
Image(systemName: "arrow.right.circle.fill")
.alignmentGuide(.myAlignment, computeValue: { d in d[VerticalAlignment.center] })
.foregroundColor(.green)
VStack(alignment: .leading) {
ForEach(days.indices, id: \.self) { idx in
Group {
if idx == self.selectedIdx {
Text(self.days[idx])
.transition(AnyTransition.identity)
.alignmentGuide(.myAlignment, computeValue: { d in d[VerticalAlignment.center] })
} else {
Text(self.days[idx])
.transition(AnyTransition.identity)
.onTapGesture {
withAnimation {
self.selectedIdx = idx
}
}
}
}
}
}
}
.padding(20)
.font(.largeTitle)
}
}
struct DifferentViewHierarchy_Previews: PreviewProvider {
static var previews: some View {
DifferentViewHierarchy()
}
}
これは下記のような構造になっています。
ImageとTextが同じHStackにいるため
それぞれに.centerを指定することで高さを合わせています。
同様にHStackに.centerを指定することも必要です。
ここでちょっと複雑なことが起きています。
選択されたText以外はalignmentを明示的に指定していないため
全てのTextが一番上のテキストに上乗せされるように思えます。
しかし
VStackを使用しているため
全てのViewは垂直に並ぶようになります。
そこに明示的にalignmentを指定すると
レイアウトシステムはそれを優先的に使用するため
選択されたTextはImageと揃うようになり
他はそれに合わせて相対的に配置されるようになります。
理解するためのサンプル
alignmentを理解するためのサンプルとして
とても参考になります。
"Show in Two Phases"をONにすると
alignmentGuideがどう移動して
その後Viewが実施にどう移動するのかを分けてみることができます。
また
作者は下記について確認してみて欲しいと言っています。
- コンテナのスペースを
Viewで埋めている場合と余裕がある場合のframe(alignment:)の動きの違い - コンテナのguideとViewのguideが異なる場合に何も変化しないこと
-
.leadingなどを指定した場合と数値を指定した場合の違い - マイナスや
Viewの幅以上の値を設定した場合の動き
まとめ
alignmentについて見てきました。
一見複雑ですが
どういうルールでalignmentが決まるのかがわかれば
比較的わかりやすいのではないかと思いました。
あとはやはり実際に動かしてみて
どういう動きをするのかを見ていくのが一番良いですね😃
何か間違いなどございましたら
教えて頂けますとうれしいです🙇🏻♂️
参考記事
こちらの記事を主に参考にさせていただきました。
https://swiftui-lab.com/alignment-guides/







