← All repositories

listless

A simple list app for Apple platforms

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


commit 0cbbf73afb234730b23a1229d16969827c2422eb
parent 3593ebb10a03e6a706c7afc55ccb7c927bb8d02b
Author: Michael Camilleri <[email protected]>
Date:   Thu,  5 Feb 2026 20:21:02 +0900

Task creation apparently working

Co-Authored-By: Claude 4.5 Sonnet <[email protected]>

Diffstat

MListless.xcodeproj/project.pbxproj | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MListless.xcodeproj/xcshareddata/xcschemes/Listless macOS.xcscheme | 13+++++++++++++
MListless/Views/TaskListView.swift | 104++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
MListless/Views/TaskRowView.swift | 4++++
MListlessMac/ListlessMacApp.swift | 8+++++++-
MListlessMac/Views/TaskRowDragGesture.swift | 57+++++++++++++++++++++------------------------------------
MListlessiOS/Views/TaskRowDragGesture.swift | 39+++------------------------------------

7 files changed, 234 insertions(+), 101 deletions(-)

diff --git a/Listless.xcodeproj/project.pbxproj b/Listless.xcodeproj/project.pbxproj

@@ -11,6 +11,7 @@
 		15B71073767FB4766A6BA2BE /* HoverCursorModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D41D6E0CED14D79F31C45062 /* HoverCursorModifier.swift */; };
 		3ABE52A15C2059D8D5570528 /* TaskStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3DEE364304587D280C5672 /* TaskStore.swift */; };
 		42E4CDE1D17463554CC4F41F /* TaskListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537A913AC421BAEF60D26D9C /* TaskListView.swift */; };
+		585A9E665730E3C4FD32AE8D /* KeyboardNavigationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F99C67C30185163A51DCD268 /* KeyboardNavigationTests.swift */; };
 		5B60B409CE4BA668DB30A65D /* Listless.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = C093494053E6C348F245D4EC /* Listless.xcdatamodeld */; };
 		614FCCA450EC0BFFD8B40640 /* ListlessMacApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA467DF2E59BDBE6EEF6A7D /* ListlessMacApp.swift */; };
 		6C252050E62AED3A0A684EBF /* KeyboardNavigationModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7B37EF6A2389656D105FF8 /* KeyboardNavigationModifier.swift */; };
@@ -33,6 +34,16 @@
 		FEC96DAA2BF8BB5D6D8504EB /* HoverCursorModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82A3509AD32A54434BCC8017 /* HoverCursorModifier.swift */; };
 /* End PBXBuildFile section */
 
+/* Begin PBXContainerItemProxy section */
+		CBF0AD7D47832BDE6A4FB05B /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 3256C2BF8F1DAF371DA32120 /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 0FB4F07A37999BBC6DFE4DBB;
+			remoteInfo = "Listless macOS";
+		};
+/* End PBXContainerItemProxy section */
+
 /* Begin PBXFileReference section */
 		007D500D2EFF3E66B780ADE0 /* TaskRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskRowView.swift; sourceTree = "<group>"; };
 		01E141436176F83594E2F26B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
@@ -42,6 +53,7 @@
 		1B3D0B4710C7A0EE4F227851 /* TaskRowDragGesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskRowDragGesture.swift; sourceTree = "<group>"; };
 		1DA467DF2E59BDBE6EEF6A7D /* ListlessMacApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListlessMacApp.swift; sourceTree = "<group>"; };
 		3313FEDB101EECA4B344EEF4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
+		3F0FC3AFB591F222FB63C13B /* Listless macOS UITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Listless macOS UITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
 		48AE1AE43296C1692FA6F755 /* PlatformScrollIndicatorsModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformScrollIndicatorsModifier.swift; sourceTree = "<group>"; };
 		537A913AC421BAEF60D26D9C /* TaskListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskListView.swift; sourceTree = "<group>"; };
 		5B0E22B8F7B2B7283CAF749E /* Listless macOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Listless macOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -60,6 +72,7 @@
 		D41D6E0CED14D79F31C45062 /* HoverCursorModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoverCursorModifier.swift; sourceTree = "<group>"; };
 		DC3DEE364304587D280C5672 /* TaskStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskStore.swift; sourceTree = "<group>"; };
 		EF6B0195EA0176B72DE9B092 /* PlatformTextFieldWidthModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformTextFieldWidthModifier.swift; sourceTree = "<group>"; };
+		F99C67C30185163A51DCD268 /* KeyboardNavigationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardNavigationTests.swift; sourceTree = "<group>"; };
 		FBB8A3BEB346267B30B4675F /* TaskItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskItem.swift; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
@@ -79,6 +92,7 @@
 			isa = PBXGroup;
 			children = (
 				126108860D7878DDC3BECC4B /* Listless iOS.app */,
+				3F0FC3AFB591F222FB63C13B /* Listless macOS UITests.xctest */,
 				5B0E22B8F7B2B7283CAF749E /* Listless macOS.app */,
 			);
 			name = Products;
@@ -128,6 +142,14 @@
 			path = ListlessiOS;
 			sourceTree = "<group>";
 		};
+		A4CAFE5C963E75C444972E10 /* ListlessMacUITests */ = {
+			isa = PBXGroup;
+			children = (
+				F99C67C30185163A51DCD268 /* KeyboardNavigationTests.swift */,
+			);
+			path = ListlessMacUITests;
+			sourceTree = "<group>";
+		};
 		AA563E991F69ED14DCD9A1AB /* Infrastructure */ = {
 			isa = PBXGroup;
 			children = (
@@ -173,6 +195,7 @@
 				58051CBDE2390F9E13647235 /* Listless */,
 				954AAB8DDFF6E6D6FD6A0A2C /* ListlessiOS */,
 				D5B197AFF26144948D032299 /* ListlessMac */,
+				A4CAFE5C963E75C444972E10 /* ListlessMacUITests */,
 				3936BDEE64D16E6C4C85B3DD /* Products */,
 			);
 			sourceTree = "<group>";
@@ -214,6 +237,24 @@
 			productReference = 126108860D7878DDC3BECC4B /* Listless iOS.app */;
 			productType = "com.apple.product-type.application";
 		};
+		4DDF5F8F5508AA35A41BECAB /* Listless macOS UITests */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 9FF482EB568895AF0B3F35CC /* Build configuration list for PBXNativeTarget "Listless macOS UITests" */;
+			buildPhases = (
+				34DA2E4A88D1AFCE78FCE279 /* Sources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				180D7333E25210955007E660 /* PBXTargetDependency */,
+			);
+			name = "Listless macOS UITests";
+			packageProductDependencies = (
+			);
+			productName = "Listless macOS UITests";
+			productReference = 3F0FC3AFB591F222FB63C13B /* Listless macOS UITests.xctest */;
+			productType = "com.apple.product-type.bundle.ui-testing";
+		};
 /* End PBXNativeTarget section */
 
 /* Begin PBXProject section */
@@ -231,6 +272,11 @@
 						DevelopmentTeam = 7TD7PZBNXP;
 						ProvisioningStyle = Automatic;
 					};
+					4DDF5F8F5508AA35A41BECAB = {
+						DevelopmentTeam = 7TD7PZBNXP;
+						ProvisioningStyle = Automatic;
+						TestTargetID = 0FB4F07A37999BBC6DFE4DBB;
+					};
 				};
 			};
 			buildConfigurationList = CAACA40A09D5F78ECE7A0EDF /* Build configuration list for PBXProject "Listless" */;
@@ -249,11 +295,20 @@
 			targets = (
 				34A03D42B91730DEAC2EBD8E /* Listless iOS */,
 				0FB4F07A37999BBC6DFE4DBB /* Listless macOS */,
+				4DDF5F8F5508AA35A41BECAB /* Listless macOS UITests */,
 			);
 		};
 /* End PBXProject section */
 
 /* Begin PBXSourcesBuildPhase section */
+		34DA2E4A88D1AFCE78FCE279 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				585A9E665730E3C4FD32AE8D /* KeyboardNavigationTests.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 		409D108909CBEC2F69B56D0E /* Sources */ = {
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
@@ -294,7 +349,34 @@
 		};
 /* End PBXSourcesBuildPhase section */
 
+/* Begin PBXTargetDependency section */
+		180D7333E25210955007E660 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 0FB4F07A37999BBC6DFE4DBB /* Listless macOS */;
+			targetProxy = CBF0AD7D47832BDE6A4FB05B /* PBXContainerItemProxy */;
+		};
+/* End PBXTargetDependency section */
+
 /* Begin XCBuildConfiguration section */
+		086CB8FDA1EC9D4A988C382E /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CODE_SIGN_STYLE = Automatic;
+				COMBINE_HIDPI_IMAGES = YES;
+				DEVELOPMENT_TEAM = 7TD7PZBNXP;
+				GENERATE_INFOPLIST_FILE = YES;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+					"@loader_path/Frameworks",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = net.inqk.listless.mac.uitests;
+				SDKROOT = macosx;
+				TEST_TARGET_NAME = "Listless macOS";
+			};
+			name = Release;
+		};
 		2D4E3CC5FF8E6299F754CCFC /* Debug */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
@@ -313,6 +395,25 @@
 			};
 			name = Debug;
 		};
+		31AB47694981749AC7C9E541 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				BUNDLE_LOADER = "$(TEST_HOST)";
+				CODE_SIGN_STYLE = Automatic;
+				COMBINE_HIDPI_IMAGES = YES;
+				DEVELOPMENT_TEAM = 7TD7PZBNXP;
+				GENERATE_INFOPLIST_FILE = YES;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+					"@loader_path/Frameworks",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = net.inqk.listless.mac.uitests;
+				SDKROOT = macosx;
+				TEST_TARGET_NAME = "Listless macOS";
+			};
+			name = Debug;
+		};
 		A5A377EA0FE470803E2B6BA1 /* Release */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
@@ -500,6 +601,15 @@
 			defaultConfigurationIsVisible = 0;
 			defaultConfigurationName = Debug;
 		};
+		9FF482EB568895AF0B3F35CC /* Build configuration list for PBXNativeTarget "Listless macOS UITests" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				31AB47694981749AC7C9E541 /* Debug */,
+				086CB8FDA1EC9D4A988C382E /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Debug;
+		};
 		B7581C7E1D90658668F3AAE2 /* Build configuration list for PBXNativeTarget "Listless iOS" */ = {
 			isa = XCConfigurationList;
 			buildConfigurations = (

diff --git a/Listless.xcodeproj/xcshareddata/xcschemes/Listless macOS.xcscheme b/Listless.xcodeproj/xcshareddata/xcschemes/Listless macOS.xcscheme

@@ -39,7 +39,20 @@
          </BuildableReference>
       </MacroExpansion>
       <Testables>
+         <TestableReference
+            skipped = "NO"
+            parallelizable = "NO">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "4DDF5F8F5508AA35A41BECAB"
+               BuildableName = "Listless macOS UITests.xctest"
+               BlueprintName = "Listless macOS UITests"
+               ReferencedContainer = "container:Listless.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
       </Testables>
+      <CommandLineArguments>
+      </CommandLineArguments>
    </TestAction>
    <LaunchAction
       buildConfiguration = "Debug"

diff --git a/Listless/Views/TaskListView.swift b/Listless/Views/TaskListView.swift

@@ -21,6 +21,7 @@ struct TaskListView: View {
     @State private var draggedTaskID: UUID?
     @State private var visualOrder: [UUID]?
     @State private var editingTaskID: UUID?
+    @State private var justCreatedTaskID: UUID?
 
     init(store: TaskStore = TaskStore()) {
         _store = State(wrappedValue: store)
@@ -38,19 +39,20 @@ struct TaskListView: View {
                 }
 
                 ForEach(completedTasks) { task in
+                    let taskID = task.id
                     TaskRowView(
                         task: task,
-                        taskID: task.id,
-                        isSelected: selectedTaskID == task.id,
-                        isEditing: editingTaskID == task.id,
+                        taskID: taskID,
+                        isSelected: selectedTaskID == taskID,
+                        isEditing: editingTaskID == taskID,
                         focusedField: $focusedField,
                         onToggle: toggleCompletion(_:),
                         onSubmit: handleSubmit(_:),
                         onTitleChange: updateTitle(_:_:),
                         onDelete: deleteTask(_:),
-                        onSelect: { selectTask(task.id) },
-                        onStartEdit: { startEditing(task.id) },
-                        onEndEdit: { endEditing() }
+                        onSelect: { selectTask(taskID) },
+                        onStartEdit: { startEditing(taskID) },
+                        onEndEdit: { endEditing(taskID) }
                     )
                 }
 
@@ -60,26 +62,26 @@ struct TaskListView: View {
                 }
 
                 ForEach(displayActiveTasks) { task in
+                    let taskID = task.id
                     TaskRowView(
                         task: task,
-                        taskID: task.id,
-                        isSelected: selectedTaskID == task.id,
-                        isEditing: editingTaskID == task.id,
+                        taskID: taskID,
+                        isSelected: selectedTaskID == taskID,
+                        isEditing: editingTaskID == taskID,
                         focusedField: $focusedField,
                         onToggle: toggleCompletion(_:),
                         onSubmit: handleSubmit(_:),
                         onTitleChange: updateTitle(_:_:),
                         onDelete: deleteTask(_:),
-                        onSelect: { selectTask(task.id) },
-                        onStartEdit: { startEditing(task.id) },
-                        onEndEdit: { endEditing() }
+                        onSelect: { selectTask(taskID) },
+                        onStartEdit: { startEditing(taskID) },
+                        onEndEdit: { endEditing(taskID) }
+                    )
+                    .taskDragGesture(
+                        isActive: !task.isCompleted,
+                        taskID: task.id,
+                        onDragStart: { startDrag(taskID: task.id) }
                     )
-                    .onDrag {
-                        startDrag(taskID: task.id)
-                        return NSItemProvider(object: task.id.uuidString as NSString)
-                    } preview: {
-                        Color.clear.frame(width: 1, height: 1)
-                    }
                     .overlay {
                         if draggedTaskID != nil && draggedTaskID != task.id {
                             VStack(spacing: 0) {
@@ -140,6 +142,7 @@ struct TaskListView: View {
         .focusable()
         .focused($focusedField, equals: .scrollView)
         .focusEffectDisabled()
+        .accessibilityIdentifier("task-list-scrollview")
         .keyboardNavigation(
             onUpArrow: navigateUp,
             onDownArrow: navigateDown,
@@ -180,13 +183,33 @@ struct TaskListView: View {
     }
 
     private func createTaskAndFocus() {
+        // Clear any lingering drag state
+        draggedTaskID = nil
+        visualOrder = nil
+
+        // Create Core Data task
         let task = store.createTask(title: "")
+
+        // Protect from immediate deletion
+        justCreatedTaskID = task.id
         selectedTaskID = task.id
-        startEditing(task.id)
+
+        // Set editing state to trigger TextField render
+        editingTaskID = task.id
+
+        // Wait for view to update, then focus the TextField
+        DispatchQueue.main.async {
+            self.focusedField = .task(task.id)
+            // Clear the just-created protection once focused
+            self.justCreatedTaskID = nil
+        }
     }
 
     private func handleBackgroundTap() {
-        if focusedField != nil || selectedTaskID != nil {
+        // Check if a task is focused (not just scrollView)
+        let isTaskFocused = if case .task = focusedField { true } else { false }
+
+        if isTaskFocused || selectedTaskID != nil {
             focusScrollView()
             selectedTaskID = nil
         } else {
@@ -208,7 +231,14 @@ struct TaskListView: View {
     }
 
     private func deleteIfEmpty(taskID: UUID) {
-        guard let task = tasks.first(where: { $0.id == taskID }) else { return }
+        // Don't delete tasks that were just created
+        if taskID == justCreatedTaskID {
+            return
+        }
+
+        guard let task = tasks.first(where: { $0.id == taskID }) else {
+            return
+        }
         let trimmedTitle = task.title.trimmingCharacters(in: .whitespacesAndNewlines)
         guard trimmedTitle.isEmpty else { return }
         deleteTask(task)
@@ -221,6 +251,11 @@ struct TaskListView: View {
     private func updateTitle(_ task: TaskItem, _ title: String) {
         guard task.title != title else { return }
         store.update(taskID: task.id, title: title)
+
+        // Clear the justCreated flag once user starts typing
+        if task.id == justCreatedTaskID && !title.isEmpty {
+            justCreatedTaskID = nil
+        }
     }
 
     private func toggleCompletion(_ task: TaskItem) {
@@ -247,7 +282,9 @@ struct TaskListView: View {
     }
 
     private func navigateUp() -> KeyPress.Result {
-        guard focusedField == .scrollView else { return .ignored }
+        guard focusedField == .scrollView else {
+            return .ignored
+        }
 
         guard let currentID = selectedTaskID else {
             selectedTaskID = activeTasks.last?.id
@@ -302,8 +339,12 @@ struct TaskListView: View {
     }
 
     private func unfocusTextField() -> KeyPress.Result {
-        guard case .task = focusedField else { return .ignored }
-        endEditing()
+        guard case .task = focusedField else {
+            return .ignored
+        }
+        if let editingID = editingTaskID {
+            endEditing(editingID)
+        }
         focusScrollView()
         return .handled
     }
@@ -311,7 +352,11 @@ struct TaskListView: View {
     // MARK: - Focus Management
 
     private func focusScrollView() {
-        focusedField = .scrollView
+        // Try clearing focus first, then setting to scrollView
+        focusedField = nil
+        DispatchQueue.main.async {
+            self.focusedField = .scrollView
+        }
     }
 
     private func focusTextField(_ taskID: UUID) {
@@ -323,10 +368,13 @@ struct TaskListView: View {
         focusedField = .task(taskID)
     }
 
-    private func endEditing() {
-        if let editingID = editingTaskID {
-            deleteIfEmpty(taskID: editingID)
+    private func endEditing(_ taskID: UUID) {
+        // Only clear editingTaskID if it matches this task
+        guard editingTaskID == taskID else {
+            return
         }
+
+        deleteIfEmpty(taskID: taskID)
         editingTaskID = nil
     }
 

diff --git a/Listless/Views/TaskRowView.swift b/Listless/Views/TaskRowView.swift

@@ -54,6 +54,8 @@ struct TaskRowView: View {
                     .frame(width: 20, height: 20)
             }
             .buttonStyle(.borderless)
+            .accessibilityIdentifier("task-checkbox")
+            .accessibilityValue(task.isCompleted ? "checkmark.circle.fill" : "circle")
 
             if isEditing {
                 TextField("New task", text: $editingTitle, axis: .vertical)
@@ -66,6 +68,7 @@ struct TaskRowView: View {
                     }
                     .frame(maxWidth: .infinity, alignment: .leading)
                     .disabled(task.isCompleted)
+                    .accessibilityIdentifier("task-textfield")
             } else {
                 HStack(spacing: 0) {
                     Text(task.title.isEmpty ? "New task" : task.title)
@@ -78,6 +81,7 @@ struct TaskRowView: View {
                                 onStartEdit()
                             }
                         }
+                        .accessibilityIdentifier("task-text-\(task.title)")
 
                     Spacer(minLength: 0)
                 }

diff --git a/ListlessMac/ListlessMacApp.swift b/ListlessMac/ListlessMacApp.swift

@@ -2,7 +2,13 @@ import SwiftUI
 
 @main
 struct ListlessMacApp: App {
-    private let persistenceController = PersistenceController.shared
+    private let persistenceController: PersistenceController
+
+    init() {
+        // Use in-memory storage during UI tests
+        let isUITesting = ProcessInfo.processInfo.arguments.contains("UI_TESTING")
+        persistenceController = isUITesting ? PersistenceController(inMemory: true) : .shared
+    }
 
     var body: some Scene {
         WindowGroup {

diff --git a/ListlessMac/Views/TaskRowDragGesture.swift b/ListlessMac/Views/TaskRowDragGesture.swift

@@ -5,18 +5,12 @@ extension View {
     func taskDragGesture(
         isActive: Bool,
         taskID: UUID,
-        taskTitle: String,
-        onDragStart: @escaping () -> Void,
-        onDragEnd: @escaping () -> Void,
-        onDrop: @escaping (UUID) -> Void
+        onDragStart: @escaping () -> Void
     ) -> some View {
         self.modifier(TaskRowDragGesture(
             isActive: isActive,
             taskID: taskID,
-            taskTitle: taskTitle,
-            onDragStart: onDragStart,
-            onDragEnd: onDragEnd,
-            onDrop: onDrop
+            onDragStart: onDragStart
         ))
     }
 }
@@ -24,46 +18,37 @@ extension View {
 struct TaskRowDragGesture: ViewModifier {
     let isActive: Bool
     let taskID: UUID
-    let taskTitle: String
     let onDragStart: () -> Void
-    let onDragEnd: () -> Void
-    let onDrop: (UUID) -> Void
+
+    @State private var dragStarted = false
 
     func body(content: Content) -> some View {
         if isActive {
             content
                 .onDrag {
-                    onDragStart()
+                    if !dragStarted {
+                        dragStarted = true
+                        onDragStart()
+                    }
                     return NSItemProvider(object: taskID.uuidString as NSString)
                 } preview: {
-                    dragPreview
-                }
-                .dropDestination(for: String.self) { items, location in
-                    guard let droppedUUIDString = items.first,
-                          let droppedUUID = UUID(uuidString: droppedUUIDString) else {
-                        return false
-                    }
-                    DispatchQueue.main.async {
-                        onDrop(droppedUUID)
-                        onDragEnd()
-                    }
-                    return true
+                    Color.clear.frame(width: 1, height: 1)
                 }
+                .simultaneousGesture(
+                    DragGesture(minimumDistance: 2)
+                        .onChanged { _ in
+                            if !dragStarted {
+                                dragStarted = true
+                                onDragStart()
+                            }
+                        }
+                        .onEnded { _ in
+                            dragStarted = false
+                        }
+                )
         } else {
             content
         }
     }
 
-    private var dragPreview: some View {
-        HStack(spacing: 12) {
-            Image(systemName: "circle")
-                .frame(width: 20, height: 20)
-            Text(taskTitle.isEmpty ? "New task" : taskTitle)
-                .font(.body)
-        }
-        .padding(.vertical, 8)
-        .padding(.horizontal, 16)
-        .background(Color(nsColor: .controlBackgroundColor))
-        .cornerRadius(6)
-    }
 }

diff --git a/ListlessiOS/Views/TaskRowDragGesture.swift b/ListlessiOS/Views/TaskRowDragGesture.swift

@@ -5,18 +5,12 @@ extension View {
     func taskDragGesture(
         isActive: Bool,
         taskID: UUID,
-        taskTitle: String,
-        onDragStart: @escaping () -> Void,
-        onDragEnd: @escaping () -> Void,
-        onDrop: @escaping (UUID) -> Void
+        onDragStart: @escaping () -> Void
     ) -> some View {
         self.modifier(TaskRowDragGesture(
             isActive: isActive,
             taskID: taskID,
-            taskTitle: taskTitle,
-            onDragStart: onDragStart,
-            onDragEnd: onDragEnd,
-            onDrop: onDrop
+            onDragStart: onDragStart
         ))
     }
 }
@@ -24,10 +18,7 @@ extension View {
 struct TaskRowDragGesture: ViewModifier {
     let isActive: Bool
     let taskID: UUID
-    let taskTitle: String
     let onDragStart: () -> Void
-    let onDragEnd: () -> Void
-    let onDrop: (UUID) -> Void
 
     func body(content: Content) -> some View {
         if isActive {
@@ -36,34 +27,10 @@ struct TaskRowDragGesture: ViewModifier {
                     onDragStart()
                     return NSItemProvider(object: taskID.uuidString as NSString)
                 } preview: {
-                    dragPreview
-                }
-                .dropDestination(for: String.self) { items, location in
-                    guard let droppedUUIDString = items.first,
-                          let droppedUUID = UUID(uuidString: droppedUUIDString) else {
-                        return false
-                    }
-                    DispatchQueue.main.async {
-                        onDrop(droppedUUID)
-                        onDragEnd()
-                    }
-                    return true
+                    Color.clear.frame(width: 1, height: 1)
                 }
         } else {
             content
         }
     }
-
-    private var dragPreview: some View {
-        HStack(spacing: 12) {
-            Image(systemName: "circle")
-                .frame(width: 20, height: 20)
-            Text(taskTitle.isEmpty ? "New task" : taskTitle)
-                .font(.body)
-        }
-        .padding(.vertical, 8)
-        .padding(.horizontal, 16)
-        .background(Color(uiColor: .systemBackground))
-        .cornerRadius(6)
-    }
 }