ChatViewModel is a @MainActor ObservableObject and holds the only copy
of the conversation list. Both screens read from and write to this one
instance, so a sent message updates the inbox and the open thread at the
same time.
ConversationService is an actor. It handles JSON load and save, and
being an actor keeps file I/O off the main thread without scattering
Task.detached at the call sites.
Conversation and Message are plain Codable structs. Snake-case JSON
keys are mapped via keyDecodingStrategy.
This uses the older ObservableObject / @Published / @StateObject
pattern rather than the newer @Observable macro. The team isn't able to
adopt @Observable yet, so I matched the style they actually use. The
shape of the view model is the same either way; switching it later is a
mechanical change.
On launch, ConversationService.load() looks for a saved file in the app's
Documents directory. If nothing is there, it falls back to the bundled
code_test_data.json seed. Every send writes the full conversation list
back to that file atomically. Kill the app, relaunch, and the messages are
still there.
Conversations are sorted by lastUpdated descending in the view model
after load and after every send, so the most recently active thread sits
on top. Messages are sorted ascending in the message view, oldest first.
Split MessageView out into its own MessageViewModel instead of reaching
back into ChatViewModel. Fine at this size, awkward at scale.
Replace whole-file JSON persistence with SwiftData or per-conversation files. Rewriting the entire list on every send wastes work once threads get long.
Surface save failures in the UI instead of swallowing them with a print.
Sort messages once when the conversation opens rather than re-sorting
inside the view's body on every render.
Add unit tests for the sort and send logic in ChatViewModel, plus a
fake ConversationService to drive the offline-first paths.
Cover the missing states: empty inbox, failed load, failed save. Also accessibility labels on rows and a proper keyboard-aware layout.
Used Claude as a co-pilot for architecture discussion and SwiftUI syntax reminders. Code, decisions, and design choices are mine.