Prefetch the next room list page before reaching the bottom

Growing the list only once the last row was already visible, on top of the half a
second visible range throttle, meant hitting and bouncing off the end of the list
while the next page loaded. Ask for the next page half a page early instead, so
the growth happens while the user is still scrolling towards the boundary.

The threshold is clamped so that it stays positive for accounts with fewer rooms
than half a page, where it would otherwise always match and leave the list unable
to shrink back to a single page.
This commit is contained in:
Stefan Ceriu
2026-08-18 16:57:42 +03:00
parent e7787f4c3f
commit 9dd9fd9176
2 changed files with 37 additions and 5 deletions
@@ -68,7 +68,10 @@ struct HomeScreenContent: View {
scrollViewAdapter.scrollView = scrollView
}
.onReceive(scrollViewAdapter.didScroll) { _ in
updateVisibleRange()
// Live, undelayed updates while scrolling: pagination needs to see the
// range early enough to grow the list before the user reaches the bottom,
// and the scroll geometry is valid mid-scroll.
sendVisibleRange()
}
.onReceive(scrollViewAdapter.isScrolling) { _ in
updateVisibleRange()
@@ -133,13 +136,21 @@ struct HomeScreenContent: View {
/// Often times the scroll view's content size isn't correct yet when this method is called e.g. when cancelling a search
/// Dispatch it with a delay to allow the UI to update and the computations to be correct
/// Once we move to iOS 17 we should remove all of this and use scroll anchors instead
/// Update: We're on iOS 26 now and the scroll achors still don't work properly.
private func updateVisibleRange() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { delayedUpdateVisibleRange() }
}
private func delayedUpdateVisibleRange() {
guard scrollViewAdapter.isScrolling.value == false else {
// Scrolling reports live through didScroll
return
}
sendVisibleRange()
}
private func sendVisibleRange() {
guard let scrollView = scrollViewAdapter.scrollView,
scrollViewAdapter.isScrolling.value == false, // Ignore while scrolling
context.searchQuery.isEmpty == true, // Ignore while filtering
!context.viewState.visibleRooms.isEmpty else {
return