listless

A simple list app for Apple platforms
Log | Files | Refs | README | LICENSE

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