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:
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
}
}