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/