← All repositories

listless

A simple list app for Apple platforms

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


commit f7fd505561ba27161bed9b87b0021ec7ae1199c7
parent b6947ab9c1df71ddd081901fa2f227f32cbd615a
Author: Michael Camilleri <[email protected]>
Date:   Fri, 13 Mar 2026 08:20:22 +0900

Remove unnecessary shims

Diffstat

MListless/Extensions/TaskListView+Logic.swift | 70+++++++++++++++++++++++++++++++++++-----------------------------------
MListless/Helpers/TaskListTypes.swift | 6++++++
MListless/Helpers/TaskListViewProtocol.swift | 3+--
MListlessMac/Extensions/TaskListView+Toolbar.swift | 4++--
MListlessMac/Views/TaskListView.swift | 75++++++++++++++++++++++-----------------------------------------------------
MListlessiOS/Extensions/TaskListView+Drag.swift | 8++++----
MListlessiOS/Extensions/TaskListView+NavigationHeader.swift | 2+-
MListlessiOS/Extensions/TaskListView+PullToClear.swift | 4++--
MListlessiOS/Extensions/TaskListView+Undo.swift | 6+++---
MListlessiOS/Views/TaskListView.swift | 111++++++++++++++++++++++---------------------------------------------------------

10 files changed, 106 insertions(+), 183 deletions(-)

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

@@ -98,17 +98,17 @@ extension TaskListViewProtocol {
         let taskID = draftTaskID(for: placement)
         draftTaskTitle = ""
         draftTaskPlacement = placement
-        pendingFocus = .task(taskID)
+        fState.pendingFocus = .task(taskID)
         focusedField = .task(taskID)
-        selectedTaskID = taskID
+        fState.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
+        fState.selectedTaskID = taskID
+        if case .task(let id) = fState.pendingFocus, id == taskID {
+            fState.pendingFocus = nil
         }
     }
 
@@ -117,17 +117,17 @@ extension TaskListViewProtocol {
         let taskID = draftTaskID(for: placement)
         let title = draftTaskTitle.trimmingCharacters(in: .whitespacesAndNewlines)
 
-        // Clear pendingFocus before clearDraftTaskUI so that the iOS
+        // Clear fState.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
+        // the draft row via a stale fState.pendingFocus value.
+        if case .task(let id) = fState.pendingFocus, id == taskID {
+            fState.pendingFocus = nil
         }
 
         clearDraftTaskUI(at: placement, hasTitle: !title.isEmpty)
 
-        if selectedTaskID == taskID {
-            selectedTaskID = nil
+        if fState.selectedTaskID == taskID {
+            fState.selectedTaskID = nil
         }
 
         guard !title.isEmpty else { return }
@@ -141,7 +141,7 @@ extension TaskListViewProtocol {
             }
             try store.save()
             if placement == .append {
-                selectedTaskID = task.id
+                fState.selectedTaskID = task.id
             }
         } catch {
             presentStoreError(error)
@@ -158,7 +158,7 @@ extension TaskListViewProtocol {
             let sortOrder = try sortOrderAfter(taskID: afterTaskID)
             let newTask = try store.createTask(title: title, sortOrder: sortOrder)
             try store.save()
-            selectedTaskID = newTask.id
+            fState.selectedTaskID = newTask.id
             focusedField = .scrollView
         } catch {
             presentStoreError(error)
@@ -191,12 +191,12 @@ extension TaskListViewProtocol {
     func handleBackgroundTap() {
         let isTaskFocused = if case .task = focusedField { true } else { false }
 
-        if isTaskFocused || selectedTaskID != nil {
-            pendingFocus = nil
+        if isTaskFocused || fState.selectedTaskID != nil {
+            fState.pendingFocus = nil
             if draftTaskPlacement != nil {
                 commitDraftTask()
             }
-            selectedTaskID = nil
+            fState.selectedTaskID = nil
             focusedField = nil
         } else {
             revealDraftTask(at: .append)
@@ -224,7 +224,7 @@ extension TaskListViewProtocol {
     }
 
     private func deleteIfEmpty(taskID: UUID) {
-        if case .task(let pendingTaskID) = pendingFocus, pendingTaskID == taskID {
+        if case .task(let pendingTaskID) = fState.pendingFocus, pendingTaskID == taskID {
             return
         }
 
@@ -272,15 +272,15 @@ extension TaskListViewProtocol {
     }
 
     func selectTask(_ taskID: UUID) {
-        selectedTaskID = taskID
+        fState.selectedTaskID = taskID
     }
 
     func deleteTask(_ task: TaskItem) {
         let taskID = task.id
         do {
             try store.delete(taskID: taskID)
-            if selectedTaskID == taskID {
-                selectedTaskID = nil
+            if fState.selectedTaskID == taskID {
+                fState.selectedTaskID = nil
             }
         } catch {
             presentStoreError(error)
@@ -304,8 +304,8 @@ extension TaskListViewProtocol {
             return .ignored
         }
 
-        guard let currentID = selectedTaskID else {
-            selectedTaskID = activeTasks.last?.id
+        guard let currentID = fState.selectedTaskID else {
+            fState.selectedTaskID = activeTasks.last?.id
             return .handled
         }
 
@@ -315,7 +315,7 @@ extension TaskListViewProtocol {
         }
 
         if currentIndex > 0 {
-            selectedTaskID = displayOrder[currentIndex - 1].id
+            fState.selectedTaskID = displayOrder[currentIndex - 1].id
         }
         return .handled
     }
@@ -325,8 +325,8 @@ extension TaskListViewProtocol {
             return .ignored
         }
 
-        guard let currentID = selectedTaskID else {
-            selectedTaskID = activeTasks.first?.id ?? completedTasks.first?.id
+        guard let currentID = fState.selectedTaskID else {
+            fState.selectedTaskID = activeTasks.first?.id ?? completedTasks.first?.id
             return .handled
         }
 
@@ -336,14 +336,14 @@ extension TaskListViewProtocol {
         }
 
         if currentIndex < displayOrder.count - 1 {
-            selectedTaskID = displayOrder[currentIndex + 1].id
+            fState.selectedTaskID = displayOrder[currentIndex + 1].id
         }
         return .handled
     }
 
     func toggleSelectedTask() -> KeyPress.Result {
         guard focusedField == .scrollView else { return .ignored }
-        guard let currentID = selectedTaskID else { return .handled }
+        guard let currentID = fState.selectedTaskID else { return .handled }
         guard let task = allTasksInDisplayOrder.first(where: { $0.id == currentID }) else {
             return .handled
         }
@@ -353,7 +353,7 @@ extension TaskListViewProtocol {
 
     func focusSelectedTask() -> KeyPress.Result {
         guard focusedField == .scrollView else { return .ignored }
-        guard let currentID = selectedTaskID else { return .handled }
+        guard let currentID = fState.selectedTaskID else { return .handled }
         guard let task = allTasksInDisplayOrder.first(where: { $0.id == currentID }) else {
             return .handled
         }
@@ -366,7 +366,7 @@ extension TaskListViewProtocol {
         guard focusedField == .scrollView else {
             return .ignored
         }
-        guard let currentID = selectedTaskID else {
+        guard let currentID = fState.selectedTaskID else {
             return .handled
         }
         guard let task = allTasksInDisplayOrder.first(where: { $0.id == currentID }) else {
@@ -378,7 +378,7 @@ extension TaskListViewProtocol {
 
     func moveSelectedTaskUp() {
         guard focusedField == .scrollView else { return }
-        guard let currentID = selectedTaskID else { return }
+        guard let currentID = fState.selectedTaskID else { return }
         guard let currentIndex = activeTasks.firstIndex(where: { $0.id == currentID }) else { return }
         guard currentIndex > 0 else { return }
 
@@ -391,7 +391,7 @@ extension TaskListViewProtocol {
 
     func moveSelectedTaskDown() {
         guard focusedField == .scrollView else { return }
-        guard let currentID = selectedTaskID else { return }
+        guard let currentID = fState.selectedTaskID else { return }
         guard let currentIndex = activeTasks.firstIndex(where: { $0.id == currentID }) else { return }
         guard currentIndex < activeTasks.count - 1 else { return }
 
@@ -404,7 +404,7 @@ extension TaskListViewProtocol {
 
     func markSelectedTaskCompleted() {
         guard focusedField == .scrollView else { return }
-        guard let currentID = selectedTaskID else { return }
+        guard let currentID = fState.selectedTaskID else { return }
         guard let task = allTasksInDisplayOrder.first(where: { $0.id == currentID }) else { return }
         toggleCompletion(task)
     }
@@ -416,9 +416,9 @@ extension TaskListViewProtocol {
     }
 
     func startEditing(_ taskID: UUID) {
-        selectedTaskID = taskID
+        fState.selectedTaskID = taskID
         focusedField = .task(taskID)
-        pendingFocus = nil
+        fState.pendingFocus = nil
     }
 
     func endEditing(_ taskID: UUID, shouldCreateNewTask: Bool) {
@@ -437,7 +437,7 @@ extension TaskListViewProtocol {
         let willBeDeleted = shouldDeleteIfEmpty(taskID: taskID)
 
         if willBeDeleted {
-            selectedTaskID = nil
+            fState.selectedTaskID = nil
             deleteIfEmpty(taskID: taskID)
         } else if wasLastActiveTask && shouldCreateNewTask {
             revealDraftTask(at: .append)

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

@@ -15,5 +15,11 @@ enum DraftTaskPlacement: Equatable {
     case append
 }
 
+struct FocusStateData {
+    var focusedField: FocusField?
+    var selectedTaskID: UUID?
+    var pendingFocus: FocusField?
+}
+
 let draftPrependRowID = UUID()
 let draftAppendRowID = UUID()

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

@@ -8,8 +8,7 @@ protocol TaskListViewProtocol {
     var syncMonitor: CloudKitSyncMonitor { get }
     var managedObjectContext: NSManagedObjectContext { get }
     var focusedField: FocusField? { get nonmutating set }
-    var selectedTaskID: UUID? { get nonmutating set }
-    var pendingFocus: FocusField? { get nonmutating set }
+    var fState: FocusStateData { get nonmutating set }
     var dragState: DragState { get nonmutating set }
     var draftTaskPlacement: DraftTaskPlacement? { get nonmutating set }
     var draftTaskTitle: String { get nonmutating set }

diff --git a/ListlessMac/Extensions/TaskListView+Toolbar.swift b/ListlessMac/Extensions/TaskListView+Toolbar.swift

@@ -33,7 +33,7 @@ extension TaskListView {
                 .help("Create a new item")
 
                 Button {
-                    if let currentID = selectedTaskID,
+                    if let currentID = fState.selectedTaskID,
                         let task = allTasksInDisplayOrder.first(where: { $0.id == currentID })
                     {
                         deleteTask(task)
@@ -41,7 +41,7 @@ extension TaskListView {
                 } label: {
                     Label("Delete", systemImage: "trash")
                 }
-                .disabled(selectedTaskID == nil || focusedField != .scrollView)
+                .disabled(fState.selectedTaskID == nil || focusedField != .scrollView)
                 .help("Delete selected item")
 
                 Divider()

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

@@ -2,12 +2,6 @@ import SwiftUI
 import UniformTypeIdentifiers
 
 struct TaskListView: View, TaskListViewProtocol {
-    struct FocusStateData {
-        var focusedField: FocusField?
-        var selectedTaskID: UUID?
-        var pendingFocus: FocusField?
-    }
-
     struct InteractionStateData {
         var dragState: DragState = .idle
         var liftedTaskID: UUID?
@@ -15,10 +9,6 @@ struct TaskListView: View, TaskListViewProtocol {
         var draftTaskTitle: String = ""
     }
 
-    struct TaskStateData {
-        var refreshID = UUID()
-    }
-
     @Environment(\.undoManager) var undoManager
     @Environment(\.managedObjectContext) var managedObjectContext
 
@@ -31,9 +21,8 @@ struct TaskListView: View, TaskListViewProtocol {
     )
     var tasks: FetchedResults<TaskItem>
     @FocusState private var focusedFieldBinding: FocusField?
-    @State private var fState = FocusStateData()
+    @State var fState = FocusStateData()
     @State private var iState = InteractionStateData()
-    @State private var tState = TaskStateData()
 
     var focusedField: FocusField? {
         get { fState.focusedField }
@@ -43,26 +32,11 @@ struct TaskListView: View, TaskListViewProtocol {
         }
     }
 
-    var selectedTaskID: UUID? {
-        get { fState.selectedTaskID }
-        nonmutating set { fState.selectedTaskID = newValue }
-    }
-
-    var pendingFocus: FocusField? {
-        get { fState.pendingFocus }
-        nonmutating set { fState.pendingFocus = newValue }
-    }
-
     var dragState: DragState {
         get { iState.dragState }
         nonmutating set { iState.dragState = newValue }
     }
 
-    var liftedTaskID: UUID? {
-        get { iState.liftedTaskID }
-        nonmutating set { iState.liftedTaskID = newValue }
-    }
-
     var draftTaskPlacement: DraftTaskPlacement? {
         get { iState.draftTaskPlacement }
         nonmutating set { iState.draftTaskPlacement = newValue }
@@ -73,30 +47,25 @@ struct TaskListView: View, TaskListViewProtocol {
         nonmutating set { iState.draftTaskTitle = newValue }
     }
 
-    var refreshID: UUID {
-        get { tState.refreshID }
-        nonmutating set { tState.refreshID = newValue }
-    }
-
     var vStackSpacing: CGFloat { 0 }
     var isCompletelyEmpty: Bool { activeTasks.isEmpty && completedTasks.isEmpty }
     var selectedIndex: Int? {
-        guard let currentID = selectedTaskID else { return nil }
+        guard let currentID = fState.selectedTaskID else { return nil }
         return activeTasks.firstIndex(where: { $0.id == currentID })
     }
 
     var canDeleteSelectionFromList: Bool {
-        selectedTaskID != nil && focusedField == .scrollView
+        fState.selectedTaskID != nil && focusedField == .scrollView
     }
 
     var canMarkSelectionCompleted: Bool {
         guard focusedField == .scrollView else { return false }
-        guard let currentID = selectedTaskID else { return false }
+        guard let currentID = fState.selectedTaskID else { return false }
         return allTasksInDisplayOrder.contains(where: { $0.id == currentID })
     }
 
     var markCompletedMenuTitle: String {
-        completedTasks.contains(where: { $0.id == selectedTaskID })
+        completedTasks.contains(where: { $0.id == fState.selectedTaskID })
             ? "Mark as Incomplete" : "Mark as Complete"
     }
 
@@ -122,7 +91,7 @@ struct TaskListView: View, TaskListViewProtocol {
 
     var menuCoordinatorTrigger: MenuState {
         MenuState(
-            selectedTaskID: selectedTaskID,
+            selectedTaskID: fState.selectedTaskID,
             isScrollViewFocused: focusedField == .scrollView,
             activeTaskCount: activeTasks.count,
             completedTaskCount: completedTasks.count,
@@ -134,14 +103,14 @@ struct TaskListView: View, TaskListViewProtocol {
         let coord = menuCoordinator
         coord.newTask = { createNewTask(); focusedField = nil }
         coord.copySelectedTask = {
-            guard let taskID = selectedTaskID,
+            guard let taskID = fState.selectedTaskID,
                   let task = tasks.first(where: { $0.id == taskID }) else { return }
             let pasteboard = NSPasteboard.general
             pasteboard.clearContents()
             pasteboard.setString(task.title, forType: .string)
         }
         coord.cutSelectedTask = {
-            guard let taskID = selectedTaskID,
+            guard let taskID = fState.selectedTaskID,
                   let task = tasks.first(where: { $0.id == taskID }) else { return }
             let pasteboard = NSPasteboard.general
             pasteboard.clearContents()
@@ -149,7 +118,7 @@ struct TaskListView: View, TaskListViewProtocol {
             deleteTask(task)
         }
         coord.pasteAfterSelectedTask = {
-            guard let taskID = selectedTaskID,
+            guard let taskID = fState.selectedTaskID,
                   let string = NSPasteboard.general.string(forType: .string) else { return }
             createTask(title: string, afterTaskID: taskID)
         }
@@ -159,8 +128,8 @@ struct TaskListView: View, TaskListViewProtocol {
         coord.markSelectedTaskCompleted = { markSelectedTaskCompleted() }
         coord.clearCompletedTasks = { clearCompletedTasks() }
         let inNavMode = focusedField == .scrollView
-        coord.canCopySelectedTask = selectedTaskID != nil && inNavMode
-        coord.canCutSelectedTask = selectedTaskID != nil && inNavMode
+        coord.canCopySelectedTask = fState.selectedTaskID != nil && inNavMode
+        coord.canCutSelectedTask = fState.selectedTaskID != nil && inNavMode
         coord.canPasteAfterSelectedTask = selectedIndex != nil && inNavMode
         coord.canDeleteSelectedTask = canDeleteSelectionFromList
         coord.canMoveSelectedTaskUp = canMoveSelectionUp
@@ -177,7 +146,7 @@ struct TaskListView: View, TaskListViewProtocol {
     }
 
     func isRowLifted(_ taskID: UUID) -> Bool {
-        liftedTaskID == taskID || draggedTaskID == taskID
+        iState.liftedTaskID == taskID || draggedTaskID == taskID
     }
 
     func clearDraftTaskUI(at placement: DraftTaskPlacement, hasTitle _: Bool) {
@@ -185,8 +154,8 @@ struct TaskListView: View, TaskListViewProtocol {
             draftTaskPlacement = nil
         }
         draftTaskTitle = ""
-        if selectedTaskID == draftTaskID(for: placement) {
-            selectedTaskID = nil
+        if fState.selectedTaskID == draftTaskID(for: placement) {
+            fState.selectedTaskID = nil
         }
         focusedField = nil
     }
@@ -203,7 +172,7 @@ struct TaskListView: View, TaskListViewProtocol {
                         taskID: taskID,
                         index: index,
                         totalTasks: displayActiveTasks.count,
-                        isSelected: selectedTaskID == taskID,
+                        isSelected: fState.selectedTaskID == taskID,
                         focusedField: $focusedFieldBinding,
                         onToggle: { toggleCompletion($0) },
                         onTitleChange: { updateTitle($0, $1) },
@@ -217,11 +186,11 @@ struct TaskListView: View, TaskListViewProtocol {
                         isActive: !task.isCompleted,
                         taskID: task.id,
                         onDragStart: {
-                            liftedTaskID = nil
+                            iState.liftedTaskID = nil
                             startDrag(taskID: task.id)
                         },
-                        onLift: { liftedTaskID = task.id },
-                        onLiftEnd: { if liftedTaskID == task.id { liftedTaskID = nil } }
+                        onLift: { iState.liftedTaskID = task.id },
+                        onLiftEnd: { if iState.liftedTaskID == task.id { iState.liftedTaskID = nil } }
                     )
                     .scaleEffect(isRowLifted(taskID) ? 1.03 : 1.0)
                     .shadow(
@@ -279,7 +248,7 @@ struct TaskListView: View, TaskListViewProtocol {
                     let accentColor = cachedTaskColor(
                         forIndex: index, total: total
                     )
-                    let isSelected = selectedTaskID == draftAppendRowID
+                    let isSelected = fState.selectedTaskID == draftAppendRowID
                     HStack(alignment: .firstTextBaseline, spacing: 12) {
                         Image(systemName: "circle")
                             .foregroundStyle(.primary)
@@ -341,7 +310,7 @@ struct TaskListView: View, TaskListViewProtocol {
                     TaskRowView(
                         task: task,
                         taskID: taskID,
-                        isSelected: selectedTaskID == taskID,
+                        isSelected: fState.selectedTaskID == taskID,
                         focusedField: $focusedFieldBinding,
                         onToggle: { toggleCompletion($0) },
                         onTitleChange: { updateTitle($0, $1) },
@@ -399,10 +368,10 @@ struct TaskListView: View, TaskListViewProtocol {
             handleFocusChange(from: oldValue, to: newValue)
 
             if newValue == nil {
-                if let pending = pendingFocus {
+                if let pending = fState.pendingFocus {
                     focusedFieldBinding = pending
                     fState.focusedField = pending
-                    pendingFocus = nil
+                    fState.pendingFocus = nil
                 } else {
                     focusedFieldBinding = .scrollView
                     fState.focusedField = .scrollView

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

@@ -5,7 +5,7 @@ extension TaskListView {
         guard let draggedID = draggedTaskID,
               var order = visualOrder,
               let currentIndex = order.firstIndex(of: draggedID),
-              let draggedFrame = rowFrames[draggedID] else { return }
+              let draggedFrame = iState.rowFrames[draggedID] else { return }
 
         let threshold = draggedFrame.height * 0.2
 
@@ -32,17 +32,17 @@ extension TaskListView {
               let order = visualOrder,
               let finalIndex = order.firstIndex(of: draggedID) else {
             clearDragState()
-            isDragging = false
+            iState.isDragging = false
             return
         }
         do {
             try store.moveTask(taskID: draggedID, toIndex: finalIndex)
             clearDragState()
-            isDragging = false
+            iState.isDragging = false
         } catch {
             withAnimation(.spring(response: 0.3, dampingFraction: 0.8)) {
                 clearDragState()
-                isDragging = false
+                iState.isDragging = false
             }
             presentStoreError(error)
         }

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

@@ -33,7 +33,7 @@ extension TaskListView {
         .padding(.bottom, 8)
         .contentShape(Rectangle())
         .onTapGesture {
-            selectedTaskID = nil
+            fState.selectedTaskID = nil
             focusedField = nil
         }
     }

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

@@ -2,8 +2,8 @@ import SwiftUI
 
 extension TaskListView {
     @ViewBuilder var pullToClearIndicatorRow: some View {
-        if pullUpOffset > 0 && !completedTasks.isEmpty {
-            PullToClearIndicator(pullOffset: pullUpOffset)
+        if iState.pullUpOffset > 0 && !completedTasks.isEmpty {
+            PullToClearIndicator(pullOffset: iState.pullUpOffset)
         }
     }
 }

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

@@ -11,7 +11,7 @@ extension TaskListView {
         guard focusedField == .scrollView else {
             return .ignored
         }
-        guard let currentID = selectedTaskID else {
+        guard let currentID = fState.selectedTaskID else {
             return .handled
         }
         guard let task = allTasksInDisplayOrder.first(where: { $0.id == currentID }) else {
@@ -40,7 +40,7 @@ extension TaskListView {
 
     func showUndoToast(message: String) {
         withAnimation {
-            undoToast = UndoToastData(id: UUID(), message: message)
+            iState.undoToast = UndoToastData(id: UUID(), message: message)
         }
     }
 
@@ -56,7 +56,7 @@ extension TaskListView {
 
     func dismissUndoToast() {
         withAnimation {
-            undoToast = nil
+            iState.undoToast = nil
         }
     }
 }

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

@@ -2,12 +2,6 @@ import SwiftUI
 import UIKit
 
 struct TaskListView: View, TaskListViewProtocol {
-    struct FocusStateData {
-        var focusedField: FocusField?
-        var selectedTaskID: UUID?
-        var pendingFocus: FocusField?
-    }
-
     struct InteractionStateData {
         var dragState: DragState = .idle
         var pullToCreate = PullToCreateState()
@@ -22,10 +16,6 @@ struct TaskListView: View, TaskListViewProtocol {
         var draftTaskTitle: String = ""
     }
 
-    struct TaskStateData {
-        var refreshID = UUID()
-    }
-
     @AppStorage("headingText") var headingText = "Items"
     @Environment(\.undoManager) var undoManager
     @Environment(\.managedObjectContext) var managedObjectContext
@@ -38,9 +28,8 @@ struct TaskListView: View, TaskListViewProtocol {
     )
     var tasks: FetchedResults<TaskItem>
     @FocusState private var focusedFieldBinding: FocusField?
-    @State private var fState = FocusStateData()
-    @State private var iState = InteractionStateData()
-    @State private var tState = TaskStateData()
+    @State var fState = FocusStateData()
+    @State var iState = InteractionStateData()
 
     var focusedField: FocusField? {
         get { fState.focusedField }
@@ -50,51 +39,11 @@ struct TaskListView: View, TaskListViewProtocol {
         }
     }
 
-    var selectedTaskID: UUID? {
-        get { fState.selectedTaskID }
-        nonmutating set { fState.selectedTaskID = newValue }
-    }
-
-    var pendingFocus: FocusField? {
-        get { fState.pendingFocus }
-        nonmutating set { fState.pendingFocus = newValue }
-    }
-
     var dragState: DragState {
         get { iState.dragState }
         nonmutating set { iState.dragState = newValue }
     }
 
-    var pullToCreate: PullToCreateState {
-        get { iState.pullToCreate }
-        nonmutating set { iState.pullToCreate = newValue }
-    }
-
-    var pullUpOffset: CGFloat {
-        get { iState.pullUpOffset }
-        nonmutating set { iState.pullUpOffset = newValue }
-    }
-
-    var isDragging: Bool {
-        get { iState.isDragging }
-        nonmutating set { iState.isDragging = newValue }
-    }
-
-    var rowFrames: [UUID: CGRect] {
-        get { iState.rowFrames }
-        nonmutating set { iState.rowFrames = newValue }
-    }
-
-    var refreshID: UUID {
-        get { tState.refreshID }
-        nonmutating set { tState.refreshID = newValue }
-    }
-
-    var undoToast: UndoToastData? {
-        get { iState.undoToast }
-        nonmutating set { iState.undoToast = newValue }
-    }
-
     var draftTaskPlacement: DraftTaskPlacement? {
         get { iState.draftTaskPlacement }
         nonmutating set { iState.draftTaskPlacement = newValue }
@@ -149,7 +98,7 @@ struct TaskListView: View, TaskListViewProtocol {
     }
 
     private var selectedIndex: Int? {
-        guard let currentID = selectedTaskID else { return nil }
+        guard let currentID = fState.selectedTaskID else { return nil }
         return activeTasks.firstIndex(where: { $0.id == currentID })
     }
 
@@ -175,7 +124,7 @@ struct TaskListView: View, TaskListViewProtocol {
 
     private var menuCoordinatorTrigger: MenuState {
         MenuState(
-            selectedTaskID: selectedTaskID,
+            selectedTaskID: fState.selectedTaskID,
             isScrollViewFocused: focusedField == .scrollView,
             activeTaskCount: activeTasks.count,
             completedTaskCount: completedTasks.count,
@@ -191,11 +140,11 @@ struct TaskListView: View, TaskListViewProtocol {
         coord.moveDown = { moveSelectedTaskDown() }
         coord.markCompleted = { markSelectedTaskCompleted() }
         let inNavMode = focusedField == .scrollView
-        coord.canDelete = selectedTaskID != nil && inNavMode
+        coord.canDelete = fState.selectedTaskID != nil && inNavMode
         coord.canMoveUp = canMoveSelectionUp
         coord.canMoveDown = canMoveSelectionDown
-        coord.canMarkCompleted = selectedTaskID != nil && inNavMode
-        coord.markCompletedTitle = completedTasks.contains(where: { $0.id == selectedTaskID })
+        coord.canMarkCompleted = fState.selectedTaskID != nil && inNavMode
+        coord.markCompletedTitle = completedTasks.contains(where: { $0.id == fState.selectedTaskID })
             ? "Mark as Incomplete" : "Mark as Complete"
     }
 
@@ -215,16 +164,16 @@ struct TaskListView: View, TaskListViewProtocol {
                 draftTaskPlacement = nil
             }
             draftTaskTitle = ""
-            if selectedTaskID == draftTaskID(for: placement) {
-                selectedTaskID = nil
+            if fState.selectedTaskID == draftTaskID(for: placement) {
+                fState.selectedTaskID = nil
             }
 
             guard placement == .prepend else { return }
 
-            var state = pullToCreate
+            var state = iState.pullToCreate
             state.isInsertionPending = false
             state.indicatorOffset = 0
-            pullToCreate = state
+            iState.pullToCreate = state
         }
 
         if placement == .prepend, !hasTitle {
@@ -247,7 +196,7 @@ struct TaskListView: View, TaskListViewProtocol {
     }
 
     func didStartDrag() {
-        isDragging = true
+        iState.isDragging = true
         let generator = UIImpactFeedbackGenerator(style: .medium)
         generator.impactOccurred()
     }
@@ -262,20 +211,20 @@ struct TaskListView: View, TaskListViewProtocol {
 
     private func dragScaleEffect(for taskID: UUID) -> CGFloat {
         let liftPoints: CGFloat = 20
-        guard let width = rowFrames[taskID]?.width, width > 0 else { return 1.05 }
+        guard let width = iState.rowFrames[taskID]?.width, width > 0 else { return 1.05 }
         return (width + liftPoints) / width
     }
 
     private var pullToCreateRevealHeight: CGFloat {
         min(
-            pullToCreate.indicatorDisplayOffset(threshold: pullCreateThreshold),
+            iState.pullToCreate.indicatorDisplayOffset(threshold: pullCreateThreshold),
             PullToCreateIndicator.indicatorHeight
         )
     }
 
     private var pullToCreateGap: CGFloat {
-        guard pullToCreate.shouldShowIndicator, !isPrependDraftVisible else { return 0 }
-        let exposedPull = pullToCreate.indicatorDisplayOffset(threshold: pullCreateThreshold)
+        guard iState.pullToCreate.shouldShowIndicator, !isPrependDraftVisible else { return 0 }
+        let exposedPull = iState.pullToCreate.indicatorDisplayOffset(threshold: pullCreateThreshold)
         return min(
             vStackSpacing,
             max(0, exposedPull - PullToCreateIndicator.indicatorHeight)
@@ -283,7 +232,7 @@ struct TaskListView: View, TaskListViewProtocol {
     }
 
     private var pullToCreateRowOverlap: CGFloat {
-        guard pullToCreate.shouldShowIndicator, !isPrependDraftVisible else {
+        guard iState.pullToCreate.shouldShowIndicator, !isPrependDraftVisible else {
             return 0
         }
         return PullToCreateIndicator.indicatorHeight - pullToCreateRevealHeight
@@ -293,12 +242,12 @@ struct TaskListView: View, TaskListViewProtocol {
     /// The phantom's UITextView is created while the indicator is visible
     /// (during the pull), so it's ready when the user releases.
     @ViewBuilder var pullToCreateIndicatorRow: some View {
-        let showIndicator = pullToCreate.shouldShowIndicator
+        let showIndicator = iState.pullToCreate.shouldShowIndicator
         let showPhantom = isPrependDraftVisible
         if showIndicator || showPhantom {
             ZStack(alignment: .topLeading) {
                 PullToCreateIndicator(
-                    pullOffset: pullToCreate.indicatorDisplayOffset(
+                    pullOffset: iState.pullToCreate.indicatorDisplayOffset(
                         threshold: pullCreateThreshold
                     ),
                     threshold: pullCreateThreshold,
@@ -326,7 +275,7 @@ struct TaskListView: View, TaskListViewProtocol {
         let accentColor = taskColor(
             forIndex: 0, total: max(1, displayActiveTasks.count + 1)
         )
-        let isSelected = selectedTaskID == draftPrependRowID
+        let isSelected = fState.selectedTaskID == draftPrependRowID
         HStack(alignment: .center, spacing: TaskRowMetrics.contentSpacing) {
             Image(systemName: "circle")
                 .frame(width: 22, height: 22)
@@ -388,7 +337,7 @@ struct TaskListView: View, TaskListViewProtocol {
             let total = max(1, displayActiveTasks.count + 1)
             let index = displayActiveTasks.count
             let accentColor = taskColor(forIndex: index, total: total)
-            let isSelected = selectedTaskID == draftAppendRowID
+            let isSelected = fState.selectedTaskID == draftAppendRowID
             HStack(alignment: .center, spacing: TaskRowMetrics.contentSpacing) {
                 Image(systemName: "circle")
                     .frame(width: 22, height: 22)
@@ -459,7 +408,7 @@ struct TaskListView: View, TaskListViewProtocol {
                 taskID: taskID,
                 index: index,
                 totalTasks: displayActiveTasks.count,
-                isSelected: selectedTaskID == taskID,
+                isSelected: fState.selectedTaskID == taskID,
                 isDragging: isDraggingStateBinding,
                 isLastActiveTask: index == displayActiveTasks.count - 1,
                 focusedField: $focusedFieldBinding,
@@ -469,8 +418,8 @@ struct TaskListView: View, TaskListViewProtocol {
                 onSelect: { selectTask($0) },
                 onStartEdit: { startEditing($0) },
                 onEndEdit: {
-                    if selectedTaskID == $0 {
-                        selectedTaskID = nil
+                    if fState.selectedTaskID == $0 {
+                        fState.selectedTaskID = nil
                     }
                     endEditing($0, shouldCreateNewTask: $1)
                 }
@@ -491,7 +440,7 @@ struct TaskListView: View, TaskListViewProtocol {
             .onGeometryChange(for: CGRect.self) { proxy in
                 proxy.frame(in: .global)
             } action: { frame in
-                rowFrames[taskID] = frame
+                iState.rowFrames[taskID] = frame
             }
             .id(taskID)
         }
@@ -504,7 +453,7 @@ struct TaskListView: View, TaskListViewProtocol {
             TaskRowView(
                 task: task,
                 taskID: taskID,
-                isSelected: selectedTaskID == taskID,
+                isSelected: fState.selectedTaskID == taskID,
                 focusedField: $focusedFieldBinding,
                 onToggle: { toggleCompletion($0) },
                 onTitleChange: { updateTitle($0, $1) },
@@ -596,7 +545,7 @@ struct TaskListView: View, TaskListViewProtocol {
                 }
                 .padding(
                     .bottom,
-                    (pullToCreate.shouldShowIndicator && !isPrependDraftVisible)
+                    (iState.pullToCreate.shouldShowIndicator && !isPrependDraftVisible)
                         ? (pullToCreateGap - vStackSpacing) : 0
                 )
                 taskRows
@@ -605,7 +554,7 @@ struct TaskListView: View, TaskListViewProtocol {
             .frame(maxWidth: .infinity, alignment: .topLeading)
             .padding(.trailing, 16)
             .padding(.vertical, 12)
-            .offset(y: -pullToCreate.pullOffset)
+            .offset(y: -iState.pullToCreate.pullOffset)
             .onChange(of: focusedFieldBinding) { oldValue, newValue in
                 fState.focusedField = newValue
                 handleFocusChange(from: oldValue, to: newValue)
@@ -614,10 +563,10 @@ struct TaskListView: View, TaskListViewProtocol {
                     !iState.isShowingSettings,
                     !iState.isShowingSyncDiagnostics
                 {
-                    if let pending = pendingFocus {
+                    if let pending = fState.pendingFocus {
                         focusedFieldBinding = pending
                         fState.focusedField = pending
-                        pendingFocus = nil
+                        fState.pendingFocus = nil
                     } else {
                         focusedFieldBinding = .scrollView
                         fState.focusedField = .scrollView