При создании представлений SwiftUI, которые должны адаптироваться к размерам контейнера, многие разработчики обращаются к GeometryReader. Это стандартное решение, но оно имеет свои издержки: влияя на иерархию представлений, может усложнить логику макета.

В этой статье я покажу, как создать правильный модификатор с помощью onGeometryChange и contentMargins — двух мощных API iOS 17+, которые позволяют отслеживать изменения геометрии без обертывания представлений в GeometryReader.

Примечание: onGeometryChange  вышел с iOS 17 SDK, но был портирован на iOS 16.

Проблема: скрытые затраты GeometryReader

Допустим, вам надо центрировать контент по горизонтали с ограничением максимальной ширины. Это распространенная задача для макетов iPad или дизайнов для широких экранов. Традиционный подход выглядит примерно так:

var body: some View {
  GeometryReader { geometry in
    ScrollView {
      LazyVStack {
        // Контент
      }
      .frame(maxWidth: 768)
    }
    .contentMargins(
      .horizontal,
      max(0, (geometry.size.width - 768) / 2),
      for: .scrollContent
    )
  }
}

Это работает, но GeometryReader имеет несколько недостатков:

  • влияет на компоновку: занимает все доступное пространство, что может нарушить иерархию представлений;
  • приводит к многословности: требуется оборачивать всю структуру представлений;
  • не подлежит переиспользованию: этот паттерн приходится повторять во множестве представлений.

Решение: правильный ViewModifier

Вместо оборачивания представлений в GeometryReader, можно создать переиспользуемый модификатор, который применяет onGeometryChange для отслеживания изменений размера без влияния на компоновку. Вот как это сделать:

import SwiftUI

struct MaxWidthContentMargins: ViewModifier {
  @State private var containerWidth: CGFloat = 0

  func body(content: Content) -> some View {
    content
      .onGeometryChange(
        for: CGFloat.self,
        of: { geometry in
          geometry.size.width
        }
      ) { width in
        containerWidth = width
      }
      .contentMargins(
        .horizontal,
        max(0, (containerWidth - .editorMaxColumnWidth) / 2),
        for: .scrollContent
      )
  }
}

extension View {
  func maxWidthContentMargins() -> some View {
    modifier(MaxWidthContentMargins())
  }
}

Как это работает

1.  onGeometryChange: отслеживание без влияния

Модификатор onGeometryChange позволяет отслеживать изменения геометрии без обертывания представления в GeometryReader. Он имеет три параметра:

  • for: тип отслеживаемого значения (в данном случае CGFloat для ширины);
  • of: замыкание, которое извлекает значение из GeometryProxy;
  • action: замыкание, которое выполняется при изменении геометрии.
.onGeometryChange(
  for: CGFloat.self,
  of: { geometry in
    geometry.size.width
  }
) { width in
  containerWidth = width
}

Вот ключевое отличие: onGeometryChange отслеживает геометрию, не влияя на иерархию представлений. Ваши представления остаются чистыми и неизмененными.

2. contentMargins: точное позиционирование контента

Модификатор contentMargins позволяет регулировать отступы вокруг прокручиваемого контента, не затрагивая индикаторы прокрутки. Это идеально подходит для центрирования контента при сохранении индикаторов прокрутки у краев экрана.

При этом чистое пространство, которое стало отступами, остается прокручиваемым и интерактивным.

.contentMargins(
  .horizontal,
  max(0, (containerWidth - .editorMaxColumnWidth) / 2),
  for: .scrollContent
)

Данное вычисление max(0, (containerWidth — .editorMaxColumnWidth) / 2) обеспечивает:

  • на широких экранах: центрирование Content с равными отступами с обеих сторон;
  • на узких экранах: отсутствие дополнительных отступов (функция max(0, …) предотвращает появление отрицательных значений)

Применение модификатора

Теперь применение модификатора становится предельно простым:

struct LibraryView: View {
  var body: some View {
    ScrollView {
      LazyVStack {
        // Контент
      }
      .frame(maxWidth: .editorMaxColumnWidth)
    }
    .maxWidthContentMargins()
  }
}

Никаких оберток GeometryReader. Никаких повторяющихся геометрических вычислений. Всего лишь чистый, декларативный модификатор.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Thomas Ricouard: Beyond GeometryReader: Building Better SwiftUI Modifiers with onGeometryChange

Предыдущая статьяКак создать платный доступ, который не вызовет раздражения у пользователей
Следующая статьяC++: полное руководство по динамическим массивам