
При создании представлений 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. Никаких повторяющихся геометрических вычислений. Всего лишь чистый, декларативный модификатор.
Читайте также:
- Как вернуть контроль над состоянием данных с RemoteResult
- Apple убивает Swift
- 7 лучших ресурсов для iOS-разработчиков в 2025 году
Читайте нас в Telegram, VK и Дзен
Перевод статьи Thomas Ricouard: Beyond GeometryReader: Building Better SwiftUI Modifiers with onGeometryChange





