← All repositories

listless

A simple list app for Apple platforms

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


commit 0ea68133bf492fe7f8038b08e5073f1b2e2a995c
parent b56a533c2d4dcc3b519c5c5c5140d43d9e13d69b
Author: Michael Camilleri <[email protected]>
Date:   Thu, 26 Feb 2026 06:59:21 +0900

Avoid multiple updates during SwiftUI

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

Diffstat

MListlessMac/Helpers/ClickableTextField.swift | 19+++++++++++++++++--
MListlessMac/Views/TaskRowView.swift | 8++++----
MListlessiOS/Helpers/TappableTextField.swift | 9+++++++--
MListlessiOS/Views/TaskRowView.swift | 8++++----

4 files changed, 32 insertions(+), 12 deletions(-)

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

@@ -19,6 +19,7 @@ struct ClickableTextField: NSViewRepresentable {
     @Binding var text: String
     let isCompleted: Bool
     let onEditingChanged: (Bool, _ shouldCreateNewTask: Bool) -> Void
+    var onContentChange: ((String) -> Void)? = nil
 
     func makeNSView(context: Context) -> ClickableNSTextField {
         let textField = ClickableNSTextField()
@@ -86,7 +87,7 @@ struct ClickableTextField: NSViewRepresentable {
     }
 
     func makeCoordinator() -> Coordinator {
-        Coordinator(text: $text, onEditingChanged: onEditingChanged)
+        Coordinator(text: $text, onEditingChanged: onEditingChanged, onContentChange: onContentChange)
     }
 
     enum EditEndReason {
@@ -129,14 +130,17 @@ struct ClickableTextField: NSViewRepresentable {
     final class Coordinator: NSObject, NSTextFieldDelegate {
         @Binding var text: String
         let onEditingChanged: (Bool, _ shouldCreateNewTask: Bool) -> Void
+        let onContentChange: ((String) -> Void)?
         var editEndReason: EditEndReason = .focusLost
 
         init(
             text: Binding<String>,
-            onEditingChanged: @escaping (Bool, _ shouldCreateNewTask: Bool) -> Void
+            onEditingChanged: @escaping (Bool, _ shouldCreateNewTask: Bool) -> Void,
+            onContentChange: ((String) -> Void)? = nil
         ) {
             _text = text
             self.onEditingChanged = onEditingChanged
+            self.onContentChange = onContentChange
         }
 
         @MainActor
@@ -155,11 +159,21 @@ struct ClickableTextField: NSViewRepresentable {
                 string: text, attributes: attributes)
         }
 
+        private var hasNotifiedEditingStarted = false
+
         func handleBecomeFirstResponder() {
+            hasNotifiedEditingStarted = true
+            onEditingChanged(true, false)
+        }
+
+        func controlTextDidBeginEditing(_ obj: Notification) {
+            guard !hasNotifiedEditingStarted else { return }
+            hasNotifiedEditingStarted = true
             onEditingChanged(true, false)
         }
 
         func controlTextDidEndEditing(_ obj: Notification) {
+            hasNotifiedEditingStarted = false
             let shouldCreateNewTask = editEndReason == .returnKey
             editEndReason = .focusLost  // Reset for next time
             onEditingChanged(false, shouldCreateNewTask)
@@ -168,6 +182,7 @@ struct ClickableTextField: NSViewRepresentable {
         func controlTextDidChange(_ obj: Notification) {
             guard let textField = obj.object as? NSTextField else { return }
             text = textField.stringValue
+            onContentChange?(textField.stringValue)
         }
 
         func control(

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

@@ -90,6 +90,10 @@ struct TaskRowView: View {
                     } else {
                         onEndEdit(taskID, shouldCreateNewTask)
                     }
+                },
+                onContentChange: { newTitle in
+                    guard !task.isCompleted else { return }
+                    onTitleChange(task, newTitle)
                 }
             )
             .focused($focusedField, equals: .task(taskID))
@@ -151,10 +155,6 @@ struct TaskRowView: View {
                 onDelete(task)
             }
         }
-        .onChange(of: editingTitle) {
-            guard !task.isCompleted else { return }
-            onTitleChange(task, editingTitle)
-        }
         .onChange(of: task.title) { _, newValue in
             // Keep editingTitle in sync with task.title when not editing
             if !isCurrentlyEditing {

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

@@ -8,6 +8,7 @@ struct TappableTextField: UIViewRepresentable {
     @Binding var text: String
     let isCompleted: Bool
     let onEditingChanged: (Bool, _ shouldCreateNewTask: Bool) -> Void
+    var onContentChange: ((String) -> Void)? = nil
 
     func makeUIView(context: Context) -> UITextView {
         let textView = UITextView()
@@ -55,7 +56,7 @@ struct TappableTextField: UIViewRepresentable {
     }
 
     func makeCoordinator() -> Coordinator {
-        Coordinator(text: $text, onEditingChanged: onEditingChanged)
+        Coordinator(text: $text, onEditingChanged: onEditingChanged, onContentChange: onContentChange)
     }
 
     private func applyStyle(to textView: UITextView, text: String, isCompleted: Bool) {
@@ -73,18 +74,22 @@ struct TappableTextField: UIViewRepresentable {
     final class Coordinator: NSObject, UITextViewDelegate {
         @Binding var text: String
         let onEditingChanged: (Bool, _ shouldCreateNewTask: Bool) -> Void
+        let onContentChange: ((String) -> Void)?
         var returnKeyPressed: Bool = false
 
         init(
             text: Binding<String>,
-            onEditingChanged: @escaping (Bool, _ shouldCreateNewTask: Bool) -> Void
+            onEditingChanged: @escaping (Bool, _ shouldCreateNewTask: Bool) -> Void,
+            onContentChange: ((String) -> Void)? = nil
         ) {
             _text = text
             self.onEditingChanged = onEditingChanged
+            self.onContentChange = onContentChange
         }
 
         func textViewDidChange(_ textView: UITextView) {
             text = textView.text
+            onContentChange?(textView.text)
             if let placeholder = textView.viewWithTag(100) as? UILabel {
                 placeholder.isHidden = !textView.text.isEmpty
             }

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

@@ -79,6 +79,10 @@ struct TaskRowView: View {
                         if editing { onStartEdit(taskID) }
                         else { onEndEdit(taskID, shouldCreateNewTask) }
                     }
+                },
+                onContentChange: { newTitle in
+                    guard !task.isCompleted else { return }
+                    onTitleChange(task, newTitle)
                 }
             )
             .focused($focusedField, equals: .task(taskID))
@@ -138,10 +142,6 @@ struct TaskRowView: View {
             editingTitle = task.title
             cachedAccentColor = computeAccentColor()
         }
-        .onChange(of: editingTitle) {
-            guard !task.isCompleted else { return }
-            onTitleChange(task, editingTitle)
-        }
         .onChange(of: task.title) { _, newValue in
             if !isCurrentlyEditing {
                 editingTitle = newValue