fix: auto-scroll loads more when near bottom
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s

onAppear doesn't fire during programmatic scrolling (UIScrollView
contentOffset changes don't trigger SwiftUI lifecycle). Added
onNearBottom callback to ScrollViewDriver — fires when within 500pt
of bottom during auto-scroll tick. 3s cooldown prevents rapid-fire.

Auto-scroll no longer stops at bottom — idles at maxOffset while
loadMore fetches. When new entries arrive, contentSize grows and
scrolling resumes automatically.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yusuf Suleman
2026-04-04 13:39:55 -05:00
parent ae3b3f11bf
commit 1cfb729cae
2 changed files with 22 additions and 4 deletions

View File

@@ -40,7 +40,9 @@ struct EntryListView: View {
} else { } else {
ScrollView { ScrollView {
// Auto-scroll engine zero-size, drives parent UIScrollView // Auto-scroll engine zero-size, drives parent UIScrollView
ScrollViewDriver(isScrolling: $isAutoScrolling, speed: scrollSpeed) ScrollViewDriver(isScrolling: $isAutoScrolling, speed: scrollSpeed) {
Task { await vm.loadMore() }
}
.frame(width: 0, height: 0) .frame(width: 0, height: 0)
if isCardView { if isCardView {

View File

@@ -7,6 +7,7 @@ import UIKit
struct ScrollViewDriver: UIViewRepresentable { struct ScrollViewDriver: UIViewRepresentable {
@Binding var isScrolling: Bool @Binding var isScrolling: Bool
let speed: Double // 1.0 = 60pt/sec let speed: Double // 1.0 = 60pt/sec
var onNearBottom: (() -> Void)? = nil
func makeUIView(context: Context) -> UIView { func makeUIView(context: Context) -> UIView {
let view = DriverView() let view = DriverView()
@@ -21,6 +22,7 @@ struct ScrollViewDriver: UIViewRepresentable {
let coordinator = context.coordinator let coordinator = context.coordinator
coordinator.speed = speed coordinator.speed = speed
coordinator.isScrollingBinding = $isScrolling coordinator.isScrollingBinding = $isScrolling
coordinator.onNearBottom = onNearBottom
if isScrolling && coordinator.displayLink == nil { if isScrolling && coordinator.displayLink == nil {
coordinator.startScrolling(in: driver) coordinator.startScrolling(in: driver)
@@ -54,8 +56,10 @@ struct ScrollViewDriver: UIViewRepresentable {
var displayLink: CADisplayLink? var displayLink: CADisplayLink?
var speed: Double = 1.0 var speed: Double = 1.0
var isScrollingBinding: Binding<Bool>? var isScrollingBinding: Binding<Bool>?
var onNearBottom: (() -> Void)?
private var originalDelegate: UIScrollViewDelegate? private var originalDelegate: UIScrollViewDelegate?
private var delegateInstalled = false private var delegateInstalled = false
private var loadMoreTriggered = false
func findScrollView(from view: UIView) { func findScrollView(from view: UIView) {
var current: UIView? = view.superview var current: UIView? = view.superview
@@ -110,10 +114,22 @@ struct ScrollViewDriver: UIViewRepresentable {
originalDelegate?.scrollViewDidScroll?(sv) originalDelegate?.scrollViewDidScroll?(sv)
if newY >= maxOffset - 1 { // Trigger load more when within 500pt of bottom
stopAndNotify() let distanceToBottom = maxOffset - newY
if distanceToBottom < 500 && !loadMoreTriggered {
loadMoreTriggered = true
DispatchQueue.main.async { [weak self] in
self?.onNearBottom?()
// Reset after a delay so it can trigger again for the next page
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self?.loadMoreTriggered = false
} }
} }
}
// Don't stop at bottom contentSize may grow after loadMore.
// The tick keeps running; if no more content, it just idles at maxOffset.
}
private func stopAndNotify() { private func stopAndNotify() {
stopScrolling() stopScrolling()