SwiftUI AlignmentGuide

от автора

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