До
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 работают только для программно установленных значений. Понимание этих различий помогает строить интерфейсы, которые естественно реагируют на действия пользователя.