73. Отслеживаем окончание скролла
Недавно делал экран, для которого нужно распознавать момент окончания скролла. В UIKit есть scrollViewDidEndDecelerating, а в SwiftUI такого пока нет, но есть обходное решение с использованием Combine и PreferenceKey.
Демо готового экрана
На гифке по центру экрана видно 2 текста, первый из которых показывает актуальный оффсет, а второй - оффсет в момент "остановки" (позже будет понятно, для чего нужны кавычки).
Решение
За решение призываю поставить лайк этому комментарию на SO.
Хитрость заключается в том, что мы при помощи Combine отслеживаем все изменения оффсета скролла, дожидаемся пока изменения перестанут поступать, ждем еще 0.2 секунды и считаем, что скролл остановился.
Преимущество такого решения - работает на iOS 13+
Краткая версия кода
struct ScrollViewDidEndScrollingExample: View {
/// Распознаватель изменения в оффсете
private let detector: CurrentValueSubject<CGFloat, Never>
/// Публикатор сообщений от распознавателя (с дебаунсом в 0.2 сек)
private let publisher: AnyPublisher<CGFloat, Never>
private let scrollId = "ScrollID"
@State private var stopOffset = CGFloat.zero
init() {
// Настраиваем отслеживание оффсета
let detector = CurrentValueSubject<CGFloat, Never>(0)
self.publisher = detector
.debounce(for: .seconds(0.2), scheduler: DispatchQueue.main)
.dropFirst() // Пропускаем первое событие при появлении экрана
.eraseToAnyPublisher()
self.detector = detector
}
var body: some View {
ScrollView {
VStack(spacing: 20) {
// демо-список
}
.background(
// Настраиваем отслеживание оффсета
GeometryReader {
Color.clear.preference(
key: ViewOffsetKey.self,
value: -$0.frame(in: .named(scrollId)).origin.y
)
}
)
// Публикуем изменения оффсета
.onPreferenceChange(ViewOffsetKey.self, perform: detector.send)
}
// Задаем идентификатор для координатной плоскости
.coordinateSpace(name: scrollId)
.onReceive(publisher) { finalOffset in
// обрабатываем "окончание" скролла
}
}
}
Подводный камень
Вместе с "хитростью" приходит и побочный эффект, о котором нужно помнить: пользователь может не отпускать палец от экрана, но перестать скроллить - тогда мы тоже будем считать, что скролл закончился:
Заключение
Несмотря на отсутствие всех необходимых инструментов для работы со скроллом в iOS < 18 (сравниваю с UIKit), мы все-равно достаточно успешно справляемся с рабочими задачами на SwiftUI 😁
Кстати, в iOS 18 эта задача будет решаться при помощи onScrollPhaseChange.
Код для этой статьи можно посмотреть тут, а другие статьи - тут.