Итак, самый простой путь понять, как работает инструмент, это посмотреть на код и результат. Поэтому, без долгих предисловий, я привожу пример самого распространенного применения. И, также, хочу заметить, что подобное применение уже достаточно активно используется в моем рабочем проекте iOS СберБизнес, и на данный момент побочных эффектов к счастью не замечено 😊.
Тут у нас HStack с иконкой и вложенным VStack. И мы выравниваем иконку по центру второго текста в HStack
(голубая стрелка показывает направление горизонтальной выравнивающей). Читать подобный код надо с того места, где расположен вызов .alignmentGuide
.
Тогда смысл этого кода звучит примерно так: Text("Sorting keys..")
задает кастомный горизонтальный центр customHorizontalCenter
для HStack(alignment: .customHorizontalCenter)
.
Код
struct HorizontalCenterExample: View {
var body: some View {
HStack(alignment: .customHorizontalCenter, spacing: 16) {
Image(systemName: "sun.min.fill" )
VStack(alignment: .leading, spacing: 16) {
Text("Theme")
.font(.title)
.border(.green)
Text("Sorting keys for json encoding")
.font(.title2)
.border(.green)
.alignmentGuide(.customHorizontalCenter, computeValue: {
$0[VerticalAlignment.center]
})
Text("The JsonObject representation will preserve insertion order, whether you build the object with empty and add or with from or fromIterable ")
.border(.green)
}
}
.border(Color.red)
}
}
extension VerticalAlignment {
private enum CustomHorizontalCenter: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context[VerticalAlignment.center]
}
}
static let customHorizontalCenter = VerticalAlignment(CustomHorizontalCenter.self)
}
Второй пример практически аналогичен первому, кроме того, что тут используется вертикальное выравнивание, вместо горизонтального. И, кстати, тут довольно много путаницы с горизонталями и вертикалями, поэтому нужно быть аккуратным в нейменге, чтобы не запутался самому и не запутать других.
Смысл этого кода звучит примерно так: Зеленый прямоугольник
задает кастомный вертикальны центр customVerticalCenter
для VStack(alignment: .customVerticalCenter)
.
Код
struct VerticalCenterExample: View {
var body: some View {
VStack(alignment: .customVerticalCenter, spacing: 16) {
Image(systemName: "sun.min.fill")
.foregroundStyle(.green)
HStack(alignment: .top, spacing: 16) {
Rectangle()
.foregroundStyle(.yellow)
.frame(width: 50, height: 100)
Rectangle()
.foregroundStyle(.blue)
.frame(width: 120, height: 100)
Rectangle()
.foregroundStyle(.green)
.frame(width: 100, height: 100)
.alignmentGuide(.customVerticalCenter, computeValue: {
$0[HorizontalAlignment.center]
})
}
}
.border(Color.red)
}
}
extension HorizontalAlignment {
struct CustomVertivalCenter: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context[HorizontalAlignment.center]
}
}
static let customVerticalCenter = HorizontalAlignment(CustomVertivalCenter.self)
}
Modifier AlignmentGuide
Надеюсь, приведенные примеры и комментарии помогли понять как работает модификатор AlignmentGuide, и теперь посмотрим на его синтаксис:
func alignmentGuide(_ g: HorizontalAlignment, computeValue: @escaping (ViewDimensions) -> CGFloat) -> some View
func alignmentGuide(_ g: VerticalAlignment, computeValue: @escaping (ViewDimensions) -> CGFloat) -> some View
Используйте модификатор alignmentGuide(_:computeValue:) чтобы рассчитать специфические смещения Views относительно друг друга. Смещение рассчитывается в замыкании computeValue через параметр типа ViewDimensions.
Тип ViewDemensions
public struct ViewDimensions {
var width: CGFloat
var height: CGFloat
/// Implicit guides
subscript(guide: HorizontalAlignment) -> CGFloat
subscript(guide: VerticalAlignment) -> CGFloat
/// Explicit guides
subscript(explicit guide: HorizontalAlignment) -> CGFloat?
subscript(explicit guide: VerticalAlignment) -> CGFloat?
}
ViewDemensions - это тип, который передается в замыкание и представляет метрики локального View в системе его локальных координат. Структура ViewDemensions имеет параметры width и height, а также сабскрипты для доступа к горизонтальному и вертикальному alignment-ам. Вот так это выглядит в коде:
Rectangle()
.alignmentGuide(.customVerticalCenter, computeValue: { dimension in
dimension[HorizontalAlignment.center] - dimension.width / 4]
})
Сабскрипты ViewDimensions в модификаторе alignmentGuide могут вызываться для явных и неявных alignment guide:
subscript(guide: HorizontalAlignment) // неявный (Implicit) alignment
subscript(explicit guide: HorizontalAlignment) // явный (Explicit) alignment
Давайте на простом примере разберемся, что это означает.
Implicit & Explicit Alignments
Каждый контейнер имеет выравнивание (alignment), который отвечает за расположение дочерних элементов. И дочерние элементы неявно (Implicit) получают этот alignment. Метод .alignmentGuide задает явный (Explicit) alignment у элемента.
Давайте посмотрим это на простом как работают Implicit и Explicit Alignments.
Код
VStack(alignment: .leading) {
// Смотрим различия между значениями dimension в 1-м и 2-м замыкании
Rectangle()
.foregroundColor(.yellow)
.frame(width: 120, height: 50)
.alignmentGuide(.leading, computeValue: { dimension in
// dimension[.leading] == 0
// dimension[explicit: .leading]) == nil
dimension[.trailing]
})
.alignmentGuide(.leading, computeValue: { dimension in
// после добавления .alignmentGuide выше, появилось значение 120
// dimension[.leading] == 120
// dimension[explicit: .leading]) == Optional(120.0)
dimension[.trailing]
})
// Значения dimension не накапливаются, относятся только к локальному View
Rectangle()
.foregroundColor(.red)
.frame(width: 100, height: 50)
.alignmentGuide(.leading, computeValue: { dimension in
// dimension[.leading] == 0
// dimension[explicit: .leading]) == nil
dimension[.trailing]
})
.alignmentGuide(.leading, computeValue: { dimension in
// после добавления .alignmentGuide выше, появилось значение 100
// dimension[.leading] == 100.0
// dimension[explicit: .leading]) == Optional(100.0)
dimension[.trailing]
})
// explicit nil != implicit 0 (так только в .leading)
Rectangle()
.foregroundColor(.yellow)
.frame(width: 120, height: 50)
.alignmentGuide(.leading, computeValue: { dimension in
// dimension[.trailing] == 120
// dimension[explicit: .trailing]) == nil
dimension[.trailing]
})
}
}
Здесь нужно обратить внимание на несколько моментов:
Явный (Explicit) alignment опционален. Он получает значения только, если перед его вызовом явно указан модификатор alignmentGuide.
Неявный (Implicit) alignment не опционален. Если Explicit alignment определен, то значения Implicit и Explicit равны. Иначе, неявный alignment отдает значения выравнивания, вычисленное от родительского контейнера.
Значения alignments у дочерних прямоугольников не зависимы, не происходит накопление смещения в нашем примере, хотя такое поведение могло бы показаться логичным. Таким образом, с помощью этих значений нельзя построить "ступеньки" с нарастающим отступом.
Для контейнера можно задать только один alignment. Нельзя, например, использовать одновременно .leading и .trailing выравнивание, что ограничивает применение инструмента. Замечу, что система выравнивания в ZStack более сложная, тем не менее там тоже один alignment, хотя и композитный.
AlignmentGuid другие примеры использования:
Еще один интересный пример, вероятно, вы уже видели в разных источниках (в конце статьи я приведу весь список источников, которые были мне полезны).
Здесь у нас нет вложенных стеков, только один HStack, с пятью прямоугольниками. И каждый черный прямоугольник, вернее его .bottom, задает новый .top в HStask (голубая стрелка), от которого выравниваются все цветные прямоугольники в этом стеке
Код
struct DiagramHorizontalExample: View {
var body: some View {
HStack(alignment: .top, spacing: 0) {
Rectangle()
.frame(width: 50, height: 100)
// The Rectangle define new top guide for HStack
// other Rectangles will start from it
.alignmentGuide(.top, computeValue: { dimension in
dimension[.bottom] + 10
})
Rectangle()
.foregroundStyle(.blue)
.frame(width: 60, height: 40)
Rectangle()
.frame(width: 70, height: 50)
.alignmentGuide(.top, computeValue: { dimension in
dimension[.bottom] + 10
})
Rectangle()
.foregroundStyle(.red)
.frame(width: 50, height: 50)
Rectangle()
.frame(width: 80, height: 40)
.alignmentGuide(.top, computeValue: { dimension in
dimension[.bottom] + 10
})
}
.padding()
.border(.red)
}
}
Допустим, мы хотим немного модифицировать данный пример, и добавить голубой дивайдер между черными и цветными прямоугольниками. Для этого придется создать новую направляющую выравнивания middleLine
, в которую мы сохраним top-значение синего прямоугольника (можем и красного: любого цветного). А также добавим код:
.overlay(alignment: .init(horizontal: .center, vertical: .middleLine))
Overlay работает по аналогии с ZStack. Немного ранее я упоминала, что ZStack имеет композитное выравнивание, и именно так оно выглядит. Голубой горизонтальный разделитель мы выравниваем по вертикали по алигнменту, который задал синий прямоугольник. А по горизонтали он занимает всю длину, поэтому тут параметр особого значения не будет иметь, и я указала .center.
Код
struct DiagramHorizontalDivided: View {
var body: some View {
HStack(alignment: .top, spacing: 0) {
Rectangle()
.frame(width: 50, height: 100)
.alignmentGuide(.top, computeValue: { dimension in
dimension[.bottom] + 10
})
Rectangle()
.foregroundStyle(.blue)
.frame(width: 60, height: 40)
.alignmentGuide(.middleLineTop, computeValue: { dimension in
dimension[.top]
})
Rectangle()
.frame(width: 70, height: 50)
.alignmentGuide(.top, computeValue: { dimension in
dimension[.bottom] + 10
})
Rectangle()
.foregroundStyle(.red)
.frame(width: 50, height: 50)
Rectangle()
.frame(width: 80, height: 40)
.alignmentGuide(.top, computeValue: { dimension in
dimension[.bottom] + 10
})
}
.overlay(alignment: .init(horizontal: .center, vertical: .middleLine)) {
Rectangle()
.foregroundStyle(.cyan)
.frame(height: 2)
}
.padding()
.border(.red)
}
}
extension VerticalAlignment {
private enum MiddleLine: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context[VerticalAlignment.bottom] + 4
}
}
static let middleLine = VerticalAlignment(MiddleLine.self)
}
AlignmnetGuide и альтернативы
Несмотря на то, что инструмент имеет достаточно многословный синтаксис, пока я увидела ограниченное количество задач, где он бесспорно полезен. Это я к тому, что не торопитесь прикручивать AlignmentGuide там, где можно найти решения попроще. Нашла вот такой пример: верстка с помощью Grid выглядит лучше, а код - понятнее. Оба решения привожу.
Код с AlignmentGuid
struct TwoTextColumns: View {
var body: some View {
VStack(alignment: .custom, spacing: 16) {
HStack {
Text("Username").font(Font.body.bold())
// trailing of the second text will be a leading for children of VStack
Text("Tatyana")
.alignmentGuide(.custom) { $0[.leading] }
}
HStack {
Text("Password").font(Font.body.bold())
Text("•••••••••••••••••")
.alignmentGuide(.custom) { $0[.leading] }
}
HStack {
Text("Email").font(Font.body.bold())
Text("black@mail.ru")
.alignmentGuide(.custom) { $0[.leading] }
}
}
.padding(.all, 16)
.border(.blue)
}
}
Код с Grid
struct TwoTextColumnsGrid: View {
var body: some View {
Grid(alignment: .leading, verticalSpacing: 8) {
GridRow() {
Text("Username").font(Font.body.bold())
Text("Tatyana")
}
GridRow {
Text("Password").font(Font.body.bold())
Text("•••••••••••••••••")
}
GridRow() {
VStack(alignment: .leading) {
Text("Email").font(Font.body.bold())
Text("Обязательное поле")
.font(Font.caption)
.foregroundColor(.gray)
}
Text("black@mail.ru")
}
}
.padding(.all, 16)
.border(.blue)
}
}
И вот такой пример, возможно, полезен для понимания работы .alignmentGuide, но на мой взгляд, реализация через .padding выглядит гораздо понятнее.
Надеюсь данный материал был полезен. Всем хорошего дня и интересных задач!
Оставлю тут ссылку на GitHub c приведенными в коде примерами, а также прилагаю статьи, которые показались мне полезными.
Ну и конечно, ссылки на доку:
https://developer.apple.com/documentation/swiftui/aligning-views-across-stacks
https://developer.apple.com/documentation/SwiftUI/AlignmentID
https://developer.apple.com/documentation/swiftui/view/alignmentguide(_:computevalue:)-9mdoh
https://developer.apple.com/documentation/swiftui/viewdimensions
https://developer.apple.com/documentation/swiftui/horizontalalignment
https://developer.apple.com/documentation/swiftui/verticalalignment