← All repositories

listless

A simple list app for Apple platforms

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


commit b6947ab9c1df71ddd081901fa2f227f32cbd615a
parent 81a1c8a1d2d850c85d8d55406f7dde29d9a43489
Author: Michael Camilleri <[email protected]>
Date:   Fri, 13 Mar 2026 00:28:00 +0900

Use draft task abstraction for new rows

Previous to this commit, the project used a 'phantom row' abstraction in
certain places where a new row was added but not in others. This commit
replaces the inconsistent approach with a consistent 'draft row'
abstraction. This same abstraction is used across iOS and macOS
versions.

Co-Authored-By: Claude 4.6 Opus <[email protected]>
Co-Authored-By: Codex GPT 4.6 <[email protected]>

Diffstat

MListless/Extensions/TaskListView+Logic.swift | 120++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
MListless/Helpers/TaskListTypes.swift | 8++++++++
MListless/Helpers/TaskListViewProtocol.swift | 3+++
MListlessMac/Helpers/ClickableTextField.swift | 4++++
MListlessMac/Views/TaskListView.swift | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MListlessiOS/Extensions/TaskListView+PullToCreate.swift | 50+++-----------------------------------------------
MListlessiOS/Helpers/TappableTextField.swift | 7+++++++
MListlessiOS/Views/TaskListView.swift | 168+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------

8 files changed, 351 insertions(+), 95 deletions(-)

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

@@ -58,38 +58,98 @@ extension TaskListViewProtocol {
         return lastTask.id == taskID
     }
 
+    func draftTaskID(for placement: DraftTaskPlacement) -> UUID {
+        switch placement {
+        case .prepend:
+            draftPrependRowID
+        case .append:
+            draftAppendRowID
+        }
+    }
+
+    func draftTaskPlacement(for taskID: UUID) -> DraftTaskPlacement? {
+        switch taskID {
+        case draftPrependRowID:
+            .prepend
+        case draftAppendRowID:
+            .append
+        default:
+            nil
+        }
+    }
+
     // MARK: - Task Creation
 
     func createNewTaskAtTop() -> UUID {
-        clearDragState()
-        do {
-            managedObjectContext.undoManager?.disableUndoRegistration()
-            let task = try store.createTask(title: "", atBeginning: true)
-            managedObjectContext.undoManager?.enableUndoRegistration()
-            pendingFocus = .task(task.id)
-            focusedField = .task(task.id)
-            selectedTaskID = task.id
-            return task.id
-        } catch {
-            managedObjectContext.undoManager?.enableUndoRegistration()
-            presentStoreError(error)
-            return UUID()
-        }
+        revealDraftTask(at: .prepend)
+        return draftPrependRowID
     }
 
     func createNewTask() {
+        revealDraftTask(at: .append)
+    }
+
+    func revealDraftTask(at placement: DraftTaskPlacement) {
+        if draftTaskPlacement != placement, draftTaskPlacement != nil {
+            commitDraftTask()
+        }
+
         clearDragState()
+        let taskID = draftTaskID(for: placement)
+        draftTaskTitle = ""
+        draftTaskPlacement = placement
+        pendingFocus = .task(taskID)
+        focusedField = .task(taskID)
+        selectedTaskID = taskID
+    }
+
+    func beginDraftTaskEditing(_ placement: DraftTaskPlacement) {
+        guard draftTaskPlacement == placement else { return }
+        let taskID = draftTaskID(for: placement)
+        selectedTaskID = taskID
+        if case .task(let id) = pendingFocus, id == taskID {
+            pendingFocus = nil
+        }
+    }
+
+    func commitDraftTask(shouldCreateNewTask: Bool = false) {
+        guard let placement = draftTaskPlacement else { return }
+        let taskID = draftTaskID(for: placement)
+        let title = draftTaskTitle.trimmingCharacters(in: .whitespacesAndNewlines)
+
+        // Clear pendingFocus before clearDraftTaskUI so that the iOS
+        // onChange(of: focusedFieldBinding) nil-redirect doesn't re-focus
+        // the draft row via a stale pendingFocus value.
+        if case .task(let id) = pendingFocus, id == taskID {
+            pendingFocus = nil
+        }
+
+        clearDraftTaskUI(at: placement, hasTitle: !title.isEmpty)
+
+        if selectedTaskID == taskID {
+            selectedTaskID = nil
+        }
+
+        guard !title.isEmpty else { return }
+
         do {
-            managedObjectContext.undoManager?.disableUndoRegistration()
-            let task = try store.createTask(title: "")
-            managedObjectContext.undoManager?.enableUndoRegistration()
-            pendingFocus = .task(task.id)
-            focusedField = .task(task.id)
-            selectedTaskID = task.id
+            let task = switch placement {
+            case .prepend:
+                try store.createTask(title: title, atBeginning: true)
+            case .append:
+                try store.createTask(title: title)
+            }
+            try store.save()
+            if placement == .append {
+                selectedTaskID = task.id
+            }
         } catch {
-            managedObjectContext.undoManager?.enableUndoRegistration()
             presentStoreError(error)
         }
+
+        if shouldCreateNewTask, placement == .append {
+            revealDraftTask(at: .append)
+        }
     }
 
     func createTask(title: String, afterTaskID: UUID) {
@@ -132,10 +192,14 @@ extension TaskListViewProtocol {
         let isTaskFocused = if case .task = focusedField { true } else { false }
 
         if isTaskFocused || selectedTaskID != nil {
+            pendingFocus = nil
+            if draftTaskPlacement != nil {
+                commitDraftTask()
+            }
             selectedTaskID = nil
             focusedField = nil
         } else {
-            createNewTask()
+            revealDraftTask(at: .append)
         }
     }
 
@@ -146,6 +210,11 @@ extension TaskListViewProtocol {
         guard oldID != newID, let oldID else {
             return
         }
+
+        if draftTaskPlacement(for: oldID) != nil {
+            return
+        }
+
         deleteIfEmpty(taskID: oldID)
     }
 
@@ -353,6 +422,11 @@ extension TaskListViewProtocol {
     }
 
     func endEditing(_ taskID: UUID, shouldCreateNewTask: Bool) {
+        if draftTaskPlacement(for: taskID) != nil {
+            commitDraftTask(shouldCreateNewTask: shouldCreateNewTask)
+            return
+        }
+
         do {
             try store.save()
         } catch {
@@ -366,7 +440,7 @@ extension TaskListViewProtocol {
             selectedTaskID = nil
             deleteIfEmpty(taskID: taskID)
         } else if wasLastActiveTask && shouldCreateNewTask {
-            createNewTask()
+            revealDraftTask(at: .append)
         } else if shouldCreateNewTask {
             focusedField = .scrollView
         }

diff --git a/Listless/Helpers/TaskListTypes.swift b/Listless/Helpers/TaskListTypes.swift

@@ -9,3 +9,11 @@ enum DragState: Equatable {
     case idle
     case dragging(id: UUID, order: [UUID])
 }
+
+enum DraftTaskPlacement: Equatable {
+    case prepend
+    case append
+}
+
+let draftPrependRowID = UUID()
+let draftAppendRowID = UUID()

diff --git a/Listless/Helpers/TaskListViewProtocol.swift b/Listless/Helpers/TaskListViewProtocol.swift

@@ -11,5 +11,8 @@ protocol TaskListViewProtocol {
     var selectedTaskID: UUID? { get nonmutating set }
     var pendingFocus: FocusField? { get nonmutating set }
     var dragState: DragState { get nonmutating set }
+    var draftTaskPlacement: DraftTaskPlacement? { get nonmutating set }
+    var draftTaskTitle: String { get nonmutating set }
     func didStartDrag()
+    func clearDraftTaskUI(at placement: DraftTaskPlacement, hasTitle: Bool)
 }

diff --git a/ListlessMac/Helpers/ClickableTextField.swift b/ListlessMac/Helpers/ClickableTextField.swift

@@ -60,6 +60,10 @@ struct ClickableTextField: NSViewRepresentable {
 
             // Apply styling (sets attributedStringValue)
             context.coordinator.applyStyle(to: textField, text: text, isCompleted: isCompleted)
+        } else if text.isEmpty && !textField.stringValue.isEmpty {
+            // External reset (e.g. phantom row chaining) — clear the field
+            // even though the field editor is active.
+            textField.stringValue = ""
         }
 
         // Disable if completed

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

@@ -11,6 +11,8 @@ struct TaskListView: View, TaskListViewProtocol {
     struct InteractionStateData {
         var dragState: DragState = .idle
         var liftedTaskID: UUID?
+        var draftTaskPlacement: DraftTaskPlacement?
+        var draftTaskTitle: String = ""
     }
 
     struct TaskStateData {
@@ -61,6 +63,16 @@ struct TaskListView: View, TaskListViewProtocol {
         nonmutating set { iState.liftedTaskID = newValue }
     }
 
+    var draftTaskPlacement: DraftTaskPlacement? {
+        get { iState.draftTaskPlacement }
+        nonmutating set { iState.draftTaskPlacement = newValue }
+    }
+
+    var draftTaskTitle: String {
+        get { iState.draftTaskTitle }
+        nonmutating set { iState.draftTaskTitle = newValue }
+    }
+
     var refreshID: UUID {
         get { tState.refreshID }
         nonmutating set { tState.refreshID = newValue }
@@ -168,6 +180,17 @@ struct TaskListView: View, TaskListViewProtocol {
         liftedTaskID == taskID || draggedTaskID == taskID
     }
 
+    func clearDraftTaskUI(at placement: DraftTaskPlacement, hasTitle _: Bool) {
+        if draftTaskPlacement == placement {
+            draftTaskPlacement = nil
+        }
+        draftTaskTitle = ""
+        if selectedTaskID == draftTaskID(for: placement) {
+            selectedTaskID = nil
+        }
+        focusedField = nil
+    }
+
     func didStartDrag() {}
 
     var body: some View {
@@ -250,6 +273,69 @@ struct TaskListView: View, TaskListViewProtocol {
                     }
                 }
 
+                if draftTaskPlacement == .append {
+                    let total = max(1, displayActiveTasks.count + 1)
+                    let index = displayActiveTasks.count
+                    let accentColor = cachedTaskColor(
+                        forIndex: index, total: total
+                    )
+                    let isSelected = selectedTaskID == draftAppendRowID
+                    HStack(alignment: .firstTextBaseline, spacing: 12) {
+                        Image(systemName: "circle")
+                            .foregroundStyle(.primary)
+                            .font(.system(size: 17))
+                            .fontWeight(.thin)
+                            .alignmentGuide(.firstTextBaseline) { d in
+                                d[VerticalAlignment.center] + 5
+                            }
+
+                        ClickableTextField(
+                            text: Binding(
+                                get: { iState.draftTaskTitle },
+                                set: { iState.draftTaskTitle = $0 }
+                            ),
+                            isCompleted: false,
+                            onEditingChanged: { editing, shouldCreateNewTask in
+                                if editing {
+                                    beginDraftTaskEditing(.append)
+                                } else {
+                                    commitDraftTask(
+                                        shouldCreateNewTask: shouldCreateNewTask
+                                    )
+                                }
+                            }
+                        )
+                        .focused(
+                            $focusedFieldBinding,
+                            equals: .task(draftAppendRowID)
+                        )
+                        .frame(maxWidth: .infinity, alignment: .leading)
+                    }
+                    .padding(.top, 4)
+                    .padding(.vertical, 8)
+                    .padding(.horizontal, 16)
+                    .frame(maxWidth: .infinity, alignment: .leading)
+                    .contentShape(Rectangle())
+                    .background(
+                        isSelected
+                            ? RoundedRectangle(cornerRadius: 6, style: .continuous)
+                                .fill(Color(nsColor: .controlBackgroundColor))
+                            : nil
+                    )
+                    .overlay(alignment: .leading) {
+                        Rectangle()
+                            .fill(accentColor)
+                            .frame(width: 4)
+                            .padding(.vertical, 1)
+                    }
+                    .overlay {
+                        if isSelected {
+                            RoundedRectangle(cornerRadius: 6, style: .continuous)
+                                .stroke(accentColor.opacity(0.40), lineWidth: 2)
+                        }
+                    }
+                }
+
                 ForEach(completedTasks) { task in
                     let taskID = task.id
                     TaskRowView(

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

@@ -2,57 +2,13 @@ import SwiftUI
 
 extension TaskListView {
 
-    // MARK: - Phantom Entry Row Helpers
+    // MARK: - Pull-to-Create Draft Helpers
 
-    /// Show the phantom row and focus its text field.
     func revealPhantomRow() -> UUID {
-        phantomTitle = ""
-        phantomRowVisible = true
-        pendingFocus = .task(Self.phantomRowID)
-        focusedField = .task(Self.phantomRowID)
-        selectedTaskID = Self.phantomRowID
-        return Self.phantomRowID
+        return createNewTaskAtTop()
     }
 
-    /// Commit the phantom row: create a real task if the user typed something,
-    /// then hide the phantom and reset its state.
     func commitPhantomRow() {
-        let title = phantomTitle.trimmingCharacters(in: .whitespacesAndNewlines)
-
-        let collapse: () -> Void = {
-            phantomRowVisible = false
-            phantomTitle = ""
-            selectedTaskID = nil
-            var state = pullToCreate
-            state.isInsertionPending = false
-            state.indicatorOffset = 0
-            pullToCreate = state
-        }
-
-        if title.isEmpty {
-            withAnimation(.spring(response: 0.24, dampingFraction: 0.95)) {
-                collapse()
-            }
-        } else {
-            // Keep the successful create swap immediate so focus can move
-            // straight into the real task row without a visible intermediate state.
-            var t = Transaction(animation: nil)
-            t.disablesAnimations = true
-            withTransaction(t) {
-                collapse()
-            }
-        }
-
-        focusedField = nil
-
-        guard !title.isEmpty else { return }
-
-        do {
-            let task = try store.createTask(title: title, atBeginning: true)
-            try store.save()
-            selectedTaskID = task.id
-        } catch {
-            presentStoreError(error)
-        }
+        commitDraftTask()
     }
 }

diff --git a/ListlessiOS/Helpers/TappableTextField.swift b/ListlessiOS/Helpers/TappableTextField.swift

@@ -42,6 +42,13 @@ struct TappableTextField: UIViewRepresentable {
     func updateUIView(_ textView: UITextView, context: Context) {
         if !textView.isFirstResponder {
             applyStyle(to: textView, text: text, isCompleted: isCompleted)
+        } else if text.isEmpty && !textView.text.isEmpty {
+            // External reset (e.g. phantom row chaining) — clear the view
+            // even though it's first responder.
+            textView.text = ""
+            if let placeholder = textView.viewWithTag(100) as? UILabel {
+                placeholder.isHidden = false
+            }
         }
         if textView.returnKeyType != returnKeyType {
             textView.returnKeyType = returnKeyType

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

@@ -18,12 +18,10 @@ struct TaskListView: View, TaskListViewProtocol {
         var clearingTaskIDs: Set<UUID> = []
         var rowFrames: [UUID: CGRect] = [:]
         var undoToast: UndoToastData? = nil
-        var phantomRowVisible: Bool = false
-        var phantomTitle: String = ""
+        var draftTaskPlacement: DraftTaskPlacement?
+        var draftTaskTitle: String = ""
     }
 
-    static let phantomRowID = UUID()
-
     struct TaskStateData {
         var refreshID = UUID()
     }
@@ -97,20 +95,28 @@ struct TaskListView: View, TaskListViewProtocol {
         nonmutating set { iState.undoToast = newValue }
     }
 
-    var phantomRowVisible: Bool {
-        get { iState.phantomRowVisible }
-        nonmutating set { iState.phantomRowVisible = newValue }
+    var draftTaskPlacement: DraftTaskPlacement? {
+        get { iState.draftTaskPlacement }
+        nonmutating set { iState.draftTaskPlacement = newValue }
+    }
+
+    var draftTaskTitle: String {
+        get { iState.draftTaskTitle }
+        nonmutating set { iState.draftTaskTitle = newValue }
+    }
+
+    private var isPrependDraftVisible: Bool {
+        draftTaskPlacement == .prepend
     }
 
-    var phantomTitle: String {
-        get { iState.phantomTitle }
-        nonmutating set { iState.phantomTitle = newValue }
+    private var isAppendDraftVisible: Bool {
+        draftTaskPlacement == .append
     }
 
-    var phantomTitleBinding: Binding<String> {
+    var draftTaskTitleBinding: Binding<String> {
         Binding(
-            get: { iState.phantomTitle },
-            set: { iState.phantomTitle = $0 }
+            get: { iState.draftTaskTitle },
+            set: { iState.draftTaskTitle = $0 }
         )
     }
 
@@ -203,6 +209,43 @@ struct TaskListView: View, TaskListViewProtocol {
         self.syncMonitor = syncMonitor
     }
 
+    func clearDraftTaskUI(at placement: DraftTaskPlacement, hasTitle: Bool) {
+        let clear: () -> Void = {
+            if draftTaskPlacement == placement {
+                draftTaskPlacement = nil
+            }
+            draftTaskTitle = ""
+            if selectedTaskID == draftTaskID(for: placement) {
+                selectedTaskID = nil
+            }
+
+            guard placement == .prepend else { return }
+
+            var state = pullToCreate
+            state.isInsertionPending = false
+            state.indicatorOffset = 0
+            pullToCreate = state
+        }
+
+        if placement == .prepend, !hasTitle {
+            withAnimation(.spring(response: 0.24, dampingFraction: 0.95)) {
+                clear()
+            }
+        } else if placement == .prepend {
+            var transaction = Transaction(animation: nil)
+            transaction.disablesAnimations = true
+            withTransaction(transaction) {
+                clear()
+            }
+        } else {
+            clear()
+        }
+
+        if placement == .prepend || !hasTitle {
+            focusedField = nil
+        }
+    }
+
     func didStartDrag() {
         isDragging = true
         let generator = UIImpactFeedbackGenerator(style: .medium)
@@ -231,7 +274,7 @@ struct TaskListView: View, TaskListViewProtocol {
     }
 
     private var pullToCreateGap: CGFloat {
-        guard pullToCreate.shouldShowIndicator, !iState.phantomRowVisible else { return 0 }
+        guard pullToCreate.shouldShowIndicator, !isPrependDraftVisible else { return 0 }
         let exposedPull = pullToCreate.indicatorDisplayOffset(threshold: pullCreateThreshold)
         return min(
             vStackSpacing,
@@ -240,7 +283,7 @@ struct TaskListView: View, TaskListViewProtocol {
     }
 
     private var pullToCreateRowOverlap: CGFloat {
-        guard pullToCreate.shouldShowIndicator, !iState.phantomRowVisible else {
+        guard pullToCreate.shouldShowIndicator, !isPrependDraftVisible else {
             return 0
         }
         return PullToCreateIndicator.indicatorHeight - pullToCreateRevealHeight
@@ -251,7 +294,7 @@ struct TaskListView: View, TaskListViewProtocol {
     /// (during the pull), so it's ready when the user releases.
     @ViewBuilder var pullToCreateIndicatorRow: some View {
         let showIndicator = pullToCreate.shouldShowIndicator
-        let showPhantom = iState.phantomRowVisible
+        let showPhantom = isPrependDraftVisible
         if showIndicator || showPhantom {
             ZStack(alignment: .topLeading) {
                 PullToCreateIndicator(
@@ -283,7 +326,7 @@ struct TaskListView: View, TaskListViewProtocol {
         let accentColor = taskColor(
             forIndex: 0, total: max(1, displayActiveTasks.count + 1)
         )
-        let isSelected = selectedTaskID == Self.phantomRowID
+        let isSelected = selectedTaskID == draftPrependRowID
         HStack(alignment: .center, spacing: TaskRowMetrics.contentSpacing) {
             Image(systemName: "circle")
                 .frame(width: 22, height: 22)
@@ -291,19 +334,21 @@ struct TaskListView: View, TaskListViewProtocol {
                 .font(.system(size: 17))
 
             TappableTextField(
-                text: phantomTitleBinding,
+                text: draftTaskTitleBinding,
                 isCompleted: false,
                 isDragging: false,
                 onEditingChanged: { editing, _ in
                     DispatchQueue.main.async {
-                        if !editing {
-                            commitPhantomRow()
+                        if editing {
+                            beginDraftTaskEditing(.prepend)
+                        } else {
+                            commitDraftTask()
                         }
                     }
                 },
                 returnKeyType: .done
             )
-            .focused($focusedFieldBinding, equals: .task(Self.phantomRowID))
+            .focused($focusedFieldBinding, equals: .task(draftPrependRowID))
             .frame(maxWidth: .infinity, alignment: .leading)
         }
         .padding(.vertical, TaskRowMetrics.contentVerticalPadding)
@@ -338,6 +383,74 @@ struct TaskListView: View, TaskListViewProtocol {
         )
     }
 
+    @ViewBuilder private var phantomAppendRowContent: some View {
+        if isAppendDraftVisible {
+            let total = max(1, displayActiveTasks.count + 1)
+            let index = displayActiveTasks.count
+            let accentColor = taskColor(forIndex: index, total: total)
+            let isSelected = selectedTaskID == draftAppendRowID
+            HStack(alignment: .center, spacing: TaskRowMetrics.contentSpacing) {
+                Image(systemName: "circle")
+                    .frame(width: 22, height: 22)
+                    .foregroundStyle(Color.secondary)
+                    .font(.system(size: 17))
+
+                TappableTextField(
+                    text: draftTaskTitleBinding,
+                    isCompleted: false,
+                    isDragging: false,
+                    onEditingChanged: { editing, shouldCreateNewTask in
+                        DispatchQueue.main.async {
+                            if editing {
+                                beginDraftTaskEditing(.append)
+                            } else {
+                                commitDraftTask(
+                                    shouldCreateNewTask: shouldCreateNewTask
+                                )
+                            }
+                        }
+                    },
+                    returnKeyType: draftTaskTitle.trimmingCharacters(
+                        in: .whitespacesAndNewlines
+                    ).isEmpty ? .done : .next
+                )
+                .focused($focusedFieldBinding, equals: .task(draftAppendRowID))
+                .frame(maxWidth: .infinity, alignment: .leading)
+            }
+            .padding(.vertical, TaskRowMetrics.contentVerticalPadding)
+            .padding(.trailing, TaskRowMetrics.contentHorizontalPadding)
+            .padding(.leading, TaskRowMetrics.activeLeadingPadding)
+            .frame(maxWidth: .infinity, alignment: .leading)
+            .contentShape(Rectangle())
+            .background {
+                Color.taskCard.overlay(accentColor.opacity(0.15))
+            }
+            .clipShape(
+                UnevenRoundedRectangle(
+                    topLeadingRadius: 0, bottomLeadingRadius: 0,
+                    bottomTrailingRadius: TaskRowMetrics.trailingCornerRadius,
+                    topTrailingRadius: TaskRowMetrics.trailingCornerRadius
+                )
+            )
+            .overlay(alignment: .leading) {
+                Rectangle()
+                    .fill(accentColor)
+                    .frame(width: TaskRowMetrics.accentBarWidth)
+            }
+            .overlay(
+                isSelected
+                    ? UnevenRoundedRectangle(
+                        topLeadingRadius: 0, bottomLeadingRadius: 0,
+                        bottomTrailingRadius: TaskRowMetrics.trailingCornerRadius,
+                        topTrailingRadius: TaskRowMetrics.trailingCornerRadius
+                    )
+                    .stroke(accentColor.opacity(0.40), lineWidth: 2)
+                    : nil
+            )
+            .id(draftAppendRowID)
+        }
+    }
+
     @ViewBuilder private var taskRows: some View {
         ForEach(Array(displayActiveTasks.enumerated()), id: \.element.id) { index, task in
             let taskID = task.id
@@ -356,7 +469,9 @@ struct TaskListView: View, TaskListViewProtocol {
                 onSelect: { selectTask($0) },
                 onStartEdit: { startEditing($0) },
                 onEndEdit: {
-                    selectedTaskID = nil
+                    if selectedTaskID == $0 {
+                        selectedTaskID = nil
+                    }
                     endEditing($0, shouldCreateNewTask: $1)
                 }
             )
@@ -381,6 +496,8 @@ struct TaskListView: View, TaskListViewProtocol {
             .id(taskID)
         }
 
+        phantomAppendRowContent
+
         ForEach(completedTasks) { task in
             let taskID = task.id
             let isBeingCleared = iState.clearingTaskIDs.contains(taskID)
@@ -479,7 +596,7 @@ struct TaskListView: View, TaskListViewProtocol {
                 }
                 .padding(
                     .bottom,
-                    (pullToCreate.shouldShowIndicator && !iState.phantomRowVisible)
+                    (pullToCreate.shouldShowIndicator && !isPrependDraftVisible)
                         ? (pullToCreateGap - vStackSpacing) : 0
                 )
                 taskRows
@@ -509,7 +626,7 @@ struct TaskListView: View, TaskListViewProtocol {
 
                 if case .task(let id) = (newValue ?? fState.focusedField),
                     draggedTaskID == nil,
-                    id != Self.phantomRowID
+                    id != draftPrependRowID
                 {
                     withAnimation {
                         scrollProxy.scrollTo(id)
@@ -517,7 +634,8 @@ struct TaskListView: View, TaskListViewProtocol {
                 }
             }
             .onChange(of: fState.selectedTaskID) { _, newID in
-                if let newID, draggedTaskID == nil, newID != Self.phantomRowID {
+                if let newID, draggedTaskID == nil {
+                    guard newID != draftPrependRowID else { return }
                     withAnimation {
                         scrollProxy.scrollTo(newID)
                     }