commit a940a5509db9407091a71ff3529ee816a5b21154
parent 5ac94bb9029e2c1e8e4e6e936eec2894ff4417ec
Author: Michael Camilleri <[email protected]>
Date: Fri, 27 Mar 2026 13:22:34 +0900
Improve scrolling during row reordering
Diffstat:
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)