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.sizeCGSize с доступной шириной и высотой;
  • 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, включая производительность, память и сопровождение.

Sources

Related topics