{"title":"Keeping A SwiftUI App Quiet When Nothing Changed","html_url":"https://deallocatedobjects.com/posts/keeping-a-swiftui-app-quiet-when-nothing-changed","source_url":"","markdown_content":"`@Published` doesn't compare values. Assign the same value and it still tells every observer that something changed. Here's a small test:\n\n```swift\nfinal class Store: ObservableObject {\n    @Published var isLoading = false\n    @Published var error: String? = nil\n}\n\nlet store = Store()\nvar count = 0\nlet sink = store.objectWillChange.sink { count += 1 }\n\nfor _ in 0..<5 {\n    store.isLoading = false\n    store.error = nil\n}\nprint(count) // 10\n```\n\nThat matters most in code that runs on a timer. [Postage](https://postageformac.com/?ref=deallocated-post), my Mac email client, refreshes in the background every minute on my Mac, and most of those refreshes find nothing new. The store's routine bookkeeping came to about five assignments plus two per account each time. Each one republishes the store and re-renders every window observing it.\n\nSo the refresh now checks before it assigns:\n\n```swift title=\"EmailStore.swift\"\nif error != nil { error = nil }\nif isLoading { isLoading = false }\nif !hasAttemptedLoad { hasAttemptedLoad = true }\n\nlet total = known.reduce(0) { $0 + $1.messageCount }\nif serverTotalCount != total { serverTotalCount = total }\n```\n\n`removeDuplicates()` won't help here. It filters what a subscriber to `$isLoading` sees, while SwiftUI is listening to `objectWillChange`, which has already fired.\n\n### The Same Check Further Down\n\nOnce the UI was quiet, the same pattern showed up in two other places in that refresh.\n\nThe Dock badge was set per folder and per account on every pass. `EmailStore` is the only thing in the app that writes the badge, so it can remember the last value:\n\n```swift\nprivate var lastBadgeCount: Int?\n\nprivate func setBadgeIfChanged(_ count: Int) {\n    guard lastBadgeCount != count else { return }\n    lastBadgeCount = count\n    UNUserNotificationCenter.current().setBadgeCount(count)\n}\n```\n\nIf you use this, make sure nothing else sets the badge, or the remembered value goes stale.\n\nThe database got one commit per folder to store each mailbox's sync state, even when it hadn't moved. IMAP makes this check easy: if `UIDVALIDITY` and `UIDNEXT` match what's stored, nothing new has arrived, and there's nothing to write:\n\n```swift\nlet state = MailboxSyncState(uidValidity: selection.uidValidity, uidNext: selection.uidNext)\nif state != previousState {\n    db.setMailboxState(state, accountEmail: scope.accountEmail, mailbox: scope.mailbox)\n}\n```\n\nTo find this in your own app, start with whatever runs on a timer and look at what it does when nothing has changed.","html_content":"<p><code>@Published</code> doesn’t compare values. Assign the same value and it still tells every observer that something changed. Here’s a small test:</p>\n<figure class=\"code\" data-lang=\"swift\"><figcaption><span class=\"lang\">Swift</span></figcaption><pre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\"><code><span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">final</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> class</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> Store</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">: </span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">ObservableObject </span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">{</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">    @Published</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> var</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> isLoading </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> false</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">    @Published</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> var</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> error: </span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">String</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">?</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> nil</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">let</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> store </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> Store</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">()</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">var</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> count </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> 0</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">let</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> sink </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> store.objectWillChange.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">sink</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> { count </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">+=</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> 1</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">for</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> _</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> in</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> 0</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">..&#x3C;</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">5</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">    store.isLoading </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> false</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">    store.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">error</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> =</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> nil</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">}</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">print</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(count) </span><span style=\"--shiki-light:#6A737D;--shiki-dark:#6A737D\">// 10</span></span></code></pre></figure><p>That matters most in code that runs on a timer. <a href=\"https://postageformac.com/?ref=deallocated-post\">Postage</a>, my Mac email client, refreshes in the background every minute on my Mac, and most of those refreshes find nothing new. The store’s routine bookkeeping came to about five assignments plus two per account each time. Each one republishes the store and re-renders every window observing it.</p>\n<p>So the refresh now checks before it assigns:</p>\n<figure class=\"code\" data-lang=\"swift\"><figcaption><span class=\"file\">EmailStore.swift</span><span class=\"lang\">Swift</span></figcaption><pre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\"><code><span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> error </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">!=</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> nil</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> { error </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> nil</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> isLoading { isLoading </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> false</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> !</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">hasAttemptedLoad { hasAttemptedLoad </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> true</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">let</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> total </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> known.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">reduce</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">0</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">) { </span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">$0</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> +</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> $1</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">.messageCount }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> serverTotalCount </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">!=</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> total { serverTotalCount </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> total }</span></span></code></pre></figure><p><code>removeDuplicates()</code> won’t help here. It filters what a subscriber to <code>$isLoading</code> sees, while SwiftUI is listening to <code>objectWillChange</code>, which has already fired.</p>\n<h2 id=\"the-same-check-further-down\" data-level=\"3\">The Same Check Further Down</h2>\n<p>Once the UI was quiet, the same pattern showed up in two other places in that refresh.</p>\n<p>The Dock badge was set per folder and per account on every pass. <code>EmailStore</code> is the only thing in the app that writes the badge, so it can remember the last value:</p>\n<figure class=\"code\" data-lang=\"swift\"><figcaption><span class=\"lang\">Swift</span></figcaption><pre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\"><code><span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">private</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> var</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> lastBadgeCount: </span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">Int</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">?</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">private</span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\"> func</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\"> setBadgeIfChanged</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"--shiki-light:#6F42C1;--shiki-dark:#B392F0\">_</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> count: </span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">Int</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">) {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">    guard</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> lastBadgeCount </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">!=</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> count </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">else</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> { </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">return</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> }</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">    lastBadgeCount </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> count</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">    UNUserNotificationCenter.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">current</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">().</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">setBadgeCount</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(count)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">}</span></span></code></pre></figure><p>If you use this, make sure nothing else sets the badge, or the remembered value goes stale.</p>\n<p>The database got one commit per folder to store each mailbox’s sync state, even when it hadn’t moved. IMAP makes this check easy: if <code>UIDVALIDITY</code> and <code>UIDNEXT</code> match what’s stored, nothing new has arrived, and there’s nothing to write:</p>\n<figure class=\"code\" data-lang=\"swift\"><figcaption><span class=\"lang\">Swift</span></figcaption><pre class=\"shiki shiki-themes github-light github-dark\" style=\"--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e\" tabindex=\"0\"><code><span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">let</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> state </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">=</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\"> MailboxSyncState</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">uidValidity</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">: selection.uidValidity, </span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">uidNext</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">: selection.uidNext)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">if</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> state </span><span style=\"--shiki-light:#D73A49;--shiki-dark:#F97583\">!=</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\"> previousState {</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">    db.</span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">setMailboxState</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">(state, </span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">accountEmail</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">: scope.accountEmail, </span><span style=\"--shiki-light:#005CC5;--shiki-dark:#79B8FF\">mailbox</span><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">: scope.mailbox)</span></span>\n<span class=\"line\"><span style=\"--shiki-light:#24292E;--shiki-dark:#E1E4E8\">}</span></span></code></pre></figure><p>To find this in your own app, start with whatever runs on a timer and look at what it does when nothing has changed.</p>","created_at":"2026-11-12T14:00:00.000Z","updated_at":"2026-11-12T14:00:00.000Z"}