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