eleven / messenger Public

mac: @MainActor-gate ChatTextSize's UserDefaults writers (#1200)

ChatTextSize.bump/reset were nonisolated static funcs that write
UserDefaults.standard, so their main-thread safety rested on call-site
discipline rather than the compiler. Every caller (View-menu commands, the
⌘= key monitor) already runs on the main thread, but nothing stopped a
future off-main caller — the same UserDefaults concurrent-write hazard that
caused the CFBasicHash crash fixed for shared code in #1194/#1196.

Mark the two writers @MainActor so the guarantee is compiler-enforced. Pure
reads/constants (current, snap, stops) stay nonisolated. No call-site changes
needed; Mac app builds clean.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Dermot Daly eccce7b95a9b0f94eb3869512041047398fbf7f2 parent efd0add
1 file changed, +7 −2
  • ios/LChatMac/TextSize.swift +7 −2
diff --git a/ios/LChatMac/TextSize.swift b/ios/LChatMac/TextSize.swift
index 6003f0e..ed2cc48 100644
--- a/ios/LChatMac/TextSize.swift
+++ b/ios/LChatMac/TextSize.swift
@@ -23,13 +23,18 @@ enum ChatTextSize {
stops.min(by: { abs($0 - v) < abs($1 - v) }) ?? defaultSize
}
- static func bump(_ direction: Int) {
+ // @MainActor on the two WRITERS (not the pure reads/constants): UserDefaults
+ // is process-global and unsafe against concurrent writes from two threads, so
+ // every writer must stay on the main actor. All callers already do (View-menu
+ // commands, the ⌘= key monitor); this makes that a compiler-enforced guarantee
+ // rather than call-site discipline.
+ @MainActor static func bump(_ direction: Int) {
guard let i = stops.firstIndex(of: current) else { return }
let j = min(max(i + direction, 0), stops.count - 1)
UserDefaults.standard.set(stops[j], forKey: key)
}
- static func reset() {
+ @MainActor static func reset() {
UserDefaults.standard.set(defaultSize, forKey: key)
}
}