← All repositories

listless

A simple list app for Apple platforms

git clone https://code.inqk.net/listless.git


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)