← All repositories

listless

A simple list app for Apple platforms

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


commit dc334b1feb015162fa79bcf4ed4385334eefeb78
parent b113eac1740c48ee5bb0eaed2176ebee67ae180b
Author: Michael Camilleri <[email protected]>
Date:   Wed, 25 Feb 2026 21:26:04 +0900

Improve failure case when dragging fails

Co-Authored-By: Codex GPT 5.3 <[email protected]>

Diffstat

MListless/Extensions/TaskListView+Logic.swift | 20++++++++++++++++----
MListless/Models/TaskStore.swift | 11+++++++++++
MListlessiOS/Extensions/TaskListView+Drag.swift | 8++++++--

3 files changed, 33 insertions(+), 6 deletions(-)

diff --git a/Listless/Extensions/TaskListView+Logic.swift b/Listless/Extensions/TaskListView+Logic.swift

@@ -89,7 +89,8 @@ extension TaskListView {
     func createTask(title: String, afterTaskID: UUID) {
         clearDragState()
         do {
-            let newTask = try store.createTask(title: title, sortOrder: sortOrderAfter(taskID: afterTaskID))
+            let sortOrder = try sortOrderAfter(taskID: afterTaskID)
+            let newTask = try store.createTask(title: title, sortOrder: sortOrder)
             selectedTaskID = newTask.id
             focusedField = .scrollView
         } catch {
@@ -97,14 +98,22 @@ extension TaskListView {
         }
     }
 
-    private func sortOrderAfter(taskID: UUID) -> Int64? {
+    private func sortOrderAfter(taskID: UUID) throws -> Int64? {
         guard let afterIndex = activeTasks.firstIndex(where: { $0.id == taskID }) else {
             return nil
         }
         let afterTask = activeTasks[afterIndex]
         if afterIndex + 1 < activeTasks.count {
             let nextTask = activeTasks[afterIndex + 1]
-            return (afterTask.sortOrder + nextTask.sortOrder) / 2
+            let midpoint = (afterTask.sortOrder + nextTask.sortOrder) / 2
+            if midpoint == afterTask.sortOrder {
+                // Consecutive sort orders leave no room; re-normalise with 1000-unit gaps
+                // then recompute. Core Data's identity map ensures afterTask/nextTask reflect
+                // the updated values immediately after normalisation.
+                try store.normalizeSortOrders()
+                return (afterTask.sortOrder + nextTask.sortOrder) / 2
+            }
+            return midpoint
         } else {
             return afterTask.sortOrder + 1000
         }
@@ -449,10 +458,13 @@ extension TaskListView {
 
         do {
             try store.moveTask(taskID: droppedUUID, toIndex: finalIndex)
+            clearDragState()
         } catch {
+            withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
+                clearDragState()
+            }
             presentStoreError(error)
         }
-        clearDragState()
 
         return true
     }

diff --git a/Listless/Models/TaskStore.swift b/Listless/Models/TaskStore.swift

@@ -111,6 +111,17 @@ final class TaskStore {
         try save()
     }
 
+    func normalizeSortOrders() throws {
+        let activeTasks = try fetchTasks().filter { !$0.isCompleted }
+            .sorted { $0.sortOrder < $1.sortOrder }
+
+        for (index, task) in activeTasks.enumerated() {
+            task.sortOrder = Int64(index) * 1000
+        }
+
+        try save()
+    }
+
     func moveTask(taskID: UUID, toIndex: Int) throws {
         let activeTasks = try fetchTasks().filter { !$0.isCompleted }
             .sorted { $0.sortOrder < $1.sortOrder }

diff --git a/ListlessiOS/Extensions/TaskListView+Drag.swift b/ListlessiOS/Extensions/TaskListView+Drag.swift

@@ -37,10 +37,14 @@ extension TaskListView {
         }
         do {
             try store.moveTask(taskID: draggedID, toIndex: finalIndex)
+            clearDragState()
+            isDragging = false
         } catch {
+            withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
+                clearDragState()
+                isDragging = false
+            }
             presentStoreError(error)
         }
-        clearDragState()
-        isDragging = false
     }
 }