SwiftUIMiddleCoding
Как использовать GeometryReader в SwiftUI и каковы его подводные камни?
GeometryReader предоставляет дочерним View доступ к размерам контейнера через GeometryProxy (.size, .safeAreaInsets, .frame(in:)), но занимает всё доступное пространство и может вызывать лишние перерисовки.
GeometryReader в SwiftUI
GeometryReader — контейнерный View, который предоставляет дочерним View информацию о доступном пространстве через объект GeometryProxy. Он занимает всё предложенное ему пространство (flexible sizing) и передаёт его размеры внутрь через замыкание.
Базовое использование
struct RelativeCard: View {
var body: some View {
GeometryReader { proxy in
VStack {
// Карточка занимает 80% доступной ширины
RoundedRectangle(cornerRadius: 12)
.fill(Color.blue)
.frame(
width: proxy.size.width * 0.8,
height: proxy.size.height * 0.4
)
.position(
x: proxy.size.width / 2,
y: proxy.size.height / 2
)
}
}
}
}
GeometryProxy API
Объект GeometryProxy предоставляет три основных свойства:
proxy.size—CGSizeс доступной шириной и высотой;proxy.safeAreaInsets— вставки безопасной зоны (EdgeInsets);proxy.frame(in:)— фрейм в заданной системе координат (.global,.local,.named()).
struct OffsetTracker: View {
@State private var offsetY: CGFloat = 0
var body: some View {
ScrollView {
GeometryReader { proxy in
Color.clear
.preference(
key: ScrollOffsetKey.self,
value: proxy.frame(in: .named("scroll")).minY
)
}
.frame(height: 0)
ForEach(0..<50) { i in
Text("Item \(i)")
.padding()
}
}
.coordinateSpace(name: "scroll")
.onPreferenceChange(ScrollOffsetKey.self) { offsetY = $0 }
.overlay(Text("Offset: \(Int(offsetY))"), alignment: .top)
}
}
struct ScrollOffsetKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
Паттерн с PreferenceKey
Классический способ «пробросить» размер из дочернего View к родителю — GeometryReader + PreferenceKey. Это позволяет выровнять несколько View по одной высоте:
struct EqualHeightCards: View {
@State private var maxHeight: CGFloat = 0
var items = ["Short", "A very long text that wraps", "Medium text"]
var body: some View {
HStack(alignment: .top, spacing: 16) {
ForEach(items, id: \.self) { text in
Text(text)
.padding()
.background(
GeometryReader { proxy in
Color.clear.preference(
key: HeightKey.self,
value: proxy.size.height
)
}
)
.frame(height: maxHeight == 0 ? nil : maxHeight)
.background(Color.gray.opacity(0.2))
.cornerRadius(8)
}
}
.onPreferenceChange(HeightKey.self) { maxHeight = $0 }
}
}
struct HeightKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
}
Подводные камни
- GeometryReader занимает всё доступное пространство — в отличие от большинства View он расширяется до максимума, что ломает компоновку при использовании внутри
HStack/VStackбез явногоframe. - Вложенные GeometryReader вызывают layout-циклы — каждый уровень вложенности может провоцировать лишние проходы компоновки; минимизируйте вложенность.
- Использование в List/LazyVStack — при прокрутке GeometryReader внутри ячейки пересчитывает размеры на каждый frame, что сильно нагружает CPU.
- proxy.size не обновляется при анимации — значение фиксируется в момент рендера; для анимированных размеров используйте
.matchedGeometryEffectили явные анимации черезwithAnimation. - Начальный размер = CGSize.zero — в первый рендер
proxy.sizeможет быть нулевым, что вызывает мигание. Защищайтесь черезif proxy.size.width > 0. - Избыточное использование вместо ViewThatFits — в iOS 16+ для адаптации под доступное пространство лучше использовать
ViewThatFits, который не захватывает всё пространство. - frame(in: .global) возвращает позицию в момент рендера — при прокрутке значение устаревает мгновенно; для отслеживания прокрутки нужен
coordinateSpace(name:).
Common mistakes
- Сводить «использовать
GeometryReaderв SwiftUI и каковы его подводные камни» к синтаксису и не объяснять идентичность view. - Игнорировать жизненный цикл, основной поток или момент освобождения ресурсов в сценарии swiftui-10.
- Выбирать API по привычке, не проверяя состояние, ошибки, доступность и платформенные ограничения.
What the interviewer is testing
- Формулирует точную модель для «использовать
GeometryReaderв SwiftUI и каковы его подводные камни» и подтверждает ее корректным примером. - Умеет связать ответ с инвалидация body, тестированием и отладкой на устройстве.
- Называет ограничения подхода swiftui-10, включая производительность, память и сопровождение.