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