Add custom item offset scrolling#615
Conversation
Expose a Listable-owned item scrolling API that lets callers compute a vertical content offset adjustment without accessing the scroll view.
|
🤖 @codex review |
| list.animation = .fast | ||
| list.scrollIndicatorInsets.bottom = 112.0 | ||
|
|
||
| list.autoScrollAction = .pin( |
| @discardableResult | ||
| public func scrollTo( | ||
| item : AnyIdentifier, | ||
| contentOffsetAdjustment : @escaping ListItemScrollPositionAdjustment, |
There was a problem hiding this comment.
I think it's okay that this is marked as escaping but making a note for myself
johnnewman-square
left a comment
There was a problem hiding this comment.
This is a neat enhancement. I left just a few notes that I think we could potentially explore.
There was a problem hiding this comment.
I received this feedback from Codex. I think we could potentially make a few adjustments here and add a unit test to capture this specific case:
In ListView.Delegate.swift, onDidEndScrollingAnimation now gets actions, so a client can start another scroll from inside that callback. But self.view.didEndScrolling() is called after the observer callback. If the callback starts an animated scroll with a completion, ListView.swift appends that new completion, and then the current “old scroll ended” event drains it immediately. So the completion can fire with stale position info before the newly requested scroll finishes.
The likely fix is to snapshot/drain existing scroll completions before invoking onDidEndScrollingAnimation, or otherwise make didEndScrolling() only process handlers that existed before this delegate callback began.
There was a problem hiding this comment.
🤖 [Codex] Addressed in bd63c5a. The delegate now drains the completion handlers that existed before onDidEndScrollingAnimation, runs the observer callback with actions, and then executes only that drained batch. I also added a regression test covering a new animated scroll started from the callback so its completion waits for the next scroll end.
| list.appearance = .demoAppearance | ||
| list.layout = .demoLayout | ||
| list.animation = .fast | ||
| list.scrollIndicatorInsets.bottom = 112.0 |
There was a problem hiding this comment.
We could replace the hardcoded inset with an example of a calculated inset that gets the scroll indicator closer to the top edge of the footer. Though, we'd need to call updateList in viewSafeAreaInsetsDidChange() for good measure. Up to you!
| list.scrollIndicatorInsets.bottom = 112.0 | |
| list.scrollIndicatorInsets.bottom = footer.bounds.height - view.safeAreaInsets.bottom |
There was a problem hiding this comment.
Good suggestion. This is more aligned with the target use case too 👍🏼
There was a problem hiding this comment.
🤖 [Codex] Addressed in c27fe33. The demo now calculates the bottom scroll indicator inset from the footer height minus the safe-area bottom inset, clamps it to zero, and re-runs updateList() from viewSafeAreaInsetsDidChange().
| list.autoScrollAction = .pin( | ||
| .item(targetIdentifier), | ||
| itemPosition: .verticalContentOffsetAdjustment { [weak self] info in | ||
| self?.footerAwareScrollDelta(for: info) ?? 0.0 | ||
| }, | ||
| animated: false, | ||
| scrollInterruptionPolicy: .deferDuringUserScrolling, | ||
| shouldPerform: { _ in true } | ||
| ) |
There was a problem hiding this comment.
I think we should explore the initial animations when the view is presented. When opening the demo, the list animates to the selected row when viewDidLayoutSubviews() calls updateList(). It would be a great enhancement if we could find a way around this issue in the demo example. Also curious if this is an issue in the production use case.
Here's a recording with slow animations, taken from an iOS 17.5 simulator.
Simulator.Screen.Recording.-.UI.Test.-.2026-05-27.at.09.53.29.mov
While investigating, I flipped animations to enabled by default in AutoScrollingViewController3 but opening that demo's controller didn't perform the same initial scroll there. I don't think this is an issue in the other demos that showcase pin and scrollTo because they don't have the viewDidLayoutSubviews pass.
There was a problem hiding this comment.
🤖 [Codex] Addressed in 67ade56. The demo now suppresses animation only for the first layout-driven updateList() pass when the screen is presented. Later row changes still honor the animation toggle.
There was a problem hiding this comment.
Sorry, AI missed here; let me explore addressing this workaround within the library itself.
There was a problem hiding this comment.
Actually, on second though, I do think this maneuver should be done within the client app.
| itemPosition: .verticalContentOffsetAdjustment { [weak self] info in | ||
| self?.footerAwareScrollDelta(for: info) ?? 0.0 | ||
| }, | ||
| animated: false, |
There was a problem hiding this comment.
A button in the navigation bar to toggle the animated flag would be a neat enhancement here, similar to some of the other autoscrolling demos.
Adds a Listable-owned API for custom vertical item positioning from both imperative
ListActionsand declarativeAutoScrollAction.What's changed
ListItemScrollPosition.standard(_:)and.verticalContentOffsetAdjustment(_:).AutoScrollAction.scrollTo(... itemPosition:)andpin(... itemPosition:), with existingposition:overloads preserved.AutoScrollAction.ScrollInterruptionPolicy:.performImmediately.deferDuringUserScrolling.skipDuringUserScrollingCustom Auto Scrolling (Footer-Aware Pin).Example
Use
.skipDuringUserScrollingwhen an auto-scroll request should be dropped instead of retried after user scrolling ends.Visuals
auto-scroll-vertical-offset-rotated.mov
Test Plan
mise exec -- xcodebuild test -workspace Development/ListableDevelopment.xcworkspace -scheme ListableDevelopment-Workspace -destination 'id=52DC49DC-0376-4B43-99BE-BC7A016A51B3' -derivedDataPath /tmp/listable-dd-development-tests -only-testing:ListableTestsgit diff --checkCustom Auto Scrolling (Footer-Aware Pin)demo on iOS SimulatorChecklist
Please do the following before merging:
Mainsection.