Hi everybody.
We have created this app around two months and this is third version with fixed bugs. Now it is amazing app that synchronization your tasks throughs iPhone iPad Mac via iCloud with no Sign In! And in pocket you will already have a useful notes.
It was a huge code work. Hours and hours. Such a pleasure.
What I want to note here for you fellas, we were going from these scheme:
What's really happening
The self-overwrite loop
User edits subtask title
↓
onChange fires → scheduleSave() → debounce 1s
↓
...debounce fires → DataManager.save() writes todos.json to iCloud
↓
NSMetadataQuery detects file change on disk
↓ ↑
└── todosChanged() ───────┘
↓
taskManager.loadTodos()
↓
self.todos = loadedTodos ← 💥 replaces entire array mid-edit
The core problem is that NSMetadataQuery watches the file at the OS level. It has no concept of who made the change — your own app writing the file looks identical to another device syncing a change over iCloud. So every save you make triggers a reload that cancels whatever the user is currently doing.
Why subtask titles are worst affected
A TextField bound to $subtask.title is live — it reflects the array value character by character. The moment self.todos = loadedTodos runs, SwiftUI throws away the in-memory array and rebuilds from the freshly decoded JSON. If the save hasn't happened yet (debounce still counting down), the loaded file has the old title, and the field visually snaps back.
Progress sliders have the same issue but it's less noticeable because a slider value is a Double — the snap-back is a jump rather than disappearing characters.
Why it only shows up on real devices
The simulator runs everything on the same Mac so iCloud writes are near-instant and the race window is tiny. On a real device the file system is slower and iCloud sync adds latency, making the timing gap between "user is editing" and "reload fires" much more visible.
The three fixes needed
Fix 1 — Ignore self-triggered reloads in DataManager
Track a isSavingLocally flag. Set it true before writing, false after. In todosChanged() skip the reload if the flag is set. This stops your own saves from triggering reloads entirely.
Fix 2 — Add an isEditing guard in TaskManager
Expose a simple isUserEditing: Bool flag that ContentView sets to true when a TextField is focused and false on onSubmit/focus loss. loadTodos() checks this flag and skips the reload if the user is mid-edit, queuing it for after they finish.
Fix 3 — Debounce the reload too, not just the save
Right now saves are debounced (1 second) but reloads are instant. If a remote change does arrive legitimately, it should also wait briefly before replacing the array — giving any in-flight edits time to be committed first. A 0.5-second debounce on todosChanged() is enough.
TO THIS:
Own save → flag set → reload blocked entirely
Mid-edit → isEditing → reload deferred until focus lost
Remote sync → debounced → reload waits 0.5s before replacing array
Interesting in your opinions.
[LINK TO THE APP(https://apps.apple.com/us/app/do-list-100/id6758045201)]
Top comments (0)