Итак, самый простой путь понять, как работает инструмент, это посмотреть на код и результат. Поэтому, без долгих предисловий, я привожу пример самого распространенного применения. И, также, хочу заметить, что подобное применение уже достаточно активно используется в моем рабочем проекте 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
ссылка на оригинал статьи https://habr.com/ru/articles/838234/
Добавить комментарий