← All repositories

listless

A simple list app for Apple platforms

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


commit b9c2b35dc7f8aabe4d7bd6c0f02005fe7248f72a
parent 39ccaaaaf50ed04e49886e7f48f080fc58cb7efc
Author: Michael Camilleri <[email protected]>
Date:   Wed, 18 Feb 2026 18:54:26 +0900

Improve code quality

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

Diffstat

MListless.xcodeproj/project.pbxproj | 4++++
MListless/Extensions/TaskListView+Logic.swift | 3++-
MListlessiOS/Extensions/TaskListView+PullGestures.swift | 33+++++++++++++++++++--------------
MListlessiOS/Extensions/TaskListView+PullToCreate.swift | 3++-
MListlessiOS/Helpers/TappableTextField.swift | 6++++--
AListlessiOS/Helpers/TaskRowMetrics.swift | 11+++++++++++
MListlessiOS/Views/PullToCreate.swift | 19+++++++++----------
MListlessiOS/Views/TaskListView.swift | 3++-
MListlessiOS/Views/TaskRowView.swift | 24++++++++++++++----------

9 files changed, 67 insertions(+), 39 deletions(-)

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

@@ -53,6 +53,7 @@
 		F50E8B8F73F64D8E641DC74C /* TaskStoreCompletionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E88DDD119EEEECCD45F36D2 /* TaskStoreCompletionTests.swift */; };
 		F6587B84ECC6BFE92A5FB493 /* KeyboardNavigationModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4669414A460FC0758D5B49A8 /* KeyboardNavigationModifier.swift */; };
 		FDD09FECEED48EC9598538F4 /* TaskListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 632DA39B24C4CF1528A1A24D /* TaskListView.swift */; };
+		FF58A3AB2BCFFA42BF2F6413 /* TaskRowMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2EF927F09D67532F44BB80D /* TaskRowMetrics.swift */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -97,6 +98,7 @@
 		9B2BBE01D99CDA278BCB9F49 /* TaskStoreEdgeCaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskStoreEdgeCaseTests.swift; sourceTree = "<group>"; };
 		9F5D8B5866362D422A2A331C /* HoverCursorModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoverCursorModifier.swift; sourceTree = "<group>"; };
 		9FBEB58DD41817F09B0EB9F0 /* Listless.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Listless.xcdatamodel; sourceTree = "<group>"; };
+		A2EF927F09D67532F44BB80D /* TaskRowMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskRowMetrics.swift; sourceTree = "<group>"; };
 		AC245331D715EA85887C0BA0 /* ListlessiOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListlessiOSApp.swift; sourceTree = "<group>"; };
 		B7588879D0FA1C2A8BCEF14F /* HoverCursorModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoverCursorModifier.swift; sourceTree = "<group>"; };
 		BFF7D84B54AE70036D205CA4 /* PullToCreate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullToCreate.swift; sourceTree = "<group>"; };
@@ -129,6 +131,7 @@
 				E2174C43654733E9D4023157 /* PlatformTextFieldWidthModifier.swift */,
 				6ECE0E961F87BA32FA87BF90 /* TappableTextField.swift */,
 				D57C3CC81C4380EFAE4DB910 /* TaskRowDragGesture.swift */,
+				A2EF927F09D67532F44BB80D /* TaskRowMetrics.swift */,
 				067D666DF0AF1C53404ECF7C /* TaskRowSwipeGesture.swift */,
 			);
 			path = Helpers;
@@ -464,6 +467,7 @@
 				77970A2323A598E830D95301 /* TaskListView+Toolbar.swift in Sources */,
 				C169823665158AA347A63990 /* TaskListView.swift in Sources */,
 				2C53717A0EAFA39240615C9A /* TaskRowDragGesture.swift in Sources */,
+				FF58A3AB2BCFFA42BF2F6413 /* TaskRowMetrics.swift in Sources */,
 				182D9FB61E3A3650D7D83D2A /* TaskRowSwipeGesture.swift in Sources */,
 				7E366008FF9335A77FE1636D /* TaskRowView.swift in Sources */,
 				91EDF52C7C5C0B35E9D8B51E /* TaskStore.swift in Sources */,

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

@@ -42,13 +42,14 @@ extension TaskListView {
 
     // MARK: - Task Creation
 
-    func createNewTaskAtTop() {
+    func createNewTaskAtTop() -> UUID {
         draggedTaskID = nil
         visualOrder = nil
         let task = store.createTask(title: "", atBeginning: true)
         pendingFocus = .task(task.id)
         focusedField = .task(task.id)
         selectedTaskID = task.id
+        return task.id
     }
 
     func createNewTask() {

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

@@ -11,7 +11,7 @@ extension TaskListView {
         var pullOffset: CGFloat = 0
         var indicatorOffset: CGFloat = 0
         var isInsertionPending: Bool = false
-        var activeTaskCountBeforeCreate: Int = 0
+        var pendingTaskID: UUID?
         var isScrollInteracting: Bool = false
 
         var shouldShowIndicator: Bool {
@@ -32,25 +32,27 @@ extension TaskListView {
         mutating func handlePhaseChange(
             from oldPhase: ScrollPhase,
             to newPhase: ScrollPhase,
-            activeTaskCount: Int,
             threshold: CGFloat
         ) -> Action {
             isScrollInteracting = (newPhase == .interacting)
             guard oldPhase == .interacting, newPhase != .interacting else { return .none }
 
             if pullOffset >= threshold {
-                activeTaskCountBeforeCreate = activeTaskCount
                 isInsertionPending = true
+                pendingTaskID = nil
                 return .createTask
             }
 
             isInsertionPending = false
+            pendingTaskID = nil
             return .collapseIndicator
         }
 
-        mutating func resolvePendingInsertion(activeTaskCount: Int) {
-            guard isInsertionPending, activeTaskCount > activeTaskCountBeforeCreate else { return }
+        mutating func resolvePendingInsertion(activeTaskIDs: [UUID]) {
+            guard isInsertionPending, let pendingTaskID else { return }
+            guard activeTaskIDs.contains(pendingTaskID) else { return }
             isInsertionPending = false
+            self.pendingTaskID = nil
             indicatorOffset = 0
         }
     }
@@ -60,11 +62,11 @@ private struct PullCreationGestureModifier: ViewModifier {
     @Binding var pullToCreate: TaskListView.PullToCreateState
     @Binding var pullUpOffset: CGFloat
 
-    let activeTaskCount: Int
+    let activeTaskIDs: [UUID]
     let hasCompletedTasks: Bool
     let pullCreateThreshold: CGFloat
     let pullClearThreshold: CGFloat
-    let onCreateTaskAtTop: () -> Void
+    let onCreateTaskAtTop: () -> UUID
     let onClearCompleted: () -> Void
 
     func body(content: Content) -> some View {
@@ -86,11 +88,11 @@ private struct PullCreationGestureModifier: ViewModifier {
             .onScrollPhaseChange { oldPhase, newPhase in
                 handlePullToCreateScrollPhaseChange(from: oldPhase, to: newPhase)
             }
-            .onChange(of: activeTaskCount) { _, newCount in
+            .onChange(of: activeTaskIDs) { _, newIDs in
                 var transaction = Transaction(animation: nil)
                 transaction.disablesAnimations = true
                 withTransaction(transaction) {
-                    pullToCreate.resolvePendingInsertion(activeTaskCount: newCount)
+                    pullToCreate.resolvePendingInsertion(activeTaskIDs: newIDs)
                 }
             }
             .sensoryFeedback(
@@ -108,7 +110,6 @@ private struct PullCreationGestureModifier: ViewModifier {
         let action = pullToCreate.handlePhaseChange(
             from: oldPhase,
             to: newPhase,
-            activeTaskCount: activeTaskCount,
             threshold: pullCreateThreshold
         )
 
@@ -118,8 +119,12 @@ private struct PullCreationGestureModifier: ViewModifier {
         case .createTask:
             var transaction = Transaction(animation: .spring(response: 0.28, dampingFraction: 0.9))
             transaction.disablesAnimations = false
+            var createdTaskID: UUID?
             withTransaction(transaction) {
-                onCreateTaskAtTop()
+                createdTaskID = onCreateTaskAtTop()
+            }
+            if let createdTaskID {
+                pullToCreate.pendingTaskID = createdTaskID
             }
         case .collapseIndicator:
             withAnimation(.spring(response: 0.22, dampingFraction: 0.95)) {
@@ -140,18 +145,18 @@ extension View {
     func pullCreationGesture(
         pullToCreate: Binding<TaskListView.PullToCreateState>,
         pullUpOffset: Binding<CGFloat>,
-        activeTaskCount: Int,
+        activeTaskIDs: [UUID],
         hasCompletedTasks: Bool,
         pullCreateThreshold: CGFloat,
         pullClearThreshold: CGFloat,
-        onCreateTaskAtTop: @escaping () -> Void,
+        onCreateTaskAtTop: @escaping () -> UUID,
         onClearCompleted: @escaping () -> Void
     ) -> some View {
         modifier(
             PullCreationGestureModifier(
                 pullToCreate: pullToCreate,
                 pullUpOffset: pullUpOffset,
-                activeTaskCount: activeTaskCount,
+                activeTaskIDs: activeTaskIDs,
                 hasCompletedTasks: hasCompletedTasks,
                 pullCreateThreshold: pullCreateThreshold,
                 pullClearThreshold: pullClearThreshold,

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

@@ -4,7 +4,8 @@ extension TaskListView {
     @ViewBuilder var pullToCreateIndicatorRow: some View {
         if pullToCreate.shouldShowIndicator {
             PullToCreateIndicator(
-                pullOffset: pullToCreate.indicatorDisplayOffset(threshold: pullCreateThreshold)
+                pullOffset: pullToCreate.indicatorDisplayOffset(threshold: pullCreateThreshold),
+                threshold: pullCreateThreshold
             )
         }
     }

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

@@ -48,8 +48,10 @@ struct TappableTextField: UIViewRepresentable {
     }
 
     func sizeThatFits(_ proposal: ProposedViewSize, uiView: UITextView, context: Context) -> CGSize? {
-        let width = proposal.width ?? UIScreen.main.bounds.width
-        return uiView.sizeThatFits(CGSize(width: width, height: .infinity))
+        let proposedWidth = proposal.width ?? uiView.bounds.width
+        let width = proposedWidth > 0 ? proposedWidth : (uiView.window?.bounds.width ?? 0)
+        guard width > 0 else { return nil }
+        return uiView.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude))
     }
 
     func makeCoordinator() -> Coordinator {

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

@@ -0,0 +1,11 @@
+import CoreGraphics
+
+enum TaskRowMetrics {
+    static let accentBarWidth: CGFloat = 8
+    static let trailingCornerRadius: CGFloat = 14
+    static let contentSpacing: CGFloat = 12
+    static let contentVerticalPadding: CGFloat = 14
+    static let contentHorizontalPadding: CGFloat = 16
+    static let activeLeadingPadding: CGFloat = 24
+    static let completedLeadingPadding: CGFloat = 16
+}

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

@@ -1,13 +1,11 @@
 import SwiftUI
 
-/// Pull distance at which the indicator signals readiness and task creation triggers.
-let pullCreateThreshold: CGFloat = 70
-
 struct PullToCreateIndicator: View {
     let pullOffset: CGFloat
+    let threshold: CGFloat
 
-    private var progress: CGFloat { min(1, pullOffset / pullCreateThreshold) }
-    private var isReady: Bool { pullOffset >= pullCreateThreshold }
+    private var progress: CGFloat { min(1, pullOffset / threshold) }
+    private var isReady: Bool { pullOffset >= threshold }
     private let indicatorHeight: CGFloat = 48
     private let textSlideDistance: CGFloat = 22
 
@@ -18,8 +16,8 @@ struct PullToCreateIndicator: View {
         HStack(spacing: 0) {
             Rectangle()
                 .fill(accentColor)
-                .frame(width: 8)
-            HStack(alignment: .center, spacing: 12) {
+                .frame(width: TaskRowMetrics.accentBarWidth)
+            HStack(alignment: .center, spacing: TaskRowMetrics.contentSpacing) {
                 Image(systemName: "circle")
                     .foregroundStyle(.secondary)
                     .font(.system(size: 17))
@@ -37,8 +35,8 @@ struct PullToCreateIndicator: View {
                 .animation(.easeInOut(duration: 0.18), value: isReady)
                 Spacer()
             }
-            .padding(.vertical, 14)
-            .padding(.horizontal, 16)
+            .padding(.vertical, TaskRowMetrics.contentVerticalPadding)
+            .padding(.horizontal, TaskRowMetrics.contentHorizontalPadding)
             .frame(maxWidth: .infinity, alignment: .leading)
             .background(Color.taskCard)
         }
@@ -46,7 +44,8 @@ struct PullToCreateIndicator: View {
         .clipShape(
             UnevenRoundedRectangle(
                 topLeadingRadius: 0, bottomLeadingRadius: 0,
-                bottomTrailingRadius: 14, topTrailingRadius: 14
+                bottomTrailingRadius: TaskRowMetrics.trailingCornerRadius,
+                topTrailingRadius: TaskRowMetrics.trailingCornerRadius
             )
         )
         // Reveal from the top downward as the user pulls

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

@@ -30,6 +30,7 @@ struct TaskListView: View {
     @State var rowFrames: [UUID: CGRect] = [:]
 
     var vStackSpacing: CGFloat { 12 }
+    var pullCreateThreshold: CGFloat { 70 }
 
     init(store: TaskStore = TaskStore()) {
         _store = State(wrappedValue: store)
@@ -157,7 +158,7 @@ struct TaskListView: View {
         .pullCreationGesture(
             pullToCreate: $pullToCreate,
             pullUpOffset: $pullUpOffset,
-            activeTaskCount: activeTasks.count,
+            activeTaskIDs: activeTasks.map(\.id),
             hasCompletedTasks: !completedTasks.isEmpty,
             pullCreateThreshold: pullCreateThreshold,
             pullClearThreshold: pullClearThreshold,

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

@@ -53,7 +53,7 @@ struct TaskRowView: View {
     }
 
     var body: some View {
-        HStack(alignment: .center, spacing: 12) {
+        HStack(alignment: .center, spacing: TaskRowMetrics.contentSpacing) {
             Button {
                 onToggle(task)
             } label: {
@@ -80,9 +80,12 @@ struct TaskRowView: View {
             .focused($focusedField, equals: .task(taskID))
             .frame(maxWidth: .infinity, alignment: .leading)
         }
-        .padding(.vertical, 14)
-        .padding(.trailing, 16)
-        .padding(.leading, task.isCompleted ? 16 : 24)
+        .padding(.vertical, TaskRowMetrics.contentVerticalPadding)
+        .padding(.trailing, TaskRowMetrics.contentHorizontalPadding)
+        .padding(
+            .leading,
+            task.isCompleted ? TaskRowMetrics.completedLeadingPadding : TaskRowMetrics.activeLeadingPadding
+        )
         .frame(maxWidth: .infinity, alignment: .leading)
         .contentShape(Rectangle())
         .onTapGesture {
@@ -106,15 +109,16 @@ struct TaskRowView: View {
         .clipShape(
             UnevenRoundedRectangle(
                 topLeadingRadius: 0, bottomLeadingRadius: 0,
-                bottomTrailingRadius: task.isCompleted ? 0 : 14,
-                topTrailingRadius: task.isCompleted ? 0 : 14
+                bottomTrailingRadius: task.isCompleted ? 0 : TaskRowMetrics.trailingCornerRadius,
+                topTrailingRadius: task.isCompleted ? 0 : TaskRowMetrics.trailingCornerRadius
             )
         )
         .overlay {
             if isSelected && !task.isCompleted {
                 UnevenRoundedRectangle(
                     topLeadingRadius: 0, bottomLeadingRadius: 0,
-                    bottomTrailingRadius: 14, topTrailingRadius: 14
+                    bottomTrailingRadius: TaskRowMetrics.trailingCornerRadius,
+                    topTrailingRadius: TaskRowMetrics.trailingCornerRadius
                 )
                 .strokeBorder(Color.accentColor, lineWidth: 2)
             }
@@ -123,7 +127,7 @@ struct TaskRowView: View {
             if !task.isCompleted {
                 Rectangle()
                     .fill(cachedAccentColor)
-                    .frame(width: 8)
+                    .frame(width: TaskRowMetrics.accentBarWidth)
             }
         }
         .onAppear {
@@ -164,8 +168,8 @@ struct TaskRowView: View {
         .clipShape(
             UnevenRoundedRectangle(
                 topLeadingRadius: 0, bottomLeadingRadius: 0,
-                bottomTrailingRadius: task.isCompleted ? 0 : 14,
-                topTrailingRadius: task.isCompleted ? 0 : 14
+                bottomTrailingRadius: task.isCompleted ? 0 : TaskRowMetrics.trailingCornerRadius,
+                topTrailingRadius: task.isCompleted ? 0 : TaskRowMetrics.trailingCornerRadius
             )
         )
     }