Добавить объявление

От ScrollViewReader до ScrollPosition - как изменилась прокрутка в SwiftUI

До iOS 17 программный скролл в SwiftUI требовал использования ScrollViewReader. Этот контейнер давал доступ к ScrollViewProxy с единственным методом scrollTo(_:anchor:). API было односторонним: можно было задать позицию, но нельзя было прочитать, где находится пользователь. Скролл к краю или к конкретному оффсету тоже был проблемой - приходилось ставить якорные вью на границах контента.

Начиная с iOS 17, SwiftUI начал менять подход. scrollPosition(id:anchor:) заменил прокси-подход на привязку к состоянию. А iOS 18 добавил еще больше возможностей.

Как задать начальную позицию:


По умолчанию ScrollView показывает верхнюю часть контента. defaultScrollAnchor(_:), появившийся в iOS 17, позволяет изменить это поведение. Типичный пример - чат, где контент должен начинаться снизу.

ScrollView {
LazyVStack {
ForEach(messages) { message in
MessageView(message: message)
}
}
}
.defaultScrollAnchor(.bottom)

В iOS 18 появилась более точная перегрузка defaultScrollAnchor(_:for:), которая принимает ScrollAnchorRole. Например, роль alignment позволяет центрировать небольшой контент, сохраняя нижнюю привязку для длинных списков.

Программный скролл через scrollPosition:


iOS 17 добавил scrollPosition(id:anchor:) для управления скроллом через привязку к состоянию. iOS 18 расширил API: появился scrollPosition(_:anchor:) с привязкой к ScrollPosition. Это структура, которая может хранить позицию как идентификатор, край или сырой оффсет.

Пример: кнопка для перехода к последнему сообщению в чате. Объявляем ScrollPosition, передаем в модификатор и вызываем scrollTo(id:) при нажатии.

struct ChatView: View {
let messages: [Message]
@State private var position = ScrollPosition()

var body: some View {
ScrollView {
LazyVStack {
ForEach(messages) { message in
MessageView(message: message)
.id(message.id)
}
}
}
.defaultScrollAnchor(.bottom)
.scrollPosition($position)
.onAppear {
position = ScrollPosition(id: messages.last?.id)
}
.safeAreaBar(edge: .bottom) {
Button {
withAnimation {
position.scrollTo(id: messages.last?.id)
}
} label: {
Text("Перейти к последнему")
}
}
}
}

Как читать текущую позицию:


ScrollPosition дает несколько свойств для чтения позиции. viewID показывает ID текущего видимого элемента. edge, point, x и y отражают значения, установленные программно. isPositionedByUser показывает, была ли позиция установлена пользователем.

Важный нюанс: edge, point, x и y обнуляются при взаимодействии пользователя. viewID - единственный способ отслеживать позицию во время ручной прокрутки.

Чтобы скрывать кнопку, когда пользователь уже внизу, можно использовать viewID(type:).

private var isAtBottom: Bool {
position.viewID(type: Int.self) == messages.last?.id
}

Для отслеживания сырого оффсета во время скролла используйте onScrollGeometryChange. Этот модификатор дает доступ к полной геометрии скролла и вызывает action при изменении выбранного значения.

Вывод:


SwiftUI прошел путь от ScrollViewReader с односторонним управлением до гибкого API с двусторонней связью. Теперь можно не только двигать скролл, но и читать его состояние.

Ключевые инструменты: defaultScrollAnchor для начальной позиции, scrollPosition для управления и чтения, onScrollGeometryChange для отслеживания геометрии.

API мощные, но с нюансами. viewID - единственный способ читать позицию при ручной прокрутке. edge, point, x и y работают только для программно установленных значений. Понимание этих различий помогает строить интерфейсы, которые естественно реагируют на действия пользователя.
20.07.2026 71 606