Pull to refresh

SwiftUI AlignmentGuide

Level of difficultyMedium
Reading time8 min
Views1.2K

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

Tags:
Hubs:
Total votes 3: ↑3 and ↓0+7
Comments0

Articles