listless

A simple list app for Apple platforms
Log | Files | Refs | README | LICENSE

commit a940a5509db9407091a71ff3529ee816a5b21154
parent 5ac94bb9029e2c1e8e4e6e936eec2894ff4417ec
Author: Michael Camilleri <[email protected]>
Date:   Fri, 27 Mar 2026 13:22:34 +0900

Improve scrolling during row reordering

Diffstat:
MListlessiOS/Extensions/ItemListView+Drag.swift | 28++++++++++++++++++++++++++++
MListlessiOS/Views/ItemListView.swift | 9+++++----
2 files changed, 33 insertions(+), 4 deletions(-)

diff --git a/ListlessiOS/Extensions/ItemListView+Drag.swift b/ListlessiOS/Extensions/ItemListView+Drag.swift @@ -17,6 +17,7 @@ extension ItemListView { withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { setDragOrder(order) } + revealAdjacentItem(order: order, draggedID: draggedID, fingerY: point.y) return } @@ -26,6 +27,33 @@ extension ItemListView { withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) { setDragOrder(order) } + revealAdjacentItem(order: order, draggedID: draggedID, fingerY: point.y) + return + } + + // No swap, but proactively scroll if finger is in an edge zone + revealAdjacentItem(order: order, draggedID: draggedID, fingerY: point.y) + } + + private func revealAdjacentItem(order: [UUID], draggedID: UUID, fingerY: CGFloat) { + guard let idx = order.firstIndex(of: draggedID) else { return } + + let now = CACurrentMediaTime() + guard now - layoutStorage.lastAutoScrollTime > 0.2 else { return } + + let screenHeight = UIScreen.main.bounds.height + let edgeZone: CGFloat = 120 + + if fingerY > screenHeight - edgeZone, idx + 1 < order.count { + layoutStorage.lastAutoScrollTime = now + withAnimation(.easeInOut(duration: 0.2)) { + scrollPosition.scrollTo(id: order[idx + 1], anchor: .bottom) + } + } else if fingerY < edgeZone, idx > 0 { + layoutStorage.lastAutoScrollTime = now + withAnimation(.easeInOut(duration: 0.2)) { + scrollPosition.scrollTo(id: order[idx - 1], anchor: .top) + } } } diff --git a/ListlessiOS/Views/ItemListView.swift b/ListlessiOS/Views/ItemListView.swift @@ -6,6 +6,7 @@ struct ItemListView: View, ItemListViewProtocol { var draggedRowWidth: CGFloat = 0 var draggedRowFrame: CGRect = .zero var contentBottomY: CGFloat = 0 + var lastAutoScrollTime: CFTimeInterval = 0 } struct InteractionStateData { @@ -56,6 +57,7 @@ struct ItemListView: View, ItemListViewProtocol { @State var pState = PullStateData() @State var isDragging = false @State var layoutStorage = LayoutStorage() + @State var scrollPosition = ScrollPosition() var focusedField: FocusField? { get { fState.focusedField } @@ -481,7 +483,6 @@ struct ItemListView: View, ItemListViewProtocol { private var itemScrollView: some View { ZStack(alignment: .top) { ScrollView { - ScrollViewReader { scrollProxy in VStack(alignment: .leading, spacing: vStackSpacing) { navigationHeader .padding(.bottom, 12) @@ -522,7 +523,7 @@ struct ItemListView: View, ItemListViewProtocol { id != draftPrependRowID { withAnimation { - scrollProxy.scrollTo(id) + scrollPosition.scrollTo(id: id) } } } @@ -530,12 +531,12 @@ struct ItemListView: View, ItemListViewProtocol { if let newID, draggedItemID == nil { guard newID != draftPrependRowID else { return } withAnimation { - scrollProxy.scrollTo(newID) + scrollPosition.scrollTo(id: newID) } } } - } } + .scrollPosition($scrollPosition) .scrollDisabled(draggedItemID != nil || iState.isSwiping) .scrollBounceBehavior(.always) .contentMargins(.bottom, 20)