<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Anton’s Substack]]></title><description><![CDATA[My personal Substack (and that's true!) about iOS Development]]></description><link>https://antongubarenko.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!mzA2!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1eb672de-f6cd-47a2-98b2-6de3f5be6ffe_2316x3088.jpeg</url><title>Anton’s Substack</title><link>https://antongubarenko.substack.com</link></image><generator>Substack</generator><lastBuildDate>Tue, 02 Jun 2026 18:13:43 GMT</lastBuildDate><atom:link href="https://antongubarenko.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Anton Gubarenko]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[antongubarenko@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[antongubarenko@substack.com]]></itunes:email><itunes:name><![CDATA[Anton Gubarenko]]></itunes:name></itunes:owner><itunes:author><![CDATA[Anton Gubarenko]]></itunes:author><googleplay:owner><![CDATA[antongubarenko@substack.com]]></googleplay:owner><googleplay:email><![CDATA[antongubarenko@substack.com]]></googleplay:email><googleplay:author><![CDATA[Anton Gubarenko]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Swift Bits: Broken SPM Adding]]></title><description><![CDATA[Restore SPM adding feature]]></description><link>https://antongubarenko.substack.com/p/swift-bits-broken-spm-adding</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/swift-bits-broken-spm-adding</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Tue, 02 Jun 2026 08:53:12 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/8e25957b-ef50-4687-913a-5b009af0f007_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Last weekend, a QA session hit me hard. First of all, it was a weekend. Secondly, the <code>TextField</code> refused to behave the way I wanted with the keyboard &#8212; and more importantly, the way it was described in the specs.</p><p>The struggle between spending more time fixing it or rebuilding the whole view using plain UIKit kept growing. Before taking the final step into the controlled world of <code>UITextField</code>, there was one controversial option left: using introspection to access the underlying field from SwiftUI.</p><p>For that, I used one of my favorite libraries &#8212; <a href="https://github.com/siteline/swiftui-introspect">Introspect</a>. But that&#8217;s where the story behind this post begins. SPM decided to cause some trouble as well&#8230;</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Ium5!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69da5ca7-7417-44f7-9d2a-be26b308b59b_2161x1198.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Ium5!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69da5ca7-7417-44f7-9d2a-be26b308b59b_2161x1198.png 424w, https://substackcdn.com/image/fetch/$s_!Ium5!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69da5ca7-7417-44f7-9d2a-be26b308b59b_2161x1198.png 848w, https://substackcdn.com/image/fetch/$s_!Ium5!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69da5ca7-7417-44f7-9d2a-be26b308b59b_2161x1198.png 1272w, https://substackcdn.com/image/fetch/$s_!Ium5!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69da5ca7-7417-44f7-9d2a-be26b308b59b_2161x1198.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Ium5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69da5ca7-7417-44f7-9d2a-be26b308b59b_2161x1198.png" width="1456" height="807" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/69da5ca7-7417-44f7-9d2a-be26b308b59b_2161x1198.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:807,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:133509,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/200177454?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69da5ca7-7417-44f7-9d2a-be26b308b59b_2161x1198.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Ium5!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69da5ca7-7417-44f7-9d2a-be26b308b59b_2161x1198.png 424w, https://substackcdn.com/image/fetch/$s_!Ium5!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69da5ca7-7417-44f7-9d2a-be26b308b59b_2161x1198.png 848w, https://substackcdn.com/image/fetch/$s_!Ium5!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69da5ca7-7417-44f7-9d2a-be26b308b59b_2161x1198.png 1272w, https://substackcdn.com/image/fetch/$s_!Ium5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F69da5ca7-7417-44f7-9d2a-be26b308b59b_2161x1198.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Infinite loader</figcaption></figure></div><p>Swift Package Manager itself may be fine. Your package URL may be fine. Your repo may even clone normally in Terminal. And still, Xcode&#8217;s <strong>Add Package Dependency</strong> sheet can sit there forever, spinning without validating anything.</p><p>That is what makes this bug so annoying: it looks like a networking or SPM problem, but the workaround that helps most often points somewhere else entirely &#8212; Xcode&#8217;s own remembered package-entry state.</p><div><hr></div><h2>The Symptom</h2><p>The original report is very specific: Xcode&#8217;s Swift Package sheet loads indefinitely whether a URL is entered or not, and clearing DerivedData plus SwiftPM caches does not help. The issue also affects multiple projects, not just one workspace.</p><p>That combination is a useful clue.</p><p>If the problem survives:</p><ul><li><p>multiple projects</p></li><li><p>empty or filled package URLs</p></li><li><p>DerivedData cleanup</p></li><li><p>SwiftPM cache cleanup</p></li></ul><p>then the bug is probably not inside your package manifest or repository alone. It starts looking more like Xcode UI state or package-assistant state.</p><div><hr></div><h2>The First Workaround: Clear Package History</h2><ol><li><p>Open the stuck Add Package screen</p></li><li><p>Right Click on Recently Used</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!dZrQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F714dae08-cfff-4e82-8465-bbc694aa880b_1206x270.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!dZrQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F714dae08-cfff-4e82-8465-bbc694aa880b_1206x270.png 424w, https://substackcdn.com/image/fetch/$s_!dZrQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F714dae08-cfff-4e82-8465-bbc694aa880b_1206x270.png 848w, https://substackcdn.com/image/fetch/$s_!dZrQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F714dae08-cfff-4e82-8465-bbc694aa880b_1206x270.png 1272w, https://substackcdn.com/image/fetch/$s_!dZrQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F714dae08-cfff-4e82-8465-bbc694aa880b_1206x270.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!dZrQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F714dae08-cfff-4e82-8465-bbc694aa880b_1206x270.png" width="592" height="132.53731343283582" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/714dae08-cfff-4e82-8465-bbc694aa880b_1206x270.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:270,&quot;width&quot;:1206,&quot;resizeWidth&quot;:592,&quot;bytes&quot;:115319,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/200177454?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F714dae08-cfff-4e82-8465-bbc694aa880b_1206x270.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!dZrQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F714dae08-cfff-4e82-8465-bbc694aa880b_1206x270.png 424w, https://substackcdn.com/image/fetch/$s_!dZrQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F714dae08-cfff-4e82-8465-bbc694aa880b_1206x270.png 848w, https://substackcdn.com/image/fetch/$s_!dZrQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F714dae08-cfff-4e82-8465-bbc694aa880b_1206x270.png 1272w, https://substackcdn.com/image/fetch/$s_!dZrQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F714dae08-cfff-4e82-8465-bbc694aa880b_1206x270.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div></li><li><p>Clear Recently Used&#8230;</p></li><li><p>Try adding the package again</p></li></ol><p>That is already pretty revealing. If clearing history fixes the infinite loader, the issue is not just &#8220;SPM cannot resolve packages.&#8221; It suggests the Add Package assistant may be choking on recently used package metadata or state associated with that UI.</p><p>This is also why the bug feels so strange: the visible failure is &#8220;loading forever,&#8221; but the successful fix is effectively &#8220;wipe the remembered package list.&#8221;</p><div><hr></div><h2>The 2nd Workaround: Delete the Preference Key Manually</h2><p>Here is a more direct workaround:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;7c23c66a-4d12-42f7-8295-5d3ebe0ef600&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">plutil -remove IDESwiftPackageAdditionAssistantRecentlyUsedPackages ~/Library/Preferences/com.apple.dt.Xcode.plist
</code></pre></div><p>I recommend to fully quit the Xcode first, running the command in Terminal, then reopening Xcode and trying again.</p><p>The key name is fairly descriptive on its own. It strongly suggests that Xcode stores recently used package references for the package-addition assistant in its preferences plist. Since removing that key can restore the Add Package UI, a reasonable inference is that corrupted or problematic remembered package state can block the sheet before actual validation completes. That is an inference from the key name plus the observed workaround, not an official Apple explanation.</p><div><hr></div><h2>Why Deleting DerivedData Often Does Nothing</h2><p>This is the important distinction.</p><p>DerivedData and SwiftPM caches are usually where developers look first, and for good reason. They help with build artifacts, resolved package state, and repository caches. But, removing both <code>~/Library/Developer/Xcode/DerivedData</code> and <code>~/Library/Caches/org.swift.swiftpm/</code> did not fix the problem.</p><p>That lines up with the workaround above. If the issue is in the <strong>Add Package assistant&#8217;s preferences state</strong>, clearing build caches is simply attacking the wrong layer.</p><p>In other words:</p><ul><li><p><strong>DerivedData</strong> fixes build and indexing weirdness</p></li><li><p><strong>SwiftPM caches</strong> fix package cache weirdness</p></li><li><p><strong>this bug</strong> may live in Xcode&#8217;s package-addition UI memory instead</p></li></ul><p>That separation is the whole story.</p><div><hr></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Swift Bits: What is ADR?]]></title><description><![CDATA[Architecture Decision Record explained]]></description><link>https://antongubarenko.substack.com/p/swift-bits-what-is-adr</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/swift-bits-what-is-adr</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Mon, 01 Jun 2026 08:40:00 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/63805cb4-a8e5-4d63-8ee7-6fb3f1901f24_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>There&#8217;s no clear border after which you can say: &#8220;Ohhh, now I know everything.&#8221; After that, you could switch industries &#8212; but I&#8217;m pretty sure farming also keeps getting new harvesters and growth techniques. Companies always need to sell something.</p><p>In IT, you collect new topics and terms throughout your entire career. For me, last month&#8217;s discovery was ADR.</p><p>I was reading the wonderful book &#8220;<a href="https://www.packtpub.com/en-sg/product/ai-driven-swift-architecture-9781835886540/chapter/ai-powered-code-architecture-and-legacy-system-modernization-6/section/dependency-injection-as-an-architectural-principle-ch06lvl1sec48?srsltid=AfmBOopvDgSNfRwTFcUMY_8vTxxI2WGw3mxT8ZTj6ZtTjoQRi_vCWOXq">AI Driven Swift Architecture</a>&#8221; by Walid Sassi and Dave Poirier &#8212; highly recommended. You can find it on Amazon or directly on the Packt website.</p><p>One chapter explained how to make LLM output more deterministic, and one of the approaches mentioned there was ADR. Sharing the details with you.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!RXSZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2c12576-f32c-47d9-a79f-ad41ec1976ba_1200x1904.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!RXSZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2c12576-f32c-47d9-a79f-ad41ec1976ba_1200x1904.png 424w, https://substackcdn.com/image/fetch/$s_!RXSZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2c12576-f32c-47d9-a79f-ad41ec1976ba_1200x1904.png 848w, https://substackcdn.com/image/fetch/$s_!RXSZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2c12576-f32c-47d9-a79f-ad41ec1976ba_1200x1904.png 1272w, https://substackcdn.com/image/fetch/$s_!RXSZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2c12576-f32c-47d9-a79f-ad41ec1976ba_1200x1904.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!RXSZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2c12576-f32c-47d9-a79f-ad41ec1976ba_1200x1904.png" width="1200" height="1904" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b2c12576-f32c-47d9-a79f-ad41ec1976ba_1200x1904.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1904,&quot;width&quot;:1200,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:188653,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/200090383?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2c12576-f32c-47d9-a79f-ad41ec1976ba_1200x1904.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!RXSZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2c12576-f32c-47d9-a79f-ad41ec1976ba_1200x1904.png 424w, https://substackcdn.com/image/fetch/$s_!RXSZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2c12576-f32c-47d9-a79f-ad41ec1976ba_1200x1904.png 848w, https://substackcdn.com/image/fetch/$s_!RXSZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2c12576-f32c-47d9-a79f-ad41ec1976ba_1200x1904.png 1272w, https://substackcdn.com/image/fetch/$s_!RXSZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb2c12576-f32c-47d9-a79f-ad41ec1976ba_1200x1904.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Useful links:</p><ul><li><p><a href="https://adr.github.io">What is ADR?</a></p></li><li><p><a href="https://medium.com/@naveenatmakur/what-is-adr-do-i-need-it-how-is-it-useful-b5f9802db56f">What is Architecture Decision Record (ADR)? Do I need it?</a></p></li><li><p><a href="https://github.com/architecture-decision-record/architecture-decision-record">ADR Examples</a><a href="https://developer.apple.com/documentation/swiftui/environmentvalues/displayscale"><br></a></p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[Swift Bits: MCP Advantages]]></title><description><![CDATA[Get your AI agent new pair of tools]]></description><link>https://antongubarenko.substack.com/p/swift-bits-mcp-advantages</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/swift-bits-mcp-advantages</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Mon, 25 May 2026 08:13:50 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/6005b2ac-6ac7-4fac-98e2-0c608f21e35e_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Many developers struggle to get stable results from AI tool interactions. This can even build a strong opinion that such tools are unreliable: &#8220;We don&#8217;t need them. I&#8217;m used to doing everything on my own.&#8221; Fair enough.</p><p>But just like Xcode, the Simulator, or any other development tool/service, AI tools need to be tuned and adapted. Even cars have seat, steering wheel, and mirror adjustments.</p><p>Pasting a Figma screenshot or exported image is not the same as letting Claude (for example) traverse the structure itself, inspect groups, understand paddings, and consume tokens for actual context. It&#8217;s like drawing from a photo versus drawing from a live object. Yes, AI can estimate corner radii or pick colors from an image. But fonts, spacing, hierarchy? That gets complicated.</p><p>And I&#8217;m only talking about Figma right now. There&#8217;s also Notion, Slack, Google Docs, Linear&#8230; the list is huge.</p><p>Give your CLI a pair of glasses &#8212; or a few extra hands &#8212; to make its understanding more precise.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ediB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b74ad85-08e3-49c5-b899-42944c465c02_1202x1858.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ediB!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b74ad85-08e3-49c5-b899-42944c465c02_1202x1858.png 424w, https://substackcdn.com/image/fetch/$s_!ediB!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b74ad85-08e3-49c5-b899-42944c465c02_1202x1858.png 848w, https://substackcdn.com/image/fetch/$s_!ediB!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b74ad85-08e3-49c5-b899-42944c465c02_1202x1858.png 1272w, https://substackcdn.com/image/fetch/$s_!ediB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b74ad85-08e3-49c5-b899-42944c465c02_1202x1858.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ediB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b74ad85-08e3-49c5-b899-42944c465c02_1202x1858.png" width="1202" height="1858" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2b74ad85-08e3-49c5-b899-42944c465c02_1202x1858.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1858,&quot;width&quot;:1202,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:236504,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/199159746?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b74ad85-08e3-49c5-b899-42944c465c02_1202x1858.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ediB!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b74ad85-08e3-49c5-b899-42944c465c02_1202x1858.png 424w, https://substackcdn.com/image/fetch/$s_!ediB!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b74ad85-08e3-49c5-b899-42944c465c02_1202x1858.png 848w, https://substackcdn.com/image/fetch/$s_!ediB!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b74ad85-08e3-49c5-b899-42944c465c02_1202x1858.png 1272w, https://substackcdn.com/image/fetch/$s_!ediB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b74ad85-08e3-49c5-b899-42944c465c02_1202x1858.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>Useful links:</p><ul><li><p><a href="https://modelcontextprotocol.io/docs/getting-started/intro">What is MPC?</a></p></li><li><p><a href="https://code.claude.com/docs/en/mcp">Connect MCP in Claude</a></p></li><li><p><a href="https://help.figma.com/hc/en-us/articles/32132100833559-Guide-to-the-Figma-MCP-server">Connect Figma MCP</a></p></li><li><p><a href="https://developers.notion.com/guides/mcp/overview">Connect Notion MCP</a></p></li><li><p><a href="https://slack.com/intl/en-gb/help/articles/48855576908307-Guide-to-the-Slack-MCP-server">Connect Slack MCP</a><a href="https://developer.apple.com/documentation/swiftui/environmentvalues/displayscale"><br></a></p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[Swift Bits: Display Scale]]></title><description><![CDATA[Screen data the right way]]></description><link>https://antongubarenko.substack.com/p/swift-bits-display-scale</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/swift-bits-display-scale</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Tue, 12 May 2026 07:35:48 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!8p4w!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb281f335-68ed-4cea-a79d-c5209d40b8ad_1803x3564.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>While actively coding, it&#8217;s typical to face warnings and try to resolve them. It&#8217;s not just a fancy habit, but a way to reduce tech debt and extra compilation time. Who isn&#8217;t dreaming about zero(0, totally no) warnings during compilation? Honestly?!<br><br>Over the years, Apple has been shifting toward a multi-scene architecture. We saw the migration away from AppDelegate in SwiftUI, which finally led to using the right tools for screen info tracking. One of the most vital pieces of data is display scale. Have you ever seen an image with really low resolution quality on a 3x scale device? That&#8217;s exactly what happens when scale isn&#8217;t handled correctly.<br><br>Sharing new ways to get it in both UIKit and SwiftUI.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8p4w!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb281f335-68ed-4cea-a79d-c5209d40b8ad_1803x3564.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8p4w!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb281f335-68ed-4cea-a79d-c5209d40b8ad_1803x3564.png 424w, https://substackcdn.com/image/fetch/$s_!8p4w!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb281f335-68ed-4cea-a79d-c5209d40b8ad_1803x3564.png 848w, https://substackcdn.com/image/fetch/$s_!8p4w!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb281f335-68ed-4cea-a79d-c5209d40b8ad_1803x3564.png 1272w, https://substackcdn.com/image/fetch/$s_!8p4w!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb281f335-68ed-4cea-a79d-c5209d40b8ad_1803x3564.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8p4w!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb281f335-68ed-4cea-a79d-c5209d40b8ad_1803x3564.png" width="1456" height="2878" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b281f335-68ed-4cea-a79d-c5209d40b8ad_1803x3564.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:2878,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:263343,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/197319029?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb281f335-68ed-4cea-a79d-c5209d40b8ad_1803x3564.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!8p4w!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb281f335-68ed-4cea-a79d-c5209d40b8ad_1803x3564.png 424w, https://substackcdn.com/image/fetch/$s_!8p4w!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb281f335-68ed-4cea-a79d-c5209d40b8ad_1803x3564.png 848w, https://substackcdn.com/image/fetch/$s_!8p4w!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb281f335-68ed-4cea-a79d-c5209d40b8ad_1803x3564.png 1272w, https://substackcdn.com/image/fetch/$s_!8p4w!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb281f335-68ed-4cea-a79d-c5209d40b8ad_1803x3564.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Useful links:</p><ul><li><p><a href="https://developer.apple.com/documentation/uikit/uiscreen/main">UIScreen.main</a></p></li><li><p><a href="https://developer.apple.com/documentation/uikit/uitraitcollection/current">UITraitCollection.current</a></p></li><li><p><a href="https://developer.apple.com/documentation/uikit/uiwindowscene">UIWindowScene</a></p></li><li><p><a href="https://developer.apple.com/documentation/swiftui/environmentvalues/displayscale">SwiftUI: displayScale<br></a></p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[Swift Bits: Scene for Hosted SwiftUI View]]></title><description><![CDATA[Track Phases correctly]]></description><link>https://antongubarenko.substack.com/p/swift-bits-scene-for-hosted-swiftui</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/swift-bits-scene-for-hosted-swiftui</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Mon, 11 May 2026 06:45:28 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/42e66875-d7a4-4a92-af1c-b334e699da19_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!osWi!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F672a5094-d423-4d75-8bf4-c34e8dfbbb19_946x608.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!osWi!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F672a5094-d423-4d75-8bf4-c34e8dfbbb19_946x608.png 424w, https://substackcdn.com/image/fetch/$s_!osWi!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F672a5094-d423-4d75-8bf4-c34e8dfbbb19_946x608.png 848w, https://substackcdn.com/image/fetch/$s_!osWi!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F672a5094-d423-4d75-8bf4-c34e8dfbbb19_946x608.png 1272w, https://substackcdn.com/image/fetch/$s_!osWi!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F672a5094-d423-4d75-8bf4-c34e8dfbbb19_946x608.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!osWi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F672a5094-d423-4d75-8bf4-c34e8dfbbb19_946x608.png" width="946" height="608" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/672a5094-d423-4d75-8bf4-c34e8dfbbb19_946x608.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:608,&quot;width&quot;:946,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:784476,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/194161428?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F672a5094-d423-4d75-8bf4-c34e8dfbbb19_946x608.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!osWi!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F672a5094-d423-4d75-8bf4-c34e8dfbbb19_946x608.png 424w, https://substackcdn.com/image/fetch/$s_!osWi!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F672a5094-d423-4d75-8bf4-c34e8dfbbb19_946x608.png 848w, https://substackcdn.com/image/fetch/$s_!osWi!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F672a5094-d423-4d75-8bf4-c34e8dfbbb19_946x608.png 1272w, https://substackcdn.com/image/fetch/$s_!osWi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F672a5094-d423-4d75-8bf4-c34e8dfbbb19_946x608.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><code>scenePhase</code> is SwiftUI&#8217;s built-in way to observe scene lifecycle. Apple documents <code>ScenePhase</code> as the operational state of a scene, with values such as <code>.active</code>, <code>.inactive</code>, and <code>.background</code>. Apple also notes that the meaning of the <code>scenePhase</code> environment value depends on where you read it.</p><p>That detail matters more than it first appears. Especially, when you have set the architecture once and just adding View by View.</p><p>In a pure SwiftUI app lifecycle, <code>@Environment(\.scenePhase)</code> is the natural tool. But once a SwiftUI view is embedded inside a UIKit app through <code>UIHostingController</code>, the lifecycle owner is no longer fully SwiftUI-driven. In that setup, <code>scenePhase</code> can be unreliable or simply not behave the way you expect from a SwiftUI <code>App</code> + <code>Scene</code> structure. Apple&#8217;s migration guidance frames scene monitoring as part of moving to the SwiftUI life cycle, which is a strong hint that this API is most at home there.</p><blockquote><p>So the short version is: <code>scenePhase</code> is native to SwiftUI scenes. If your SwiftUI view is hosted inside UIKit, lifecycle tracking is usually more reliable through UIKit notifications.</p></blockquote><div><hr></div><h2>What is <code>scenePhase </code>in SwiftUI?</h2><p>In SwiftUI, <code>scenePhase</code> tells you whether the current scene is active, inactive, or in the background. Apple&#8217;s docs define <code>ScenePhase</code> around scene state, not generic app state. That distinction is important because scenes and apps are related, but not identical, especially on platforms and configurations that support multiple scenes.</p><p>A standard SwiftUI example looks like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;e3dff906-cbf5-4edb-aa27-37bc7900622e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">import SwiftUI

struct ContentView: View {
    @Environment(\.scenePhase) private var scenePhase

    var body: some View {
        Text("Hello")
            .onChange(of: scenePhase) { _, newPhase in
                switch newPhase {
                case .active:
                    print("active")
                case .inactive:
                    print("inactive")
                case .background:
                    print("background")
                @unknown default:
                    break
                }
            }
    }
}</code></pre></div><p>That works well when the app is using SwiftUI&#8217;s own lifecycle model.</p><div><hr></div><h2>Why It Can Fail Inside <code>UIHostingController?</code></h2><p>A hosted SwiftUI view still renders like SwiftUI, but it does not automatically mean it participates in the same scene lifecycle model as a SwiftUI app root.</p><p>That is the real trap. No jokes, from Star Wars.</p><p>If the app is UIKit-first, with lifecycle driven by <code>UIApplicationDelegate</code>, <code>UISceneDelegate</code>, and UIKit-managed windows, then the hosted SwiftUI subtree may not receive <code>scenePhase</code> updates in the same way a real SwiftUI scene does. Apple&#8217;s documentation does not say that every hosted SwiftUI view will always get full scene lifecycle semantics; instead, it consistently describes <code>scenePhase</code> in terms of scenes and the SwiftUI lifecycle.</p><p>That is why this bug feels confusing. The code compiles, the environment value exists, and the same view may behave differently when moved into a pure SwiftUI app.</p><div><hr></div><h2>What Actually Works Instead?</h2><p>For a UIKit-hosted SwiftUI screen, the practical answer is to observe UIKit lifecycle directly from the view.</p><p>There are two good approaches:</p><ul><li><p>app-wide notifications from <code>UIApplication</code></p></li><li><p>scene-level notifications from <code>UIScene</code></p></li></ul><p>Apple documents <code>UIApplication.didBecomeActiveNotification</code> as a notification posted when the app becomes active, and <code>UIApplication.willEnterForegroundNotification</code> as one posted shortly before the app leaves background on the way to active. Apple also documents <code>UIScene.didActivateNotification</code> and related scene notifications for scene lifecycle transitions.</p><blockquote><p>So <code>UIApplication</code> notifications are not the only solution. They are just the broadest one.</p></blockquote><h2>1.  App-wide lifecycle with <code>UIApplication </code>notifications</h2><p>If you only care whether the whole app became active or inactive, observing <code>UIApplication</code> notifications directly inside the SwiftUI view is often enough. Apple documents <code>didBecomeActiveNotification</code> and <code>willResignActiveNotification</code> specifically for those app state transitions.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;11639f2a-94cb-4187-9f33-5a6f401986d5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">import SwiftUI
import UIKit

struct HostedSwiftUIView: View {
    @State private var isAppActive = true

    var body: some View {
        //Strange way to change Text, but still
        Text(isAppActive ? "App Active" : "App Inactive")
            .onReceive(NotificationCenter.default.publisher(
                for: UIApplication.didBecomeActiveNotification
            )) { _ in
                isAppActive = true
                print("App became active")
            }
            .onReceive(NotificationCenter.default.publisher(
                for: UIApplication.willResignActiveNotification
            )) { _ in
                isAppActive = false
                print("App resigned active")
            }
    }
}</code></pre></div><h2>2.  Scene-level lifecycle with <code>UIScene </code>notifications</h2><p>If your app uses scenes and you want behavior closer to what <code>scenePhase</code> is meant to represent, <code>UIScene</code> notifications are a better match than <code>UIApplication</code> notifications.</p><p>Apple documents scene notifications including:</p><ul><li><p><code>UIScene.didActivateNotification</code></p></li><li><p><code>UIScene.willDeactivateNotification</code></p></li><li><p><code>UIScene.didEnterBackgroundNotification</code></p></li><li><p><code>UIScene.willEnterForegroundNotification</code></p></li></ul><p>That makes this pattern more scene-accurate:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;46351c9b-bce6-40ad-8c57-11d73e6ec067&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">import SwiftUI
import UIKit

struct HostedSwiftUIView: View {
    @State private var phase: ScenePhase = .active

    var body: some View {
        Text(String(describing: phase))
            .onReceive(NotificationCenter.default.publisher(
                for: UIScene.didActivateNotification
            )) { _ in
                phase = .active
                print("Scene active")
            }
            .onReceive(NotificationCenter.default.publisher(
                for: UIScene.willDeactivateNotification
            )) { _ in
                phase = .inactive
                print("Scene inactive")
            }
            .onReceive(NotificationCenter.default.publisher(
                for: UIScene.didEnterBackgroundNotification
            )) { _ in
                phase = .background
                print("Scene background")
            }
            .onReceive(NotificationCenter.default.publisher(
                for: UIScene.willEnterForegroundNotification
            )) { _ in
                phase = .inactive
                print("Scene foreground")
            }
    }
}</code></pre></div><p>For many UIKit-hosted SwiftUI screens, this is the closest practical replacement for <code>scenePhase</code>.</p><div><hr></div><h2>Is There Any Other Solution?</h2><p>There are a couple of alternatives, but they are not really better for this case.</p><p>One option is to inspect <code>windowScene.activationState</code> from UIKit and derive a current value manually. That can work for one-time reads, but by itself it does not provide observation. Another option is to move the feature into a real SwiftUI <code>App</code> / <code>Scene</code> lifecycle, where <code>scenePhase</code> is naturally supported. Apple&#8217;s migration docs make clear that lifecycle monitoring is part of the SwiftUI lifecycle model, so that route is valid, but it is usually a much larger architectural move than teams want for one hosted screen.</p><p>So for a UIKit-heavy app, the practical answer remains: bridge lifecycle from UIKit.</p><div><hr></div><h2>Recommended Rule of Thumb</h2><p>A clean rule is:</p><ul><li><p>Use <code>@Environment(\.scenePhase)</code> in a real SwiftUI <code>App</code> / <code>Scene</code></p></li><li><p>Use <code>UIApplication</code> notifications in hosted SwiftUI when app-wide state is enough</p></li><li><p>Use <code>UIScene</code> notifications in hosted SwiftUI when you need scene-level behavior</p></li></ul><p>That is more precise than saying only <code>UIApplication</code> notifications work.</p><p>They work, but they are not always the best fit.</p><div><hr></div><h2>Why This Matters?</h2><p>The bug is subtle because nothing looks obviously broken. A hosted SwiftUI view still feels like SwiftUI, so it is natural to expect <code>scenePhase</code> to behave the same everywhere.</p><p>But lifecycle ownership is one of those invisible boundaries that changes behavior dramatically. <code>scenePhase</code> belongs to SwiftUI scenes. <code>UIHostingController</code> belongs to UIKit hosting. When those two worlds meet, UIKit lifecycle APIs are often the more dependable source of truth.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>References</h2><ul><li><p><a href="https://developer.apple.com/documentation/SwiftUI/ScenePhase?utm_source=chatgpt.com">Apple Developer Documentation: </a><code>ScenePhase</code><a href="https://developer.apple.com/documentation/SwiftUI/ScenePhase?utm_source=chatgpt.com">.</a></p></li><li><p><a href="https://developer.apple.com/documentation/swiftui/environmentvalues/scenephase">Apple Developer Documentation: </a><code>EnvironmentValues.scenePhase</code><a href="https://developer.apple.com/documentation/swiftui/environmentvalues/scenephase">.</a></p></li><li><p><a href="https://developer.apple.com/documentation/swiftui/migrating-to-the-swiftui-life-cycle?utm_source=chatgpt.com">Apple Developer Documentation: Migrating to the SwiftUI life cycle.</a></p></li><li><p><a href="https://developer.apple.com/documentation/uikit/uiapplication/didbecomeactivenotification?utm_source=chatgpt.com">Apple Developer Documentation: </a><code>UIApplication.didBecomeActiveNotification</code><a href="https://developer.apple.com/documentation/uikit/uiapplication/didbecomeactivenotification?utm_source=chatgpt.com">.</a></p></li><li><p><a href="https://developer.apple.com/documentation/uikit/uiapplication/willresignactivenotification">Apple Developer Documentation: </a><code>UIApplication.willResignActiveNotification</code><a href="https://developer.apple.com/documentation/uikit/uiapplication/willresignactivenotification">.</a></p></li><li><p><a href="https://developer.apple.com/documentation/uikit/uiscene/didactivatenotification?utm_source=chatgpt.com">Apple Developer Documentation: </a><code>UIScene.didActivateNotification</code><a href="https://developer.apple.com/documentation/uikit/uiscene/didactivatenotification?utm_source=chatgpt.com">.</a></p></li><li><p><a href="https://developer.apple.com/documentation/uikit/uiscene/willdeactivatenotification?utm_source=chatgpt.com">Apple Developer Documentation: </a><code>UIScene.willDeactivateNotification</code><a href="https://developer.apple.com/documentation/uikit/uiscene/willdeactivatenotification?utm_source=chatgpt.com">.</a></p></li><li><p><a href="https://developer.apple.com/documentation/uikit/uiscene/didenterbackgroundnotification">Apple Developer Documentation: </a><code>UIScene.didEnterBackgroundNotification</code><a href="https://developer.apple.com/documentation/uikit/uiscene/didenterbackgroundnotification">.</a></p></li><li><p><a href="https://developer.apple.com/documentation/uikit/uiscene/willenterforegroundnotification">Apple Developer Documentation: </a><code>UIScene.willEnterForegroundNotification</code><a href="https://developer.apple.com/documentation/uikit/uiscene/willenterforegroundnotification">.</a></p></li></ul>]]></content:encoded></item><item><title><![CDATA[Swift Bits: XCTest Migration to Swift Testing]]></title><description><![CDATA[XCTest -> Swift Testing]]></description><link>https://antongubarenko.substack.com/p/swift-bits-xctest-migration-to-swift</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/swift-bits-xctest-migration-to-swift</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Fri, 08 May 2026 05:08:01 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/b552dc87-8b3a-40ff-a642-5a66b178e8c5_379x386.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I decided to store Swift Bits notes on Substack as well. Searching on LinkedIn is pretty difficult because of the focus on the feed and the huge amount of content there. Plus, on Substack I can share extra links and references &#128578;</p><div><hr></div><p>Examining, checking, validating, and testing are essential parts of a developer&#8217;s life. Whether we&#8217;re verifying business logic, complex client-side computations, or trying to isolate a bug &#8212; tests are our helpers.<br><br>I&#8217;m not just talking about unit or UI tests. When we check an input and know the expected output, isn&#8217;t that a test? Maybe not as formal as Red&#8211;Green&#8211;Refactor or TDD (yes, I&#8217;m reading the book everyone&#8217;s been talking about lately), but it can still be seen as part of that approach.<br><br>For those who still use XCTest (like me &#128517;), as part of Swift Bits, I&#8217;m sharing some migration tips with you.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!yHh1!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f3f417-c1c0-4b3f-9ce9-bf610f335ad4_1352x1986.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!yHh1!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f3f417-c1c0-4b3f-9ce9-bf610f335ad4_1352x1986.png 424w, https://substackcdn.com/image/fetch/$s_!yHh1!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f3f417-c1c0-4b3f-9ce9-bf610f335ad4_1352x1986.png 848w, https://substackcdn.com/image/fetch/$s_!yHh1!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f3f417-c1c0-4b3f-9ce9-bf610f335ad4_1352x1986.png 1272w, https://substackcdn.com/image/fetch/$s_!yHh1!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f3f417-c1c0-4b3f-9ce9-bf610f335ad4_1352x1986.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!yHh1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f3f417-c1c0-4b3f-9ce9-bf610f335ad4_1352x1986.png" width="1352" height="1986" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c9f3f417-c1c0-4b3f-9ce9-bf610f335ad4_1352x1986.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1986,&quot;width&quot;:1352,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:492121,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/196835204?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f3f417-c1c0-4b3f-9ce9-bf610f335ad4_1352x1986.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!yHh1!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f3f417-c1c0-4b3f-9ce9-bf610f335ad4_1352x1986.png 424w, https://substackcdn.com/image/fetch/$s_!yHh1!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f3f417-c1c0-4b3f-9ce9-bf610f335ad4_1352x1986.png 848w, https://substackcdn.com/image/fetch/$s_!yHh1!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f3f417-c1c0-4b3f-9ce9-bf610f335ad4_1352x1986.png 1272w, https://substackcdn.com/image/fetch/$s_!yHh1!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc9f3f417-c1c0-4b3f-9ce9-bf610f335ad4_1352x1986.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Useful links:</p><ul><li><p><a href="https://developer.apple.com/xcode/swift-testing/">Apple Swift Testing Notes</a></p></li><li><p><a href="https://developer.apple.com/documentation/testing">Apple Developer: Swift Testing</a></p></li><li><p><a href="https://developer.apple.com/videos/play/wwdc2024/10179/">WWDC24: Meet Swift Testing</a></p></li><li><p><a href="https://developer.apple.com/documentation/testing/migratingfromxctest">Apple Guide: Migration from XCTest</a></p></li><li><p><a href="https://useyourloaf.com/blog/migrating-xctest-to-swift-testing/">UseYourLoaf: Migration to Swift Testing</a></p></li><li><p><a href="https://www.avanderlee.com/swift-testing/modern-unit-test/">Swift Testing: Writing a Modern Unit Test</a><a href="https://developer.apple.com/videos/play/wwdc2024/10179/"><br></a></p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[Q&A: Swift Concurrency - Formatted]]></title><description><![CDATA[Direct live answers from Apple Developers Team]]></description><link>https://antongubarenko.substack.com/p/q-and-a-swift-concurrency-formatted</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/q-and-a-swift-concurrency-formatted</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Tue, 28 Apr 2026 05:42:06 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/4d52ce32-9dc8-4338-8b84-f5e850c4fffd_1254x1254.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Swift Concurrency is a topic that doesn&#8217;t just appear occasionally &#8212; it&#8217;s more like a quiet whisper you hear all the time. Articles keep coming out, guides are recorded, assumptions get challenged&#8230; This has led to a <a href="https://www.youtube.com/watch?v=E95agtPgaa0&amp;t=3468s">new format</a> (or rather, an evolution of Apple&#8217;s format, if you get it), where you can ask questions live and get answers directly from Apple engineers across Foundation, SwiftUI, and other teams. So our beloved Q&amp;A section from Code-along sessions has now moved into a separate session.</p><p>The flow looks like this:</p><ul><li><p>Log in with your Apple Developer Forum account</p></li><li><p>Submit a question for moderation (yes, moderation again!)</p></li><li><p>Pass moderation</p></li><li><p>Get upvotes to increase your chances of being answered</p></li></ul><p>I took a chance and asked about task cancellation best practices &#8212; a topic many others were also interested in.</p><p>And, as usual, all questions were later removed. Thankfully, the answers were captured during the live session. To help you, I prepared a transcript for those who prefer not to watch, and added timestamps for those who prefer not to read &#128578;</p><p>Enjoy!</p><div><hr></div><h2>Is </h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;b498ba52-9b2e-4194-a684-1a3e39c4fcd5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Task { @MainActor in
   ...
 } </code></pre></div><h2>preferred over </h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;7b59c82f-d5e8-40a7-ac13-a97919770507&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Task { 
   // code to run in background
   await MainActor.run { 
     ...
   }
}</code></pre></div><p>The difference between those two approaches is that Task with <code>MainActor</code> runs the entire task body on the <code>MainActor</code>. Of course, some operations inside that Task might be isolated to something else, and the task can switch between different isolation domains depending on what the code calls. But, for example, if you have synchronous code inside the Task and you want all of it to happen on the <code>MainActor</code>, then I would write the <code>MainActor</code> annotation directly on the Task closure signature. If you only have a specific operation, maybe just a couple of lines of code inside the Task, that you want to run on the <code>MainActor</code>, but everything else should continue running in the same context where the Task was created, then that is a case where you can use <code>MainActor.run</code>. Another option is to factor that code out into a <code>MainActor</code>-isolated function and call that function from within the task.</p><p><a href="https://youtu.be/E95agtPgaa0?t=478">&#9201;&#65039;Time code</a></p><div><hr></div><h2>What&#8217;s the difference between <code>nonisolated</code> and <code>nonisolated(nonsending)</code>, and why was the latter introduced?</h2><p>Nonisolated means that a declaration has no preference for isolation. It does not have a fixed isolation, so it can be used in multiple isolation contexts. Nonisolated non-sending is about asynchronous functions. There was a behavior originally implemented for asynchronous functions in Swift concurrency where asynchronous functions would move to the concurrent thread pool, the shared executor, by default. We found that this was not the right trade-off as people adopted Swift concurrency and we learned more. So nonisolated non-sending, which is now the default behavior if you opt in using the approachable concurrency settings, is the new behavior where your asynchronous task or asynchronous function will not leave the isolation it was called from right away, unless there is an explicit reason to.</p><p><a href="https://youtu.be/E95agtPgaa0?t=563">&#9201;&#65039;Time code</a></p><div><hr></div><h2>When marking a ViewModel with <code>@MainActor</code> how should we handle a method inside it that performs heavy I/O processing? How do process outside of MainActor/Main Thread?</h2><p>Yes, if you have a particular operation on any MainActor-isolated type and you want only that operation to be offloaded, you can annotate just that method with a different isolation. For example, if you want to offload it to the concurrent thread pool, you can now use the <code>@concurrent</code> attribute. That is the right explicit spelling for it. The compiler will prevent you from directly accessing any MainActor mutable state on that view model, or whatever your MainActor-isolated type is, to make sure you are not accessing that state at the same time as another operation on that type is accessing the same mutable state from the MainActor. So take a look at @concurrent. There are also a lot of references from last year&#8217;s WWDC, where many uses of <code>@concurrent</code> were covered as well.</p><p><a href="https://youtu.be/E95agtPgaa0?t=643">&#9201;&#65039;Time code</a></p><div><hr></div><h2>When a <code>Task</code> is created from <code>@MainActor</code>, will that block the main thread and cause UI hitches?</h2><p>A task being created from <code>MainActor</code> is something you probably encounter often when working on a UI framework. I think that when a task is created on <code>MainActor</code>, there is still a suspension point that the compiler creates, so while that work is being performed, the main thread can still continue working on UI updates and avoid causing hitches. In general, the task machinery tries to schedule your work on the concurrent thread pool so that it does not block excessively. That is one of the nice behaviors that tasks provide. I think it depends on what the task does. If you create a task that is isolated to <code>MainActor</code> and that task starts running on <code>MainActor</code>, and you are doing some very expensive work that takes a long time, then it might block <code>MainActor</code>, and you might notice that. In that case, you may want to profile that code in Instruments and then decide whether to insert some suspension points or offload parts of that expensive task from <code>MainActor</code>. I really recommend that you do not worry about proactively offloading everything you can from <code>MainActor</code>. There are some things that are totally fine to run on <code>MainActor</code>. But if you notice hangs in your app and identify that piece of code through Instruments or your profiling tool of choice, that is when you should make targeted changes to move the expensive work off.</p><p><a href="https://youtu.be/E95agtPgaa0?t=720">&#9201;&#65039;Time code</a></p><div><hr></div><h2>What is the best practice to cancel a task?</h2><p>I think the practice I have been using is storing a task on the type and then calling cancel on it when needed. That is a good approach if you are trying to granularly manage the lifetime of that task. Any task that you want to be able to cancel needs to be tracked in some way, so you need to hold on to the handle.</p><p><a href="https://youtu.be/E95agtPgaa0?t=849">&#9201;&#65039;Time code</a></p><div><hr></div><h2>When should I reach for at <code>concurrent</code> instead of <code>nonisolated</code> async? Is there a one sentence rule?</h2><p><code>@concurrent</code> is guaranteed to run on the shared thread pool in the background. For <code>nonisolated async</code>, it depends on your upcoming feature settings. As described earlier, the one-sentence explanation is that you should use <code>@concurrent</code> when you want your async function to be offloaded to the concurrent thread pool. It is an explicit spelling for behavior that used to be implicit for async functions, but that behavior is changing because it was not the best trade-off and not the best default for async functions. So you should use <code>@concurrent</code> now when you want that behavior. With <code>nonisolated async</code>, it depends on whether you have the approachable concurrency upcoming features enabled in your Xcode project, which is the default for new Xcode projects. If you do not have it enabled, I recommend enabling those settings because it gives you a set of defaults that are a bit easier to use with the data safety diagnostics.</p><p><a href="https://youtu.be/E95agtPgaa0?t=900">&#9201;&#65039;Time code</a></p><div><hr></div><h2>Is it possible to have too many actors? Is there an intrinsic limit? </h2><p>There is probably an intrinsic limit depending on how much memory you have, but I would definitely think about limiting how many different actors you have in your program. Generally speaking, you want to have just a few isolation domains in a program to reason about. Concurrent programming is hard, as we said earlier, and most of your code should be synchronous unless you have a reason to introduce concurrency and a reason to introduce different isolation domains. Every actor is a different isolation domain, and if actors need to work together, that creates complexity in your program. So you really want to think carefully when you factor something out into an actor, and decide whether it is really appropriate to add the complexity of needing to access that code asynchronously from other parts of your program. I think a very common question is: when do you actually use an actor? The answer is specifically when you can define that something will always need its own isolated domain, and the entirety of its functionality requires you to maintain that isolation. On the slide, I had a couple of examples like network caching or accessing a database. Those are common things where you often already know the work is going to be in an isolated domain anyway.</p><p><a href="https://youtu.be/E95agtPgaa0?t=997">&#9201;&#65039;Time code</a></p><div><hr></div><h2>Could a Task started by <code>.task</code> modifier in SwiftUI view be cancelled if the view is refreshed by state change?</h2><p>The <code>task</code> modifier in SwiftUI cancels your task when the view disappears, and state changes usually do not cause your view to be fully destroyed by SwiftUI. State works in a way where, when the state changes, SwiftUI reevaluates the view&#8217;s body, but it does not actually destroy the view. One case where you may see this behavior is if you have an <code>if</code>statement in your view&#8217;s body, and one branch becomes true while the other branch previously contained a view with a task. Once the condition flips, SwiftUI would cancel the task because the view in the other branch of the <code>if</code> statement is destroyed. You can also think about it like <code>onDisappear</code>: <code>task</code> is a modifier, and the <code>task</code> modifier manages cancellation with the same timeline as the <code>onDisappear</code> modifier, if you have used that before.</p><p><a href="https://youtu.be/E95agtPgaa0?t=1092">&#9201;&#65039;Time code</a></p><div><hr></div><h2>Intuitively what does the nonisolated keyword mean? Especially after the recent changes.</h2><p>What has not really changed necessarily, but is now defaulted in some cases, is that <code>nonisolated</code> is a statement of your intent to not require any particular isolation for a type or function. When you annotate a type as <code>@MainActor</code>, you are stating that you only want it to be used from <code>MainActor</code>. <code>nonisolated</code> is removing that restriction. You can use it from any isolation domain, and that is really flexible and powerful. It is often the right choice for code in libraries, where you do not know where the app will want to use your library types. It can also be appropriate in your app for low-level models that do not have any inherent need to be isolated to <code>MainActor</code> or some other isolation domain. That is part of what motivated the behavior change for <code>nonisolated</code> with async functions. When something is marked as <code>nonisolated</code>, you can use it from anywhere. If you have a method that is not isolated and you call it from somewhere, it is going to run in the isolation domain it was called from. That used to not be the behavior for async functions, but now it is. So now there is a consistent meaning: when you see <code>nonisolated</code>, it means you can use it from anywhere, and when you call those methods, they are going to run wherever you call them from.</p><p><a href="https://youtu.be/E95agtPgaa0?t=1183">&#9201;&#65039;Time code</a></p><div><hr></div><h2>Are there plans to introduce tasks with timeouts (like GCD group timeout) or something to run after certain period of time (like <code>DispatchQueue.main.asyncAfter</code>)?</h2><p>There is actually a proposal under review right now that adds this exact API to <code>Task</code>. I think the naming is still under debate. Right now, it is called <code>withDeadline</code>, and it is meant to provide exactly that behavior: you run a task, and if it exceeds a certain amount of time, the task will be canceled. That behavior has also been getting a lot of feedback during review. If you are interested in checking it out, you can head over to the Swift Forums and look at the <a href="https://forums.swift.org/t/se-0526-withdeadline/85850">Swift Evolution proposal</a>. If you have any feedback about the proposed behavior, you can comment on the review thread there. Whoever asked that question should definitely go to the forums, ask questions, or provide feedback.</p><p><a href="https://youtu.be/E95agtPgaa0?t=1284">&#9201;&#65039;Time code</a></p><div><hr></div><h2>For a new app target, should I adopt MainActor default isolation? Does your answer change for libraries?</h2><p>In Xcode 26, all new app targets use <code>MainActor</code> isolation by default. As someone from the SwiftUI team, I think that is an absolutely amazing choice for app targets because, in our UI framework, views are already on <code>MainActor</code>. If you have written a SwiftUI view and also have a model type, you may have noticed that it is often convenient to put that model on <code>MainActor</code> as well. With <code>MainActor</code> by default turned on, you do not have to do that manually. It makes UI code much easier to write and reason about, and you do not have to think as much about synchronizing your state because everything is on <code>MainActor</code>. But when it comes to libraries, my answer would change, depending on what the library does. If you are trying to be performant, concurrency can help you optimize behavior in your code by parallelizing operations, and libraries can make use of that in some cases. If you find that an operation is taking too long and you want to split it up, then it can make sense to introduce more concurrency, especially in library code. The same applies to app targets as well: you start on <code>MainActor</code>, and once you see potential for a performance optimization, or you profile the app and see the main thread being blocked by an expensive operation in your model, then you can consider adding concurrency annotations and moving things off <code>MainActor</code>. I also think many general-purpose library APIs are not specific to any particular isolation domain. Most of Foundation is not isolated, and clients of Foundation can choose where they want to use those APIs. For example, with Foundation, most APIs are not specific to any isolation, so most of them are <code>nonisolated</code>. There are a few APIs that are specific, such as <code>UndoManager</code>, which is heavily UI-focused and annotated with <code>MainActor</code>. For Foundation, it makes sense to keep most things <code>nonisolated</code> by default and add <code>MainActor</code> isolation for UI-related APIs. But when you are working on an app, it is almost the inverse: most of your things are on <code>MainActor</code>, and you may want to selectively offload work from there.</p><p><a href="https://youtu.be/E95agtPgaa0?t=1344">&#9201;&#65039;Time code</a></p><div><hr></div><h2>If a class launches a long-running Task and calls <code>task.cancel()</code> in <code>deinit</code>, does the task actually stop executing? And is its memory released immediately after cancellation? Also, are tasks automatically cancelled when their owning scope is deallocated?</h2><p>Cancellation in Swift concurrency is cooperative. Depending on what the task is doing, that operation needs to check for cancellation. Usually, for a library method or similar API to support cancellation, the course of action is to throw a cancellation error. If you are running long synchronous code and someone cancels the task handle, that synchronous code is not going to be interrupted at any moment. That is actually an important behavior, because the work might need to perform some cleanup operation that must happen atomically without being interrupted by something like task cancellation. So no, the task does not necessarily stop running immediately because of cancellation. But as soon as you hit a piece of code that handles cancellation and maybe throws a cancellation error, that is when it will usually return by throwing such an error. There was another question about whether tasks get cancelled when their owning scope is deallocated. If a class has a task handle, like we discussed earlier, and you want to track a task to cancel it, you need to store it as a property. But no, it is not automatically cancelled just because the owning scope is deallocated. That is one of the gotchas when you opt into managing your task&#8217;s lifetime: you always have to remember to cancel it. That is also good in the sense that you are being explicit about the task&#8217;s lifetime. There was also a question about tasks swallowing errors. A <code>Task</code> does not require handling of thrown errors, which can lead to silently ignored failures. There is now an accepted proposal that addresses that exact pain point. In Swift 6.4, there will be a new diagnostic for exactly that case, where you have a task and have not handled errors within it. There are different ways to handle errors within a task. You can handle any thrown errors inside the task body, or if the task itself can throw and you want to keep a handle to the task, you can wait until you get the task&#8217;s value and handle any potential errors that were thrown. Those are two different ways to handle task errors. So yes, with the acceptance of that proposal, you will now get diagnostics for this. If you are interested, you can check the Swift Forums or Swift.org Swift Evolution for the accepted proposed behavior.</p><p><a href="https://youtu.be/E95agtPgaa0?t=1524">&#9201;&#65039;Time code</a></p><div><hr></div><h2>What&#8217;s the best practice for asynchronous requests in response to a button tap? Kicking off a one-off task? Exposing a synchronous method from your model that kicks off a task internally?</h2><p>The general recommendation, although you can structure your code in both ways, is that I would prefer moving that asynchronous code to your model. Then the model can respond back with synchronous output to your view, so the view stays responsive and the model handles all of that asynchronous work. The benefit of this approach is that you can also test the asynchronous work outside of your view, which brings greater benefits to your entire codebase.</p><p><a href="https://youtu.be/E95agtPgaa0?t=1737">&#9201;&#65039;Time code</a></p><div><hr></div><h2>How do you handle legacy NSObject types which are non-scendable to work with at sendable closures?</h2><p>I do not have a specific recommendation for <code>NSObject</code> types in general. It depends a lot on the situation in your code. If you have a class that inherits from <code>NSObject</code>, and your class does not add any state, and neither does <code>NSObject</code> itself, then it is possible that the type does not have any state. If it does not have any state, or it protects its mutable state, then you can mark it as <code>@unchecked Sendable</code> because you are managing that state yourself. If you really do have mutable state in that class, I do not recommend capturing it inside a <code>@Sendable</code> closure. With region-based isolation, which was introduced with the Swift 6 compiler, there are other tools you can use when you need to pass a closure over an isolation domain, but you do not necessarily need to call that closure multiple times, potentially in parallel. You can use the <code>sending</code> modifier, which allows you to pass a closure to another isolation domain. This is what the <code>Task</code> initializer and task creation methods use, because that closure goes to a different isolation domain depending on where you want to run the task, but it is only called once and only sent over an isolation domain one time. That allows you to capture non-<code>Sendable</code> state, like classes with mutable state or classes that inherit from <code>NSObject</code>, as long as you do not use them again from the original isolation domain where you formed the closure. Depending on the pattern of your code, there may be different solutions. There is not one general piece of advice for all classes that inherit from <code>NSObject</code>, but there are a variety of tools you can use depending on the exact concurrency guarantees of your code. Another tool we have used in Foundation is to avoid sharing the entire <code>NSObject</code> when you only need pieces of it. Often, an <code>NSObject</code> may have properties like a string or integer, which are <code>Sendable</code>types. So while you may not be able to share the whole <code>NSObject</code> between isolation domains, you can pull out the pieces that you need to share, as long as you do not need to worry about mutations. That has helped us avoid making everything <code>Sendable</code>. Instead, we can pick out the pieces that really need to be shared, make those <code>Sendable</code>, and let the legacy code that encapsulates everything remain non-<code>Sendable</code>. Also, when dealing with <code>@Sendable</code> closures and captured state, it is often helpful to use the closure&#8217;s capture list to capture only the <code>Sendable</code> pieces of your state, instead of trying to send over the entire containing type, which may not be <code>Sendable</code>. Hopefully, that provides some insight, but it is also worth checking the documentation and Swift Forums, where there are often many different tactics and suggestions discussed.</p><p><a href="https://youtu.be/E95agtPgaa0?t=1793">&#9201;&#65039;Time code</a></p><div><hr></div><h2>What is the best practice to write a unit test for async await code?</h2><p>You can use Swift Testing and make your test method async, then test it there. If you need that method to run with a particular isolation, you can mark the test itself or the entire test case type with <code>MainActor</code>, or with whatever isolation you need that test to run on. Swift Testing is definitely worth checking out if you have not already.</p><p><a href="https://youtu.be/E95agtPgaa0?t=2014">&#9201;&#65039;Time code</a></p><div><hr></div><h2>What is best use case of <code>Task.detached</code>?</h2><p>I have seen a lot of questions recently about when to use <code>Task.detached</code> versus when to use <code>Task { @concurrent in }</code>, because now those two things have very similar behaviors. The main difference is that the plain <code>Task</code> initializer, even when you specify some isolation, still inherits certain things from the surrounding context. Obviously, if you have explicitly specified an isolation, it does not inherit isolation, but it does inherit priority from the surrounding context. <code>Task.detached</code>, on the other hand, is completely detached. Nothing from the surrounding context is inherited by that task. So if you want something that is completely detached from the surrounding context, use <code>Task.detached</code>. If you just want to control the isolation, but you still want those other things to be inherited, use <code>Task { @concurrent in }</code>.</p><p><a href="https://youtu.be/E95agtPgaa0?t=2057">&#9201;&#65039;Time code</a></p><div><hr></div><h2>What&#8217;s the recommended pattern when processing many (thousands) of files, not just &#8220;spawn a task for every item&#8221;?</h2><p>Profile first, because you never really want to just theorize about what might improve your performance the most. Experiment, profile, and see what is actually working. I could imagine that spawning a task for every file might not be the right design. Maybe you want to have some kind of batching logic instead. But you could try the most straightforward approach first and see how it works out before doing something more complicated.</p><p><a href="https://youtu.be/E95agtPgaa0?t=2117">&#9201;&#65039;Time code</a></p><div><hr></div><h2>Async Task from a SwiftUI view that calls a shared <code>@Observable</code> service. If the user dismisses the view before the task finishes, what&#8217;s the way to cancel it so the <code>URLSession</code> request is actually dropped (not just abandoned) and no UI state mutation happens post-dismiss?</h2><p>In SwiftUI&#8217;s <code>task</code> modifier, SwiftUI does not currently expose a way to give you the task handle so you can cancel the task owned by SwiftUI. But since <code>task</code> is effectively similar to <code>onAppear</code> starting a task, you could structure your code so that you own that task yourself. Then, within <code>onAppear</code> of your SwiftUI view, you can start it, keep the task handle, and cancel it yourself when needed. If the condition is that the user dismisses the view before the task is finished, then you may also be able to use the <code>task</code> modifier directly, because SwiftUI cancels the task spawned there when the view is dismissed. So it depends on what your expectation is. But there is always a way to use your own <code>Task</code> instance that is not owned by SwiftUI if you need more granular control.</p><p><a href="https://youtu.be/E95agtPgaa0?t=2176">&#9201;&#65039;Time code</a></p><div><hr></div><h2>What are Apple&#8217;s recommended best practices for exposing data from actors in SwiftUI? Creating a &#8220;proxy&#8221; data broker between the actors and UI adds a lot of boilerplate, requires my own notification mechanism when actor data changes (I believe).</h2><p>Exposing data from actors in SwiftUI depends on how that proxy object looks. But as we discussed earlier, introducing actors into your code adds extra synchronization points, where you have to <code>await</code> certain pieces. To access actor state from SwiftUI&#8217;s <code>MainActor</code> isolation, you need to introduce a layer that handles that synchronization. So it is almost inevitable that some kind of layer has to be introduced to synchronize that state. It would be helpful to hear more about the specifics, especially what boilerplate is involved here, what the notification mechanism looks like, and how the architecture is set up. This is probably a good case to continue the conversation on the Swift Forums, or file feedback with a sample project that has a similar architecture. That would make it easier to look more closely at the setup and see whether there is room for improvement to make this easier.</p><p><a href="https://youtu.be/E95agtPgaa0?t=2287">&#9201;&#65039;Time code</a></p><div><hr></div><h2>Any plans to support defers in async contexts?</h2><p>There is another accepted proposal for Swift 6.4 that lifts the limitation on async code in <code>defer</code> blocks. There is no special syntax in a <code>defer</code> block. You can just call async methods, and it works. So yes, this is another recently accepted proposal. These questions are pointing directly to Swift Evolution. There is a dashboard on swift.org where you can filter by implemented proposals for Swift 6.4, if you are curious and want to see the full list of proposals coming in Swift 6.4.</p><p><a href="https://youtu.be/E95agtPgaa0?t=2404">&#9201;&#65039;Time code</a></p><div><hr></div><h2>If we are currently using Swift 5, have approachable concurrency set to no, default actor isolation is nonisolated and strict concurrency checking is minimal. What is the best first steps for us to move to Swift 6 concurrency?</h2><p>The first step I would recommend is enabling the approachable concurrency features. There is migration tooling you can use from the command line, and the Swift Migration Guide covers how to do this, especially in cases where you rely on the old behavior for async functions. If you are using that behavior extensively in your project, there is a straightforward way to maintain it: write <code>@concurrent</code> on those async functions. So there are mechanical ways to migrate to those upcoming features without changing the behavior of your code. The approachable concurrency features are meant to make it so there are some data safety errors you do not have to deal with. Part of their purpose is to make the defaults reduce concurrency unless you have explicitly introduced it yourself. After that, you can either change strict concurrency checking from minimal to complete, or you can go feature by feature. In Xcode build settings, there is a section with specific upcoming features where you can enable specific categories of data-race safety diagnostics. For example, static variables are generally unsafe if they are not isolated and are mutable from anywhere. You can enable just those diagnostics, deal with all your static variables, and then move on to the next category. If you prefer working that way, you can deal entirely with the same kind of problem, find the few solutions that apply, fix those cases, enable that category, and then move on. Another approach is to turn everything on first, but that can produce a lot of diagnostics and feel overwhelming. In Foundation, for example, adoption has happened target by target. Some simpler targets could enable more features earlier because there were fewer issues to work through. In other targets, specific upcoming feature flags were enabled either in the Xcode project or Swift package manifest. That makes it possible to enable diagnostics the team understands and has already decided how to handle, while leaving other areas turned off until there is a clearer approach. SwiftUI adoption followed a similar pattern. When you see a large number of warnings, it helps to look for patterns. Once you identify a recurring issue, you can think about a more general solution and apply it in multiple places. For example, marking something with <code>MainActor</code> or adding a missing <code>Sendable</code> annotation can make many warnings disappear because all the call sites of that code become concurrency-safe. A common misconception is that you have to refactor everything into a perfect concurrency model immediately. In reality, looking at the diagnostics feature by feature lets you work iteratively and focus on specific patterns that usually have only one or two real fixes. Once your current app architecture works with native Swift concurrency, you can start thinking more deeply about how to express that architecture: where to introduce isolation domains, how to break down models, and which properties or components have different concurrency stories.</p><p><a href="https://youtu.be/E95agtPgaa0?t=2454">&#9201;&#65039;Time code</a></p><div><hr></div><h2>Does MainActor guarantee main thread? If not, when does it use another thread?</h2><p>On our systems, <code>MainActor</code> is the main thread for sure. You could imagine an environment that Swift compiles for where <code>MainActor</code> might technically be some other thread. There are programs that are not apps where that might make sense, but for most intents and purposes, <code>MainActor</code> means the main thread. There are also some cases where you can have something annotated as <code>MainActor</code>, but it is still possible to call it from off <code>MainActor</code> in a context where the call site does not have strict concurrency checking enabled. I think the case where people see this most frequently is when interoperating with C, Objective-C, or C++. Those languages do not have static data safety, so it is possible to get a data race if you call something that is <code>MainActor</code>-isolated from one of those contexts. Swift also has tools to insert dynamic checking if you want to catch cases like that at runtime.</p><p><a href="https://youtu.be/E95agtPgaa0?t=2751">&#9201;&#65039;Time code</a></p><div><hr></div><h2>What&#8217;s the best practice to handle async shutdown functions during app termination? OS Lifecycle methods (such as <code>applicationWillTerminate</code>) are synchronous and they expect you to wrap up work there. Is it safe to use a semaphore in that scenario or are there better alternatives?</h2><p>Generally speaking, you do not want to use semaphores to bridge asynchronous code to synchronous code, because it is likely to result in deadlock. If you block on an asynchronous method using a semaphore, and the concurrent pool blocks on that, it is quite likely that you can use up all the resources, and suddenly nothing can make forward progress. So I do not recommend that. The situation you are describing is tough. You are in a situation where you have been given a synchronous callback, and it is the only interface you have. Trying to refactor as much code as possible that you need to handle in that context so that it is synchronous itself is probably the best approach. Obviously, that can be really difficult in some situations, but I would not recommend using a semaphore to solve that problem.</p><p><a href="https://youtu.be/E95agtPgaa0?t=2830">&#9201;&#65039;Time code</a></p><div><hr></div><h2>What is an executor? Is that exposed to us in Swift?</h2><p>You can make use of them, but you do not have to. Actors have default executors. The default executor manages scheduling for tasks, or more precisely, the synchronous pieces of a task, which are called jobs. Actors effectively have a priority queue of jobs that need to run on the actor from various tasks, and the executor handles scheduling and running those jobs. By default, that happens on the concurrent thread pool. You can also implement your own custom executors by conforming to a protocol and implementing whatever scheduling you need for your actor. Then you can provide a method on the actor that creates an instance of your custom executor type if you want to control that behavior. But the defaults are there so that you only need to drop down into that behavior if you need very fine-grained control over how those tasks are scheduled or run, or if you want to manage your own custom thread pool. That is something people sometimes do in services. So yes, those tools are available, but there are sensible defaults when you do not need that kind of control.</p><p><a href="https://youtu.be/E95agtPgaa0?t=2906">&#9201;&#65039;Time code</a></p><div><hr></div><h2>What does a healthy migration path from GCD and OperationQueue to Swift concurrency look like in a large existing app?</h2><p>On Foundation, we have experienced a lot of these migrations, moving from things that use GCD, OperationQueue, run loops, and other mechanisms. I think a healthy migration is first and foremost an incremental migration. As mentioned earlier, trying to change the architecture of your app at the same time as you adopt these features is not necessarily easy, and it may also be difficult for other members of your team to review. An incremental migration means looking at the architecture your app has today and starting with the parts that are already easy to express using the compiler tools we have now. For example, when looking at how Foundation could adopt concurrency attributes, the first step was to identify types or functionality where the implementation was known and where it was trivial to add a few annotations to express things to the compiler. Once that base layer is done, you can start working on the harder parts. That helps you work piece by piece, instead of trying to fix a simple missing <code>Sendable</code> annotation while also figuring out how to rearchitect something else. In some cases, it was simple enough to add annotations. For example, <code>UndoManager</code> is heavily UI-related, so it made sense to make it <code>MainActor</code>-bound. Other types simply received a <code>Sendable</code> annotation because they were simple Swift value types. But there are also things where you may need to think about a different approach. For example, <code>NotificationCenter</code> was not, strictly speaking, easy to use with async APIs, so we reimagined how <code>NotificationCenter</code> could work with new APIs. The goal was to structure it so that you can use new async APIs while still integrating with older legacy APIs, sometimes using unsafe escape hatches to cross the boundary between them. Working incrementally lets you start with the easy things, enable the warnings you can fully resolve early so you do not accidentally backpedal, and then look at your architecture to decide how more complex concepts should map into Swift concurrency. It takes time. Foundation is still in the process of adopting some of these features as new compiler features appear and people propose new suggestions on the Swift Forums. But over time, piece by piece, you can get to a place where complex interactions that were previously expressed through threads, queues, or run loops become much easier to understand because they are now expressed directly in Swift code, rather than hidden in documentation comments or notes. There is some upfront burden when deciding how to rethink something that was heavily OperationQueue-based or GCD-based in the Swift concurrency world. But once you have APIs and architecture that integrate nicely with Swift concurrency, it becomes much easier to build on top of them. Having a base layer that works with Swift concurrency also makes it easier for apps using Foundation to integrate with new SwiftUI features or other APIs that take advantage of Swift concurrency. As the ecosystem moves further into a world where these ideas can be expressed clearly in code, those APIs start to fit more naturally with async/await and other Swift concurrency APIs from Apple SDKs and third-party packages. One additional recommendation is to prioritize areas where you are writing new code. If some part of your codebase is in maintenance mode, has been around for a long time, and only receives occasional bug fixes, it is okay not to dive in and rewrite it wholesale just to modernize it. The most important goal is to reach a point where you can write new code using Swift concurrency features and express that new code with static data-safety guarantees. Legacy code can be revisited over time when you have a specific motivation to do that work.</p><p><a href="https://youtu.be/E95agtPgaa0?t=3019">&#9201;&#65039;Time code</a></p><div><hr></div><h2>Does it make sense to use I have a generic capture in Tasks?</h2><p>It really depends on what you are trying to do. It is definitely popular to use <code>weak self</code> in this kind of situation. People often want to optimize for the case where they are worried about capturing a reference to <code>self</code> and keeping it alive for too long. If that is your concern, you might use <code>weak self</code> to make sure the task is not what keeps <code>self</code> alive for too long. But you really need to understand what you are trying to accomplish in that task. It might be appropriate for the task to keep <code>self</code> alive until it finishes, because you want to complete that work regardless of whether everything else in the system has released its references to <code>self</code>. It depends on the kind of work. For example, with a task kicked off in a view, you may not want the view to be held onto by the task for too long after it disappears. That might be a good place to use <code>weak self</code>. As with most things, it depends. One case to be especially careful with is infinitely running tasks. If there is no end to the task, then the task may be what keeps <code>self</code> around. That is also the case where you can inadvertently get into reference cycles, especially if you are also storing a handle to that task on <code>self</code>. But as long as you do not have that kind of situation, it may be appropriate not to use <code>weak self</code> and let the task keep <code>self</code> alive until the work finishes.</p><p><a href="https://youtu.be/E95agtPgaa0?t=3361">&#9201;&#65039;Time code</a></p><div><hr></div><h2>I have a generic function that takes a Codable object: T as a parameter, makes a network call to retrieve an array of those objects and then returns an array of T. This will generate an error in Swift 6 about conformance must be on main thread. <br>Whats the best way to avoid this?</h2><p>I suspect that isolated conformance might be coming into play here. Depending on the features of the target you are building, if you have <code>MainActor</code> isolation by default enabled, it might require that the <code>Decodable</code> conformance you pass is on the main thread, or that the conformance might be <code>MainActor</code>-isolated, but you are passing it to something that does not require that. I would check the documentation. The <code>SendableMetatype</code> protocol might be a good annotation to add to this API, depending on whether the conformance is the problem or whether the values being returned are the problem. That could mean using <code>SendableMetatype</code> on the type you are passing in, or perhaps using the <code>sending</code> keyword on the values you are returning, if you are just fetching them from the network, handing them off to the caller, and never touching them again. I suspect some of those keywords might help express the fact that you are taking a conformance to <code>Codable</code> and perhaps sending it to a different isolation to perform the network request, or taking the values created from that background isolation and sending them back to the calling isolation. In one of those directions, one of those keywords is likely to help resolve it. The context here is really valuable, so providing a sample through Feedback Assistant, checking the forums, and looking at the documentation would help narrow down the exact recommendation.</p><p><a href="https://youtu.be/E95agtPgaa0?t=3468">&#9201;&#65039;Time code</a></p><div><hr></div><h2><strong>&#127942; </strong>Acknowledgments</h2><p>A huge thank-you to everyone who joined and shared thoughtful, insightful, and engaging questions throughout the session. Your curiosity and input made the discussion truly rich and collaborative.</p><p>Finally, a heartfelt thank-you to the Apple employees for leading the session, sharing expert guidance, and offering clear explanations of app optimization techniques. Your contributions made this an outstanding learning experience for everyone involved.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h2>One More thing&#8230;</h2><p>Through years of mentoring, I&#8217;ve built a small bookshelf of titles that are worth reading &#8212; or at least being aware of. The Grokking series from Manning Publishing is one of them. The style, the language, and the illustrations all combine to explain complex topics in an easy and engaging way.</p><p>You&#8217;ll find yourself wanting to learn more about things that once felt intimidating or difficult to understand. One of the latest additions is <em><a href="https://www.manning.com/books/grokking-data-structures?utm_source=AG-DEV&amp;utm_medium=affiliate&amp;utm_campaign=book_larocca3_grokking_9_28_23&amp;a_aid=AG-DEV&amp;a_bid=89b98f8d&amp;chan=sbs">Grokking Data Structures</a></em> by Marcello La Rocca.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!vuaA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vuaA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg 424w, https://substackcdn.com/image/fetch/$s_!vuaA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg 848w, https://substackcdn.com/image/fetch/$s_!vuaA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!vuaA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!vuaA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg" width="851" height="315" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:315,&quot;width&quot;:851,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:63335,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:&quot;&quot;,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/194161391?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!vuaA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg 424w, https://substackcdn.com/image/fetch/$s_!vuaA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg 848w, https://substackcdn.com/image/fetch/$s_!vuaA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!vuaA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><a href="https://www.manning.com/books/grokking-data-structures?utm_source=AG-DEV&amp;utm_medium=affiliate&amp;utm_campaign=book_larocca3_grokking_9_28_23&amp;a_aid=AG-DEV&amp;a_bid=89b98f8d&amp;chan=sbs">Sharing the link with you</a>.</p>]]></content:encoded></item><item><title><![CDATA[SwiftUI: Refreshable Task Cancellation Promo]]></title><description><![CDATA[Understand the Refreshable Modifier]]></description><link>https://antongubarenko.substack.com/p/swiftui-refreshable-task-cancellation-cfc</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/swiftui-refreshable-task-cancellation-cfc</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Wed, 22 Apr 2026 06:44:33 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/83c44cc7-ae49-4988-906a-8d84f3ea2a74_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ajnu!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60134ad8-58c0-4461-ac09-f1d105e28819_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ajnu!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60134ad8-58c0-4461-ac09-f1d105e28819_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!ajnu!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60134ad8-58c0-4461-ac09-f1d105e28819_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!ajnu!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60134ad8-58c0-4461-ac09-f1d105e28819_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!ajnu!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60134ad8-58c0-4461-ac09-f1d105e28819_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ajnu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60134ad8-58c0-4461-ac09-f1d105e28819_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/60134ad8-58c0-4461-ac09-f1d105e28819_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:634958,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/194712576?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60134ad8-58c0-4461-ac09-f1d105e28819_1536x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ajnu!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60134ad8-58c0-4461-ac09-f1d105e28819_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!ajnu!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60134ad8-58c0-4461-ac09-f1d105e28819_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!ajnu!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60134ad8-58c0-4461-ac09-f1d105e28819_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!ajnu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60134ad8-58c0-4461-ac09-f1d105e28819_1536x1024.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Pull-to-refresh became a standard interaction in iOS apps long ago, so <code>.refreshable</code> feels like one of the most natural SwiftUI APIs. You attach the modifier, await some async work, and SwiftUI handles the native refresh behavior for you.</p><p>Apple introduced <code>.refreshable</code> during the WWDC21 SwiftUI updates, in the same release cycle as Swift concurrency and the <code>task</code> modifier. In that session, Apple presented it as the modern way to support pull-to-refresh on iOS and iPadOS, while <code>task</code> was described as async work tied to a view&#8217;s lifetime and automatically cancelled when that view disappears.</p><p>And that lifecycle detail is exactly where things start to get interesting. But let&#8217;s get back to declarations.</p><div><hr></div><h2>What is<code>.refreshable</code>?</h2><p>At its core, <code>.refreshable</code> gives a view an async refresh action:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;44b01018-8f90-4e4f-a84a-a4f75a864cbf&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">List(items) { item in
    Text(item.title)
}
.refreshable {
    await reload()
}</code></pre></div><p>The system then connects that action to pull-to-refresh behavior where supported. Apple also notes that the action is propagated through the environment as a <code>RefreshAction</code>, which is why the feature integrates naturally with SwiftUI&#8217;s declarative model.</p><p>It feels like a callback, but it is more than that. It is SwiftUI-owned async work.</p><p><a href="https://open.substack.com/pub/antongubarenko/p/swiftui-refreshable-task-cancellation?r=21t43r&amp;utm_campaign=post&amp;utm_medium=web&amp;showWelcomeOnShare=true">Full article here</a>.</p><div><hr></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[SwiftUI: Refreshable Task Cancellation]]></title><description><![CDATA[Understand the Refreshable]]></description><link>https://antongubarenko.substack.com/p/swiftui-refreshable-task-cancellation</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/swiftui-refreshable-task-cancellation</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Wed, 22 Apr 2026 06:40:32 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/a3ff8937-7596-428a-93a0-871cf91467b7_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ajnu!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60134ad8-58c0-4461-ac09-f1d105e28819_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ajnu!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60134ad8-58c0-4461-ac09-f1d105e28819_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!ajnu!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60134ad8-58c0-4461-ac09-f1d105e28819_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!ajnu!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60134ad8-58c0-4461-ac09-f1d105e28819_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!ajnu!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60134ad8-58c0-4461-ac09-f1d105e28819_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ajnu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60134ad8-58c0-4461-ac09-f1d105e28819_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/60134ad8-58c0-4461-ac09-f1d105e28819_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:634958,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/194712576?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60134ad8-58c0-4461-ac09-f1d105e28819_1536x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ajnu!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60134ad8-58c0-4461-ac09-f1d105e28819_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!ajnu!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60134ad8-58c0-4461-ac09-f1d105e28819_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!ajnu!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60134ad8-58c0-4461-ac09-f1d105e28819_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!ajnu!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60134ad8-58c0-4461-ac09-f1d105e28819_1536x1024.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Pull-to-refresh became a standard interaction in iOS apps long ago, so <code>.refreshable</code> feels like one of the most natural SwiftUI APIs. You attach the modifier, await some async work, and SwiftUI handles the native refresh behavior for you.</p><p>Apple introduced <code>.refreshable</code> during the WWDC21 SwiftUI updates, in the same release cycle as Swift concurrency and the <code>task</code> modifier. In that session, Apple presented it as the modern way to support pull-to-refresh on iOS and iPadOS, while <code>task</code> was described as async work tied to a view&#8217;s lifetime and automatically cancelled when that view disappears.</p><p>And that lifecycle detail is exactly where things start to get interesting. But let&#8217;s get back to declarations.</p><div><hr></div><h2>What is<code>.refreshable</code>?</h2><p>At its core, <code>.refreshable</code> gives a view an async refresh action:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;44b01018-8f90-4e4f-a84a-a4f75a864cbf&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">List(items) { item in
    Text(item.title)
}
.refreshable {
    await reload()
}</code></pre></div><p>The system then connects that action to pull-to-refresh behavior where supported. Apple also notes that the action is propagated through the environment as a <code>RefreshAction</code>, which is why the feature integrates naturally with SwiftUI&#8217;s declarative model.</p><p>It feels like a callback, but it is more than that. It is SwiftUI-owned async work.</p><div><hr></div><h2>A Simple Example</h2><p>Let&#8217;s show <code>ScrollView</code> with random items added with delay:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;b7aa9567-5bd1-422d-80cb-aa7a602f6280&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">import SwiftUI

struct LoadedItem: Identifiable {
    let id: Int
    let value: Int
}

struct RefreshCancellView: View {

    @State private var items: [LoadedItem] = []

    var body: some View {
        NavigationStack {
            ScrollView(.vertical, showsIndicators: false) {
                LazyVStack(spacing: 16) {
                    ForEach(items) { item in
                        Text(item.value, format: .number)
                    }
                }
            }
            .padding(16)
            .refreshable {
                await loadItems(fromRefresh: true)
            }
            .task {
                //Loading the initial list
                await loadItems()
            }
            .navigationTitle("Refreshable Example")
            .navigationBarTitleDisplayMode(.inline)
        }
    }

    func loadItems(fromRefresh: Bool = false) async {
        await MainActor.run { items.removeAll() }

        let count = Int.random(in: 20...40)

        for i in 0..&lt;count {
            try? await Task.sleep(for: .milliseconds(Int.random(in: 50...150)))

            //Checking cancelaltion
            if Task.isCancelled {
                print("Cancelled fromRefresh: \(fromRefresh)")
                return
            }

            let value = Int.random(in: 0...10_000)
            let newItem = LoadedItem(id: i, value: value)

            await MainActor.run {
                items.append(newItem)
            }
        }
    }
}</code></pre></div><p>On the surface, this looks reasonable. Clear the old values, load the new ones, append them as they arrive, and check for cancellation between steps.</p><p>But this code is actually a very good demonstration of the problem.</p><div><hr></div><h2>The Problem</h2><p>The key trigger is this line:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;17edec09-57d5-49a2-8d48-2d889aa65262&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">await MainActor.run { items.removeAll() }</code></pre></div><p>That mutation happens immediately inside the <code>.refreshable</code> task.</p><p>Since <code>items</code> is <code>@State</code>, SwiftUI redraws the body right away. And because the refresh work is owned by SwiftUI, that redraw can invalidate the task context that is currently running the refresh closure. The Stack Overflow discussion describes exactly this: clearing the data causes the body to redraw, <code>.refreshable</code> is redrawn too, and the previous task gets cancelled.</p><blockquote><p>And this could throw you into debugging session for a long of time&#8230;</p></blockquote><p>Then the example makes the situation even more fragile here:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;d1c21dff-a411-4188-b2e2-9bf40f59798e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">await MainActor.run {
    items.append(newItem)
}</code></pre></div><p>Every append is another state mutation, which means another redraw opportunity while the refresh task is still active.</p><p>So this sample now has both failure multipliers:</p><ul><li><p>one immediate redraw from <code>removeAll()</code></p></li><li><p>repeated redraws from every <code>append</code></p></li></ul><p>That is why it reproduces the issue so clearly.</p><div><hr></div><h2>Why This Happens</h2><p>The simplest mental model is this:</p><p><code>.refreshable</code> runs your closure as structured work owned by SwiftUI.</p><p>Apple&#8217;s WWDC explanation of the related <code>task</code> modifier is useful here: SwiftUI can attach async work to a view lifetime and cancel it when the view is removed. While Apple&#8217;s <code>.refreshable</code> docs are brief, the observed behavior lines up with that same lifecycle-driven model.</p><p>So when your <strong>refresh code changes</strong> the very state that drives the current body, SwiftUI may <strong>rebuild</strong> enough of the hierarchy to cancel the refresh-owned task before it completes. The Stack Overflow thread phrases it quite directly: clearing the observable collection redraws the body, <code>.refreshable</code> redraws too, and the previous task is cancelled.</p><p>This is also why the issue feels so strange. The code is not &#8220;wrong&#8221; in the usual sense. It is just colliding with the ownership model of SwiftUI concurrency.</p><div><hr></div><h2>A Common Symptom</h2><p>You pull to refresh, the spinner appears, work starts, and then one of these happens:</p><ul><li><p>the task stops halfway through</p></li><li><p>only part of the list loads</p></li><li><p>a network request suddenly throws cancellation (<strong>Code -999</strong> for the those who are trying to Google it)</p></li></ul><p>That half-finished feeling is often the first clue that the problem is not your API layer, but the view lifecycle interacting with structured concurrency.</p><h3>Fix 1: Avoid intermediate UI changes until the final update</h3><p>The cleanest fix is to avoid publishing intermediate mutations while the refresh is running.</p><p>Instead of clearing and appending directly into <code>@State</code>, collect results locally and assign once at the end:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;fdf97e8d-70b2-42fe-855c-171bd341ca32&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">func loadItems(fromRefresh: Bool = false) async {
    let count = Int.random(in: 20...40)
    var loaded: [LoadedItem] = []

    for i in 0..&lt;count {
        try? await Task.sleep(for: .milliseconds(Int.random(in: 50...150)))

        if Task.isCancelled {
            print("Cancelled fromRefresh: \(fromRefresh)")
            return
        }

        let value = Int.random(in: 0...10_000)
        loaded.append(.init(id: i, value: value))
    }

    await MainActor.run {
        items = loaded
    }
}</code></pre></div><p>This works better because SwiftUI sees only one final state change instead of dozens of redraw-triggering mutations.</p><p>The tradeoff is that you lose progressive updates during refresh. But in most real pull-to-refresh flows, that is completely acceptable. The user usually cares more about a stable completion than about watching rows arrive one by one.</p><h3>Fix 2: Move the Work Into an Unstructured Task</h3><p>The other workaround is to let <code>.refreshable</code> create a separate task and await that task&#8217;s value:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;3b278f33-ad6b-441a-b9d0-4860facc1491&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">.refreshable {
    await Task {
        await loadItems(fromRefresh: true)
    }.value
}</code></pre></div><p>This pattern appears directly in one of the Stack Overflow answers: wrapping the logic in <code>Task { ... }.value</code> allows the refresh spinner to remain visible while the new task continues even if the original refresh-owned task is cancelled.</p><p>That makes it a practical fix when redraw-driven cancellation is getting in the way.</p><p>There is also a looser variation:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;c0df5d02-7f8f-4260-bdfb-4dd5f34233b3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">.refreshable {
    Task {
        await loadItems(fromRefresh: true)
    }
}</code></pre></div><p>That decouples the work even more, but it also changes the semantics more aggressively because the refresh action no longer truly owns the work. The safer version is usually <code>await Task { ... }.value</code>, since it still lets the refresh indicator track completion.</p><div><hr></div><h2>Which Fix is Better?</h2><p>In most cases, the first fix is the better design.</p><p>Use the <strong>single final update</strong> approach when:</p><ul><li><p>you can build the refreshed data off to the side</p></li><li><p>progressive rendering is not important</p></li><li><p>you want to stay inside structured concurrency as much as possible</p></li></ul><p>Use the <strong>unstructured task wrapper</strong> when:</p><ul><li><p>redraw-driven cancellation is hard to avoid</p></li><li><p>intermediate observable changes are required</p></li><li><p>you need a practical workaround more than a pure structured model</p></li></ul><p>The Stack Overflow thread includes both ideas in different forms: avoid redraw-triggering updates until later, or wrap the refresh body in a new task so it survives the redraw.</p><div><hr></div><h2>Final Thoughts</h2><p>This is one of those SwiftUI behaviors that feels confusing until you frame it correctly.</p><p>If your <code>.refreshable</code> code updates the source of truth too early, the refresh task may cancel itself through the very redraw it caused. That is why the safest approach is often to keep all temporary work local and publish one final result when loading finishes.</p><p>And if you truly need incremental changes during refresh, then an unstructured task wrapper may be the most practical escape hatch.</p><p>That is a subtle detail, but it is the difference between a refresh flow that feels flaky and one that behaves predictably.</p><div><hr></div><h2>References</h2><ul><li><p><a href="https://developer.apple.com/documentation/swiftui/view/refreshable%28action%3A%29">Apple Developer Documentation:</a> <code>refreshable(action:)</code></p></li><li><p><a href="https://developer.apple.com/documentation/swiftui/refreshaction">Apple Developer Documentation:</a> <code>RefreshAction</code></p></li><li><p><a href="https://developer.apple.com/videos/play/wwdc2021/10018/">Apple WWDC21 session</a> &#8220;What&#8217;s new in SwiftUI,&#8221; including <code>refreshable</code> and lifecycle-bound async tasks.</p></li></ul><div><hr></div><h2>One More thing&#8230;</h2><p>Through years of mentoring, I&#8217;ve built a small bookshelf of titles that are worth reading &#8212; or at least being aware of. The Grokking series from Manning Publishing is one of them. The style, the language, and the illustrations all combine to explain complex topics in an easy and engaging way.</p><p>You&#8217;ll find yourself wanting to learn more about things that once felt intimidating or difficult to understand. One of the latest additions is <em><a href="https://www.manning.com/books/grokking-data-structures?utm_source=AG-DEV&amp;utm_medium=affiliate&amp;utm_campaign=book_larocca3_grokking_9_28_23&amp;a_aid=AG-DEV&amp;a_bid=89b98f8d&amp;chan=sbs">Grokking Data Structures</a></em> by Marcello La Rocca.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!vuaA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vuaA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg 424w, https://substackcdn.com/image/fetch/$s_!vuaA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg 848w, https://substackcdn.com/image/fetch/$s_!vuaA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!vuaA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!vuaA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg" width="851" height="315" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:315,&quot;width&quot;:851,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:63335,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/194161391?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!vuaA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg 424w, https://substackcdn.com/image/fetch/$s_!vuaA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg 848w, https://substackcdn.com/image/fetch/$s_!vuaA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!vuaA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><a href="https://www.manning.com/books/grokking-data-structures?utm_source=AG-DEV&amp;utm_medium=affiliate&amp;utm_campaign=book_larocca3_grokking_9_28_23&amp;a_aid=AG-DEV&amp;a_bid=89b98f8d&amp;chan=sbs">Sharing the link with you</a>.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[Swift Bits: 320 x 480 Window]]></title><description><![CDATA[Debugging strange UI]]></description><link>https://antongubarenko.substack.com/p/swift-bits-320-x-480-window</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/swift-bits-320-x-480-window</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Wed, 15 Apr 2026 08:21:49 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/da938462-733e-48ef-aab5-770a311ddff7_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>There is a strange iOS behavior that feels almost archaeological at this point: if your app does not have a proper launch screen configured, the window can end up behaving as if the app&#8217;s size is 320 &#215; 480. Both for UIKit app with AppDelegate/SceneDelegate and SwiftUI.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!xirq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96bde9f5-b7f8-445e-bcb2-33dcbb4644f7_1206x2622.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xirq!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96bde9f5-b7f8-445e-bcb2-33dcbb4644f7_1206x2622.png 424w, https://substackcdn.com/image/fetch/$s_!xirq!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96bde9f5-b7f8-445e-bcb2-33dcbb4644f7_1206x2622.png 848w, https://substackcdn.com/image/fetch/$s_!xirq!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96bde9f5-b7f8-445e-bcb2-33dcbb4644f7_1206x2622.png 1272w, https://substackcdn.com/image/fetch/$s_!xirq!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96bde9f5-b7f8-445e-bcb2-33dcbb4644f7_1206x2622.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xirq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96bde9f5-b7f8-445e-bcb2-33dcbb4644f7_1206x2622.png" width="448" height="974.0099502487562" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/96bde9f5-b7f8-445e-bcb2-33dcbb4644f7_1206x2622.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:2622,&quot;width&quot;:1206,&quot;resizeWidth&quot;:448,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;User attachment&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="User attachment" title="User attachment" srcset="https://substackcdn.com/image/fetch/$s_!xirq!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96bde9f5-b7f8-445e-bcb2-33dcbb4644f7_1206x2622.png 424w, https://substackcdn.com/image/fetch/$s_!xirq!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96bde9f5-b7f8-445e-bcb2-33dcbb4644f7_1206x2622.png 848w, https://substackcdn.com/image/fetch/$s_!xirq!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96bde9f5-b7f8-445e-bcb2-33dcbb4644f7_1206x2622.png 1272w, https://substackcdn.com/image/fetch/$s_!xirq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F96bde9f5-b7f8-445e-bcb2-33dcbb4644f7_1206x2622.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Facing a cutting-edge issue )</figcaption></figure></div><p>Not the actual screen size.</p><p>Not the simulator device size.</p><p>That old fallback size.</p><p>And yes, this behavior has been around for years. It still shows up in practice when launch-screen configuration is missing or broken, and developers keep rediscovering it because the symptom looks unrelated at first glance: your app launches, but the window, bounds, or rendered content behave like the device is tiny.</p><div><hr></div><h2>The strange part</h2><p>You remove the launch screen, or the app no longer points to the correct launch storyboard, and suddenly the app starts acting like it lives in a much older world.</p><p>A common symptom is that values like screen bounds appear as 320 &#215; 480, regardless of the actual device.</p><p>This is what makes the issue so confusing:</p><ul><li><p>the code that reads screen size may look correct</p></li><li><p>the simulator may be a modern iPhone</p></li><li><p>the UI may still compile and run</p></li><li><p>but the app behaves like it launched in a legacy compatibility path</p></li></ul><div><hr></div><h2>Why does this happen?</h2><p>The launch screen is not only a visual placeholder while the app starts. It is also part of how iOS understands that your app supports modern screen sizes and adapts correctly to the current device family and display characteristics.</p><p>Historically, when iOS could not determine that an app supported newer display formats, it could fall back to older sizing behavior. That is why 320 &#215; 480 keeps surfacing: it is the classic original iPhone/iPod logical size, and the system can still expose behavior that resembles that legacy compatibility mode when launch-screen support is absent or invalid.</p><div><hr></div><h2>It is not really about &#8220;splash screens&#8221;</h2><p>That is also why calling it &#8220;just a splash screen&#8221; is misleading.</p><p>On iOS, the launch screen does more than show a placeholder image. It is part of the app&#8217;s launch configuration, and missing it can affect how the app is treated at startup.</p><p>So the issue is not really visual.</p><p>It is architectural.</p><div><hr></div><h2>How to check if this is your problem</h2><p>If your app behaves suspiciously small, clipped, letterboxed, or reports old dimensions, check these first:</p><ul><li><p>does the target have a valid Launch Screen configured?</p></li><li><p>does Info.plist contain the correct UILaunchStoryboardName or equivalent launch-screen configuration?</p></li><li><p>does the referenced storyboard actually exist?</p></li><li><p>did the launch-screen file get renamed without updating the target settings?</p></li></ul><p>In practice, the fix is often simply restoring a valid LaunchScreen.storyboard and making sure the target points to it correctly.</p><div><hr></div><h2>The simplest fix</h2><p>In most native iOS projects, the practical fix is:</p><ol><li><p>Create or restore LaunchScreen.storyboard</p></li><li><p>Set it as the app&#8217;s launch screen in target settings</p></li><li><p>Ensure the app bundle includes it</p></li><li><p>Clean and rebuild the app</p></li></ol><p>Typical config:</p><pre><code><code>&lt;key&gt;UILaunchStoryboardName&lt;/key&gt;
&lt;string&gt;LaunchScreen&lt;/string&gt;</code></code></pre><p>If you are using SwiftUI, plist-based launch-screen configuration is also possible in newer setups, but the core point stays the same: the app needs a valid launch-screen definition. Even the empty ones fixing it.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!9ZQ0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdda3a6d3-182f-4a48-9d02-f0f9638433dc_1702x764.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9ZQ0!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdda3a6d3-182f-4a48-9d02-f0f9638433dc_1702x764.png 424w, https://substackcdn.com/image/fetch/$s_!9ZQ0!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdda3a6d3-182f-4a48-9d02-f0f9638433dc_1702x764.png 848w, https://substackcdn.com/image/fetch/$s_!9ZQ0!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdda3a6d3-182f-4a48-9d02-f0f9638433dc_1702x764.png 1272w, https://substackcdn.com/image/fetch/$s_!9ZQ0!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdda3a6d3-182f-4a48-9d02-f0f9638433dc_1702x764.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!9ZQ0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdda3a6d3-182f-4a48-9d02-f0f9638433dc_1702x764.png" width="1456" height="654" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/dda3a6d3-182f-4a48-9d02-f0f9638433dc_1702x764.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:654,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:291167,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/194161391?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdda3a6d3-182f-4a48-9d02-f0f9638433dc_1702x764.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!9ZQ0!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdda3a6d3-182f-4a48-9d02-f0f9638433dc_1702x764.png 424w, https://substackcdn.com/image/fetch/$s_!9ZQ0!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdda3a6d3-182f-4a48-9d02-f0f9638433dc_1702x764.png 848w, https://substackcdn.com/image/fetch/$s_!9ZQ0!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdda3a6d3-182f-4a48-9d02-f0f9638433dc_1702x764.png 1272w, https://substackcdn.com/image/fetch/$s_!9ZQ0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdda3a6d3-182f-4a48-9d02-f0f9638433dc_1702x764.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Info Tab in Target Settings - Launch Screens</figcaption></figure></div><p>For Storyboard, you can check:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!UlPM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F826bb72c-cac3-4460-ac14-f474e3f68df5_1676x758.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!UlPM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F826bb72c-cac3-4460-ac14-f474e3f68df5_1676x758.png 424w, https://substackcdn.com/image/fetch/$s_!UlPM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F826bb72c-cac3-4460-ac14-f474e3f68df5_1676x758.png 848w, https://substackcdn.com/image/fetch/$s_!UlPM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F826bb72c-cac3-4460-ac14-f474e3f68df5_1676x758.png 1272w, https://substackcdn.com/image/fetch/$s_!UlPM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F826bb72c-cac3-4460-ac14-f474e3f68df5_1676x758.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!UlPM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F826bb72c-cac3-4460-ac14-f474e3f68df5_1676x758.png" width="1456" height="659" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/826bb72c-cac3-4460-ac14-f474e3f68df5_1676x758.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:659,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:288939,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/194161391?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F826bb72c-cac3-4460-ac14-f474e3f68df5_1676x758.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!UlPM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F826bb72c-cac3-4460-ac14-f474e3f68df5_1676x758.png 424w, https://substackcdn.com/image/fetch/$s_!UlPM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F826bb72c-cac3-4460-ac14-f474e3f68df5_1676x758.png 848w, https://substackcdn.com/image/fetch/$s_!UlPM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F826bb72c-cac3-4460-ac14-f474e3f68df5_1676x758.png 1272w, https://substackcdn.com/image/fetch/$s_!UlPM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F826bb72c-cac3-4460-ac14-f474e3f68df5_1676x758.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Info Tab in Target Settings - Single Storyboard</figcaption></figure></div><p>And it&#8217;s fine again!</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Ep8O!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42113eb3-3ffd-4c2e-914a-4d1293857037_1206x2622.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Ep8O!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42113eb3-3ffd-4c2e-914a-4d1293857037_1206x2622.png 424w, https://substackcdn.com/image/fetch/$s_!Ep8O!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42113eb3-3ffd-4c2e-914a-4d1293857037_1206x2622.png 848w, https://substackcdn.com/image/fetch/$s_!Ep8O!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42113eb3-3ffd-4c2e-914a-4d1293857037_1206x2622.png 1272w, https://substackcdn.com/image/fetch/$s_!Ep8O!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42113eb3-3ffd-4c2e-914a-4d1293857037_1206x2622.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Ep8O!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42113eb3-3ffd-4c2e-914a-4d1293857037_1206x2622.png" width="448" height="974.0099502487562" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/42113eb3-3ffd-4c2e-914a-4d1293857037_1206x2622.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:2622,&quot;width&quot;:1206,&quot;resizeWidth&quot;:448,&quot;bytes&quot;:3441618,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/194161391?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42113eb3-3ffd-4c2e-914a-4d1293857037_1206x2622.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Ep8O!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42113eb3-3ffd-4c2e-914a-4d1293857037_1206x2622.png 424w, https://substackcdn.com/image/fetch/$s_!Ep8O!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42113eb3-3ffd-4c2e-914a-4d1293857037_1206x2622.png 848w, https://substackcdn.com/image/fetch/$s_!Ep8O!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42113eb3-3ffd-4c2e-914a-4d1293857037_1206x2622.png 1272w, https://substackcdn.com/image/fetch/$s_!Ep8O!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F42113eb3-3ffd-4c2e-914a-4d1293857037_1206x2622.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">The Weather is fine )</figcaption></figure></div><div><hr></div><h2>Why this is still surprising in 2026</h2><p>Because it feels like the kind of issue that should have disappeared with older iPhone screen transitions.</p><p>But launch behavior sits in a compatibility-sensitive part of the platform, and when that configuration is missing, iOS can still fall back in ways that expose very old assumptions. That is why this bug feels so strange: it does not look like a missing resource problem. It looks like the whole app forgot what year it is.</p><div><hr></div><h2>References</h2><ul><li><p>Apple Developer Documentation: <a href="https://developer.apple.com/documentation/bundleresources/information-property-list/uilaunchstoryboardname?utm_source=chatgpt.com">UILaunchStoryboardName</a></p></li><li><p>Apple Developer Documentation: <a href="https://developer.apple.com/documentation/xcode/specifying-your-apps-launch-screen?utm_source=chatgpt.com">Specifying your app&#8217;s launch screen</a></p></li><li><p>Apple Human Interface Guidelines: <a href="https://developer.apple.com/design/human-interface-guidelines/launching?utm_source=chatgpt.com">Launching</a></p></li><li><p>Apple Developer Documentation: <a href="https://developer.apple.com/library/archive/documentation/Xcode/Reference/xcode_ref-Asset_Catalog_Format/LaunchImageType.html?utm_source=chatgpt.com">UILaunchScreen</a></p></li><li><p>Apple Technote: <a href="https://developer.apple.com/documentation/technotes/tn3118-debugging-your-apps-launch-screen?utm_source=chatgpt.com">Debugging your app&#8217;s launch screen</a></p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h2>One More thing&#8230;</h2><p>Through years of mentoring, I&#8217;ve built a small bookshelf of titles that are worth reading &#8212; or at least being aware of. The Grokking series from Manning Publishing is one of them. The style, the language, and the illustrations all combine to explain complex topics in an easy and engaging way.</p><p>You&#8217;ll find yourself wanting to learn more about things that once felt intimidating or difficult to understand. One of the latest additions is <em><a href="https://www.manning.com/books/grokking-data-structures?utm_source=AG-DEV&amp;utm_medium=affiliate&amp;utm_campaign=book_larocca3_grokking_9_28_23&amp;a_aid=AG-DEV&amp;a_bid=89b98f8d&amp;chan=sbs">Grokking Data Structures</a></em> by Marcello La Rocca.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!vuaA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vuaA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg 424w, https://substackcdn.com/image/fetch/$s_!vuaA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg 848w, https://substackcdn.com/image/fetch/$s_!vuaA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!vuaA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!vuaA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg" width="851" height="315" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:315,&quot;width&quot;:851,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:63335,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/194161391?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!vuaA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg 424w, https://substackcdn.com/image/fetch/$s_!vuaA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg 848w, https://substackcdn.com/image/fetch/$s_!vuaA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!vuaA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5eb50d14-7bb4-455c-9006-33f2bacfb5c8_851x315.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><a href="https://www.manning.com/books/grokking-data-structures?utm_source=AG-DEV&amp;utm_medium=affiliate&amp;utm_campaign=book_larocca3_grokking_9_28_23&amp;a_aid=AG-DEV&amp;a_bid=89b98f8d&amp;chan=sbs">Sharing the link with you</a>.</p>]]></content:encoded></item><item><title><![CDATA[Spec-Driven Development with OpenSec Promo]]></title><description><![CDATA[Determined AI coding paradigm]]></description><link>https://antongubarenko.substack.com/p/spec-driven-development-with-opensec-9ea</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/spec-driven-development-with-opensec-9ea</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Tue, 07 Apr 2026 09:40:08 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!gliH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45c5d85c-e173-41d1-bfca-6a2f4fa776c4_1115x635.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!gliH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45c5d85c-e173-41d1-bfca-6a2f4fa776c4_1115x635.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!gliH!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45c5d85c-e173-41d1-bfca-6a2f4fa776c4_1115x635.png 424w, https://substackcdn.com/image/fetch/$s_!gliH!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45c5d85c-e173-41d1-bfca-6a2f4fa776c4_1115x635.png 848w, https://substackcdn.com/image/fetch/$s_!gliH!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45c5d85c-e173-41d1-bfca-6a2f4fa776c4_1115x635.png 1272w, https://substackcdn.com/image/fetch/$s_!gliH!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45c5d85c-e173-41d1-bfca-6a2f4fa776c4_1115x635.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!gliH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45c5d85c-e173-41d1-bfca-6a2f4fa776c4_1115x635.png" width="1115" height="635" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/45c5d85c-e173-41d1-bfca-6a2f4fa776c4_1115x635.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:635,&quot;width&quot;:1115,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:626616,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/193041491?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ebd6bee-9d7c-42b8-8717-f0ec57af98c8_1536x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!gliH!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45c5d85c-e173-41d1-bfca-6a2f4fa776c4_1115x635.png 424w, https://substackcdn.com/image/fetch/$s_!gliH!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45c5d85c-e173-41d1-bfca-6a2f4fa776c4_1115x635.png 848w, https://substackcdn.com/image/fetch/$s_!gliH!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45c5d85c-e173-41d1-bfca-6a2f4fa776c4_1115x635.png 1272w, https://substackcdn.com/image/fetch/$s_!gliH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F45c5d85c-e173-41d1-bfca-6a2f4fa776c4_1115x635.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>AI-assisted coding became fast much earlier than it became predictable. That is exactly why spec-driven development is getting more attention now: instead of keeping requirements scattered across chat history, you turn them into artifacts the human and the assistant can both follow. <a href="https://openspec.dev">OpenSpec</a> is one of the tools built around that idea. It adds a lightweight workflow where you agree on the change first, generate planning artifacts, and then implement against them. </p><div><hr></div><h2>What is Spec-Driven Development?</h2><p><a href="https://en.wikipedia.org/wiki/Spec-driven_development">Spec-driven development</a>, or SDD, is a workflow where the specification becomes the source of truth before implementation starts. In OpenSpec&#8217;s wording, it helps you and your AI coding assistant agree on what to build before any code is written. Rather than relying on one long prompt, you break work into artifacts such as a proposal, specs, design, and tasks.</p><p>That matters even more in AI-heavy workflows. A good assistant can generate a lot of code quickly, but if the requirements are vague, the result is usually inconsistent, incomplete, or hard to evolve. SDD adds structure without forcing a huge process. The idea is simple: describe the change, refine the behavior, plan the work, then implement it against an agreed spec.</p><p>One of the nice ideas behind <a href="https://openspec.dev">OpenSpec</a> is that the workflow is not treated as rigid phase gates. Its docs explicitly frame the model as actions rather than locked phases, so you can create, implement, update, and archive as needed. That makes it fit better with real development, where requirements often change while implementation is already in progress.</p><blockquote><p>SDD birthplace is NASA at 1960s as a mix of formal developments and multiple specifications. So we all can feel like a Space Explorers while using it )</p></blockquote><div><hr></div><h2>What is OpenSpec, anyway?</h2><p><a href="https://openspec.dev">OpenSpec</a> is an open-source tool for spec-driven development aimed at AI coding assistants. The project describes itself as a lightweight spec layer that helps teams agree on what to build before code is written, keeps each change organized in its own folder, and works with more than 20 AI assistants through generated slash commands and skills.</p><blockquote><p>The primary goal of <a href="https://openspec.dev">OpenSpec</a> is to solve the problem of unpredictability and inconsistency that can arise when using Al tools for coding. Since Al tools are non-deterministic, they can struggle to consistently follow complex requirements. OpenSpec provides a structured approach that allows teams and Al assistants to agree on specifications before coding begins. This leads to more efficient development, fewer errors, and improved code quality</p></blockquote><p>After initialization, <a href="https://openspec.dev">OpenSpec</a> generates instructions that compatible assistants can auto-detect. In its docs, that means skills in locations like <code>.claude/skills/</code>, plus a project config file at <code>openspec/config.yaml</code> if you choose to create one. The default schema is spec-driven, and the standard artifact set includes proposal, specs, design, and tasks.</p><p>A particularly useful concept in <a href="https://openspec.dev">OpenSpec</a> is the idea of delta specs. Instead of rewriting the whole product spec every time, you describe what is changing. The docs present this with sections such as <code>ADDED</code> <code>Requirements</code> and <code>MODIFIED</code> <code>Requirements</code>, which makes <a href="https://openspec.dev">OpenSpec</a> more practical for brownfield projects where the codebase already exists. We will get back to this in the next chapters.</p><p><a href="https://open.substack.com/pub/antongubarenko/p/spec-driven-development-with-opensec?r=21t43r&amp;utm_campaign=post&amp;utm_medium=web&amp;showWelcomeOnShare=true">Full article here.</a></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Spec-Driven Development with OpenSec]]></title><description><![CDATA[Determined AI coding paradigm]]></description><link>https://antongubarenko.substack.com/p/spec-driven-development-with-opensec</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/spec-driven-development-with-opensec</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Tue, 07 Apr 2026 09:38:51 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!XNcB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19af2603-74ce-4936-a22b-d41d02f7eb5d_1107x594.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!XNcB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19af2603-74ce-4936-a22b-d41d02f7eb5d_1107x594.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!XNcB!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19af2603-74ce-4936-a22b-d41d02f7eb5d_1107x594.png 424w, https://substackcdn.com/image/fetch/$s_!XNcB!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19af2603-74ce-4936-a22b-d41d02f7eb5d_1107x594.png 848w, https://substackcdn.com/image/fetch/$s_!XNcB!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19af2603-74ce-4936-a22b-d41d02f7eb5d_1107x594.png 1272w, https://substackcdn.com/image/fetch/$s_!XNcB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19af2603-74ce-4936-a22b-d41d02f7eb5d_1107x594.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!XNcB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19af2603-74ce-4936-a22b-d41d02f7eb5d_1107x594.png" width="1107" height="594" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/19af2603-74ce-4936-a22b-d41d02f7eb5d_1107x594.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:594,&quot;width&quot;:1107,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:624923,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/193041491?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ebd6bee-9d7c-42b8-8717-f0ec57af98c8_1536x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!XNcB!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19af2603-74ce-4936-a22b-d41d02f7eb5d_1107x594.png 424w, https://substackcdn.com/image/fetch/$s_!XNcB!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19af2603-74ce-4936-a22b-d41d02f7eb5d_1107x594.png 848w, https://substackcdn.com/image/fetch/$s_!XNcB!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19af2603-74ce-4936-a22b-d41d02f7eb5d_1107x594.png 1272w, https://substackcdn.com/image/fetch/$s_!XNcB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F19af2603-74ce-4936-a22b-d41d02f7eb5d_1107x594.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>AI-assisted coding became fast much earlier than it became predictable. That is exactly why spec-driven development is getting more attention now: instead of keeping requirements scattered across chat history, you turn them into artifacts the human and the assistant can both follow. <a href="https://openspec.dev">OpenSpec</a> is one of the tools built around that idea. It adds a lightweight workflow where you agree on the change first, generate planning artifacts, and then implement against them.</p><div><hr></div><h2>What is Spec-Driven Development?</h2><p><a href="https://en.wikipedia.org/wiki/Spec-driven_development">Spec-driven development</a>, or SDD, is a workflow where the specification becomes the source of truth before implementation starts. In <a href="https://openspec.dev">OpenSpec&#8217;s</a> wording, it helps you and your AI coding assistant agree on what to build before any code is written. Rather than relying on one long prompt, you break work into artifacts such as a proposal, specs, design, and tasks.</p><p>That matters even more in AI-heavy workflows. A good assistant can generate a lot of code quickly, but if the requirements are vague, the result is usually inconsistent, incomplete, or hard to evolve. SDD adds structure without forcing a huge process. The idea is simple: describe the change, refine the behavior, plan the work, then implement it against an agreed spec.</p><p>One of the nice ideas behind <a href="https://openspec.dev">OpenSpec</a> is that the workflow is not treated as rigid phase gates. Its docs explicitly frame the model as actions rather than locked phases, so you can create, implement, update, and archive as needed. That makes it fit better with real development, where requirements often change while implementation is already in progress.</p><blockquote><p>SDD birthplace is NASA at 1960s as a mix of formal developments and multiple specifications. So we all can feel like a Space Explorers while using it )</p></blockquote><div><hr></div><h2>What is OpenSpec?</h2><p><a href="https://openspec.dev">OpenSpec</a> is an open-source tool for spec-driven development aimed at AI coding assistants. The project describes itself as a lightweight spec layer that helps teams agree on what to build before code is written, keeps each change organized in its own folder, and works with more than 20 AI assistants through generated slash commands and skills.</p><blockquote><p>The primary goal of OpenSpec is to solve the problem of unpredictability and inconsistency that can arise when using Al tools for coding. Since Al tools are non-deterministic, they can struggle to consistently follow complex requirements. OpenSpec provides a structured approach that allows teams and Al assistants to agree on specifications before coding begins. This leads to more efficient development, fewer errors, and improved code quality</p></blockquote><p>After initialization, <a href="https://openspec.dev">OpenSpec</a> generates instructions that compatible assistants can auto-detect. In its docs, that means skills in locations like <code>.claude/skills/</code>, plus a project config file at <code>openspec/config.yaml</code> if you choose to create one. The default schema is spec-driven, and the standard artifact set includes proposal, specs, design, and tasks.</p><p>A particularly useful concept in <a href="https://openspec.dev">OpenSpec</a> is the idea of delta specs. Instead of rewriting the whole product spec every time, you describe what is changing. The docs present this with sections such as <code>ADDED</code> <code>Requirements</code> and <code>MODIFIED</code> <code>Requirements</code>, which makes OpenSpec more practical for brownfield projects where the codebase already exists. We will get back to this in the next chapters.</p><div><hr></div><h2>How to install OpenSpec</h2><p>OpenSpec requires <strong>Node.js 20.19.0 or higher</strong>. The main <code>README</code> and installation docs show the global package install via npm, plus alternatives for pnpm, yarn, bun, and nix.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;59b3a26e-bed3-46c8-b51d-3432d927de15&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">node --version
npm install -g @fission-ai/openspec@latest</code></pre></div><p>If you use another package manager, the official docs also list these options:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;b42bbb7f-3d5b-4b79-a0a3-4f2976c54226&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">pnpm add -g @fission-ai/openspec@latest
yarn global add @fission-ai/openspec@latest
bun add -g @fission-ai/openspec@latest</code></pre></div><div><hr></div><h2>How to use OpenSpec</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!alsz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1146b24-b3ac-4dec-a16e-6c701cbda0c1_1264x642.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!alsz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1146b24-b3ac-4dec-a16e-6c701cbda0c1_1264x642.png 424w, https://substackcdn.com/image/fetch/$s_!alsz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1146b24-b3ac-4dec-a16e-6c701cbda0c1_1264x642.png 848w, https://substackcdn.com/image/fetch/$s_!alsz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1146b24-b3ac-4dec-a16e-6c701cbda0c1_1264x642.png 1272w, https://substackcdn.com/image/fetch/$s_!alsz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1146b24-b3ac-4dec-a16e-6c701cbda0c1_1264x642.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!alsz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1146b24-b3ac-4dec-a16e-6c701cbda0c1_1264x642.png" width="1264" height="642" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a1146b24-b3ac-4dec-a16e-6c701cbda0c1_1264x642.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:642,&quot;width&quot;:1264,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1033100,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/193041491?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5abfafce-1b4b-4b0b-80db-7f09546ff8d6_1536x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!alsz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1146b24-b3ac-4dec-a16e-6c701cbda0c1_1264x642.png 424w, https://substackcdn.com/image/fetch/$s_!alsz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1146b24-b3ac-4dec-a16e-6c701cbda0c1_1264x642.png 848w, https://substackcdn.com/image/fetch/$s_!alsz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1146b24-b3ac-4dec-a16e-6c701cbda0c1_1264x642.png 1272w, https://substackcdn.com/image/fetch/$s_!alsz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1146b24-b3ac-4dec-a16e-6c701cbda0c1_1264x642.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The quick-start flow is straightforward. Install the CLI, move into your project, run <code>openspec init</code>, and then start the workflow from your AI assistant with <code>/opsx:propose &lt;what-you-want-to-build&gt;</code>. The default quick path in the docs is propose &#8594; apply &#8594; archive.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;cc1d6ae2-ce36-4637-afda-1011ae3956dc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">cd your-project
openspec init</code></pre></div><p>This command will create the following directory structure in your project:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;f294e2f1-5c73-4b89-aa67-3e2f3c49ce2b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">openspec/
&#9500;&#9472;&#9472; specs/ # Source of truth (your system's behavior)
&#9474; &#9492;&#9472;&#9472; &lt;domain&gt;/
&#9474; &#9492;&#9472;&#9472; spec.md
&#9500;&#9472;&#9472; changes/ # Proposed updates (one folder per change)
&#9474; &#9492;&#9472;&#9472; &lt;change-name&gt;/
&#9474; &#9500;&#9472;&#9472; proposal.md
&#9474; &#9500;&#9472;&#9472; design.md
&#9474; &#9500;&#9472;&#9472; tasks.md
&#9474; &#9492;&#9472;&#9472; specs/ # Delta specs (what's changing)
&#9474; &#9492;&#9472;&#9472; &lt;domain&gt;/
&#9474; &#9492;&#9472;&#9472; spec.md
&#9492;&#9472;&#9472; config.yaml # Project configuration (optional)</code></pre></div><p>Two key directories:</p><ul><li><p><code>specs/</code> - Source of Truth. These specifications describe the current behavior of your stem. They are organized by domain (e.g., specs/auth/, specs/payments/).</p></li><li><p><code>changes/</code> - Proposed Changes. Each change gets its own folder with all related artifacts. When a change is completed, its specifications are merged into the main</p></li></ul><h3>OpenSpec Artifacts</h3><p>Each change folder contains artifacts that guide the work:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!KFVO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2126c5bc-4b44-4500-9303-bd9b4b028936_740x255.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!KFVO!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2126c5bc-4b44-4500-9303-bd9b4b028936_740x255.png 424w, https://substackcdn.com/image/fetch/$s_!KFVO!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2126c5bc-4b44-4500-9303-bd9b4b028936_740x255.png 848w, https://substackcdn.com/image/fetch/$s_!KFVO!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2126c5bc-4b44-4500-9303-bd9b4b028936_740x255.png 1272w, https://substackcdn.com/image/fetch/$s_!KFVO!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2126c5bc-4b44-4500-9303-bd9b4b028936_740x255.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!KFVO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2126c5bc-4b44-4500-9303-bd9b4b028936_740x255.png" width="606" height="208.82432432432432" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2126c5bc-4b44-4500-9303-bd9b4b028936_740x255.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:255,&quot;width&quot;:740,&quot;resizeWidth&quot;:606,&quot;bytes&quot;:35893,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/193041491?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2126c5bc-4b44-4500-9303-bd9b4b028936_740x255.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!KFVO!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2126c5bc-4b44-4500-9303-bd9b4b028936_740x255.png 424w, https://substackcdn.com/image/fetch/$s_!KFVO!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2126c5bc-4b44-4500-9303-bd9b4b028936_740x255.png 848w, https://substackcdn.com/image/fetch/$s_!KFVO!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2126c5bc-4b44-4500-9303-bd9b4b028936_740x255.png 1272w, https://substackcdn.com/image/fetch/$s_!KFVO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2126c5bc-4b44-4500-9303-bd9b4b028936_740x255.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">Main artifacts</figcaption></figure></div><p>Artifacts build on each other:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!JHbv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc49635f3-f4d3-487e-be14-0b1b494a9b00_740x166.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!JHbv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc49635f3-f4d3-487e-be14-0b1b494a9b00_740x166.png 424w, https://substackcdn.com/image/fetch/$s_!JHbv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc49635f3-f4d3-487e-be14-0b1b494a9b00_740x166.png 848w, https://substackcdn.com/image/fetch/$s_!JHbv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc49635f3-f4d3-487e-be14-0b1b494a9b00_740x166.png 1272w, https://substackcdn.com/image/fetch/$s_!JHbv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc49635f3-f4d3-487e-be14-0b1b494a9b00_740x166.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!JHbv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc49635f3-f4d3-487e-be14-0b1b494a9b00_740x166.png" width="632" height="141.77297297297298" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c49635f3-f4d3-487e-be14-0b1b494a9b00_740x166.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:166,&quot;width&quot;:740,&quot;resizeWidth&quot;:632,&quot;bytes&quot;:16725,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/193041491?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc49635f3-f4d3-487e-be14-0b1b494a9b00_740x166.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!JHbv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc49635f3-f4d3-487e-be14-0b1b494a9b00_740x166.png 424w, https://substackcdn.com/image/fetch/$s_!JHbv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc49635f3-f4d3-487e-be14-0b1b494a9b00_740x166.png 848w, https://substackcdn.com/image/fetch/$s_!JHbv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc49635f3-f4d3-487e-be14-0b1b494a9b00_740x166.png 1272w, https://substackcdn.com/image/fetch/$s_!JHbv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc49635f3-f4d3-487e-be14-0b1b494a9b00_740x166.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Then in your AI assistant (Claude, in example):</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;4b4cc3ca-d340-41bc-a61e-6784d1fd6c3f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">/opsx:propose Add dark theme support to the iOS app</code></pre></div><p>If you want a more expanded workflow, <a href="https://openspec.dev">OpenSpec</a> also supports commands such as <code>/opsx:new, /opsx:continue, /opsx:ff, /opsx:verify, /opsx:sync, /opsx:bulk-archive, and /opsx:onboard</code>. The docs say these are enabled by selecting a workflow profile with openspec config profile and then applying it with openspec update.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;3a62b81b-4c7f-4012-a375-3f8242c2acb1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">openspec config profile
openspec update</code></pre></div><p><a href="https://openspec.dev">OpenSpec</a> can also inject project context and rules through <code>openspec/config.yaml</code>. The official docs show fields like schema, context, and per-artifact rules, which is useful when you want the AI to consistently follow your stack, style, testing, or architecture expectations.</p><p>Example config:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;83c054c5-5f14-4fe8-8b89-2e3b5522d5b1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">schema: spec-driven

context: |
  Platform: iOS
  Language: Swift 6
  UI: SwiftUI
  Architecture: MVVM
  Testing: XCTest
  Style: Keep features modular and accessibility-friendly

rules:
  proposal:
    - Mention user-visible impact
    - Mention rollback strategy if theme breaks visual contrast
  specs:
    - Use Given/When/Then scenarios
  design:
    - Note persistence approach and affected views
  tasks:
    - Keep tasks small and implementation-ready</code></pre></div><div><hr></div><h2>What is Delta and why is it needed?</h2><p>One of the most useful <a href="https://openspec.dev">OpenSpec</a> ideas is the <strong>delta</strong>. In simple terms, a delta is <strong>the change itself</strong>, not the entire product spec rewritten from scratch. OpenSpec&#8217;s concepts docs define delta specs as specifications for what is being <strong>added, modified, or removed</strong>.</p><p>That matters because most real work happens in an existing codebase. You are usually not writing a greenfield product spec from zero. You are adding dark theme, changing onboarding, refining analytics, or replacing a legacy flow. Rewriting the full spec every time would add noise and make reviews harder. Delta specs keep the focus on what changed.</p><p>This is also why delta works well with AI assistants. Instead of telling the model to reinterpret the whole product, you give it a precise target: implement this difference. That makes output easier to review, easier to evolve, and less likely to drift away from the original intent. This last point is an inference from OpenSpec&#8217;s artifact model and workflow design, rather than a direct quote from the docs.</p><p>A simple way to think about it:</p><ul><li><p><strong>full spec</strong> = what the product is</p></li><li><p><strong>delta spec</strong> = what this feature changes</p></li></ul><p>A typical delta can contain sections like:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;aa3a189f-6b87-484a-a2b1-1989c71675cb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">## ADDED Requirements
### Requirement: Manual Theme Override

## MODIFIED Requirements
### Requirement: Accessible Theme Colors

## REMOVED Requirements
### Requirement: Legacy Fixed Light Theme</code></pre></div><p>So if your app already exists and you want to add dark mode, the delta does not describe the whole app. It describes only the new or changed behavior related to theme support. That is what makes spec updates manageable over time.</p><div><hr></div><h2>Example: iOS spec to add Dark Theme</h2><p>Here is the kind of change request <a href="https://openspec.dev">OpenSpec</a> fits well. Imagine an existing SwiftUI app that currently supports only a light theme, and now you want to add dark mode properly rather than as a quick visual patch.</p><p>A strong OpenSpec workflow would usually begin with a proposal, then move into specs, design notes, and tasks. OpenSpec&#8217;s docs position these as the standard artifacts in the default schema, and their examples recommend small, structured tasks and Given/When/Then-style scenarios for requirements.</p><h3>1. Proposal</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;markdown&quot;,&quot;nodeId&quot;:&quot;4a561c86-6052-43de-8358-28da8dd75d3c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-markdown"># Proposal: Add Dark Theme Support

## Summary
Add full dark theme support to the iOS app so users can use the interface comfortably in low-light environments and align the app with modern iOS expectations.

## Motivation
The app currently uses a light-only visual style. This creates poor visual comfort at night, makes the product feel less native on iOS, and limits accessibility for users who prefer darker interfaces.

## Goals
- Support system light/dark appearance
- Allow optional manual theme override in Settings
- Persist user preference between launches
- Update key screens and reusable components

## Non-Goals
- Rebuild the design system from scratch
- Add seasonal or branded themes
- Change unrelated layout or navigation behavior

## Risks
- Some screens may still use hard-coded colors
- Charts, overlays, and custom backgrounds may need separate tuning
- Contrast issues may appear on older screens

## Success Criteria
- Main user flows look correct in light and dark mode
- Theme switches without broken contrast
- Manual selection persists after relaunch</code></pre></div><h3>2. Specs</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;markdown&quot;,&quot;nodeId&quot;:&quot;de03269c-d134-44c4-844c-8dc8edc03bff&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-markdown"># Delta for Theme

## ADDED Requirements

### Requirement: System Theme Support
The application MUST support both light and dark appearance modes.

#### Scenario: Follow system appearance
- GIVEN the user has not selected a manual theme override
- WHEN the iOS system appearance changes between light and dark
- THEN the application updates its appearance automatically

### Requirement: Manual Theme Override
The application MUST allow the user to choose Light, Dark, or System in Settings.

#### Scenario: Select dark theme manually
- GIVEN the user opens Settings
- WHEN the user selects Dark theme
- THEN the application immediately applies dark appearance
- AND the selection persists across app launches

#### Scenario: Revert to system theme
- GIVEN the user previously selected a manual theme
- WHEN the user selects System
- THEN the application follows the current iOS appearance again

### Requirement: Accessible Theme Colors
The application MUST use theme-aware colors that preserve readable contrast in both appearances.

#### Scenario: Readability in dark mode
- GIVEN the app is displayed in dark mode
- WHEN the user opens any primary screen
- THEN text, icons, separators, and controls remain clearly readable</code></pre></div><h3>3. Design</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;markdown&quot;,&quot;nodeId&quot;:&quot;018723fe-2e7d-461b-87d8-14c1fce9a7d2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-markdown"># Design: Dark Theme Support

## Overview
Implement a centralized theme layer for SwiftUI that supports:
- system appearance
- manual override
- persistent storage
- theme-aware semantic colors

## Architecture
- Add `AppTheme` enum with cases: `system`, `light`, `dark`
- Add `ThemeStore` as an observable state holder
- Persist selected theme using `@AppStorage`
- Inject `ThemeStore` at the app root
- Map theme selection to `preferredColorScheme`

## UI Impact
Affected areas:
- app root scene
- settings screen
- reusable buttons
- cards and list rows
- onboarding screens
- charts and branded surfaces

## Edge Cases
- legacy UIKit-hosted screens may ignore SwiftUI preference
- hard-coded `Color.white` and `Color.black` usages must be replaced
- image assets may require dark variants</code></pre></div><h3>4. Tasks</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;f0e31b15-b57c-42a3-b1e8-1b32b840130d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext"># Tasks

## 1. Theme Infrastructure
- [ ] 1.1 Create `AppTheme` enum with `system`, `light`, `dark`
- [ ] 1.2 Create `ThemeStore` observable object
- [ ] 1.3 Persist selected theme with `@AppStorage`
- [ ] 1.4 Inject theme store into the app entry point

## 2. App Integration
- [ ] 2.1 Apply `preferredColorScheme` at root level
- [ ] 2.2 Add theme selector to Settings
- [ ] 2.3 Ensure changes update UI immediately

## 3. UI Migration
- [ ] 3.1 Replace hard-coded colors with semantic colors
- [ ] 3.2 Audit reusable components
- [ ] 3.3 Update custom backgrounds and overlays
- [ ] 3.4 Test charts and illustrations in dark mode

## 4. Validation
- [ ] 4.1 Test launch persistence
- [ ] 4.2 Test system appearance switching
- [ ] 4.3 Check accessibility contrast on key screens</code></pre></div><p>[img: iOS dark theme concept illustration. iPhone screens transitioning from light mode to dark mode, with a settings panel and palette tokens floating around. Elegant product art, square, no text.]</p><div><hr></div><h2>Example Implementation Prompt</h2><p>Once the planning artifacts are in place, you can move to implementation. In OpenSpec&#8217;s workflow, that is the role of<code> /opsx:apply</code>, which implements tasks while allowing artifacts to be updated as needed.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;e1e5f08f-0cce-4736-a7b9-e7f1811b8bf7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">/opsx:apply Implement the dark theme change for the SwiftUI iOS app.
Respect the existing architecture.
Use semantic colors where possible.
Add manual theme selection in Settings.
Persist the selected option.
Flag any screens that still depend on hard-coded colors.</code></pre></div><h3>Further edit: refining the spec after first implementation</h3><p>This is where OpenSpec becomes more useful than a single big prompt. After the first pass, you often discover missing details. Maybe the theme works, but charts still look wrong, onboarding illustrations are too bright, or a UIKit bridge ignores the chosen mode. OpenSpec&#8217;s docs explicitly support updating artifacts during work, and its delta-spec model is built for describing those changes rather than rewriting everything.</p><p>For example, after reviewing the first implementation, you might extend the spec like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;markdown&quot;,&quot;nodeId&quot;:&quot;2e1a1766-dce5-42f7-98fb-04312f81dc70&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-markdown">## MODIFIED Requirements

### Requirement: Accessible Theme Colors
The application MUST use theme-aware colors that preserve readable contrast in both appearances, including charts, onboarding graphics, and empty states.

#### Scenario: Charts in dark mode
- GIVEN the app is displayed in dark mode
- WHEN the user opens an analytics or chart-based screen
- THEN chart axes, grid lines, labels, and highlighted values remain readable
- AND color choices preserve sufficient contrast against the background</code></pre></div><p>And you could add follow-up tasks:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;143484d7-43a8-42be-809e-ffc40b4c7c1f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">## 5. Dark Mode Polish
- [ ] 5.1 Tune chart palette for dark backgrounds
- [ ] 5.2 Add dark-compatible illustration assets where needed
- [ ] 5.3 Audit UIKit-hosted views for color-scheme mismatch
- [ ] 5.4 Re-test Settings, onboarding, and analytics screens</code></pre></div><p>That iterative loop is probably the strongest part of the <a href="https://openspec.dev">OpenSpec</a> approach. You do not just tell the assistant &#8220;fix dark mode.&#8221; You update the source of truth, then implement again from a better artifact set.</p><div><hr></div><h2>Why OpenSpec is interesting for iOS work</h2><p>For iOS development, <a href="https://openspec.dev">OpenSpec</a> feels especially useful in feature work that is bigger than a one-file change but smaller than a full RFC process. Dark theme, onboarding redesigns, analytics instrumentation, subscription flows, paywall experiments, or widget updates all benefit from a shared definition of behavior before the assistant starts touching code.</p><p>It also fits the reality of app development better than prompt-only coding. Mobile work usually includes UI states, accessibility, persistence, migration concerns, asset variants, platform conventions, and sometimes legacy UIKit or extension targets. A structured spec makes those details much easier to preserve across multiple AI-assisted implementation steps.</p><h3>Competitors</h3><p>Honestly, there is a <a href="https://kiro.dev">Kiro</a>. It positions itself as an AI development environment built around spec-driven development, custom agents, and structured workflows instead of prompt-only coding. The most interesting part is the emphasis on moving from fast prototypes to production work with executable specs, steering, and IDE plus CLI support. But it has a credits system.</p><div><hr></div><h2>Final thoughts</h2><p>Spec-driven development is really a response to a new problem: code generation got fast, but reliable intent transfer did not. <a href="https://openspec.dev">OpenSpec</a> addresses that gap with a lightweight workflow built around proposals, specs, design, and tasks, plus a set of assistant-friendly commands and project configuration. It is not about replacing coding. It is about making AI-generated coding more intentional, reviewable, and repeatable.</p><p>For an iOS team or even a solo developer, that can be enough process to improve predictability without turning every feature into a documentation project.</p><div><hr></div><h2>References</h2><ul><li><p><a href="https://github.com/Fission-AI/OpenSpec">Fission-AI OpenSpec GitHub repository and README</a></p></li><li><p><a href="https://github.com/Fission-AI/OpenSpec/blob/main/docs/getting-started.md">OpenSpec Getting Started docs</a></p></li><li><p><a href="https://github.com/Fission-AI/OpenSpec/blob/main/docs/opsx.md">OpenSpec opsx workflow docs</a></p></li><li><p><a href="https://github.com/Fission-AI/OpenSpec/blob/main/docs/concepts.md">OpenSpec concepts docs, including delta specs and task guidance</a></p></li><li><p><a href="https://github.com/Fission-AI/OpenSpec/blob/main/docs/installation.md?utm_source=chatgpt.com">OpenSpec installation docs</a></p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h2>One More thing&#8230;</h2><p>Through years of mentoring, I&#8217;ve built a small bookshelf of titles that are worth reading &#8212; or at least being aware of. The Grokking series from Manning Publishing is one of them. The style, the language, and the illustrations all combine to explain complex topics in an easy and engaging way.</p><p>You&#8217;ll find yourself wanting to learn more about things that once felt intimidating or difficult to understand. One of the latest additions is <em>Grokking Data Structures</em> by Marcello La Rocca.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!vnCt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf5af70d-3a1d-4356-837f-5261a6d0e4f9_360x451.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vnCt!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf5af70d-3a1d-4356-837f-5261a6d0e4f9_360x451.jpeg 424w, https://substackcdn.com/image/fetch/$s_!vnCt!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf5af70d-3a1d-4356-837f-5261a6d0e4f9_360x451.jpeg 848w, https://substackcdn.com/image/fetch/$s_!vnCt!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf5af70d-3a1d-4356-837f-5261a6d0e4f9_360x451.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!vnCt!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf5af70d-3a1d-4356-837f-5261a6d0e4f9_360x451.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!vnCt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf5af70d-3a1d-4356-837f-5261a6d0e4f9_360x451.jpeg" width="308" height="385.85555555555555" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bf5af70d-3a1d-4356-837f-5261a6d0e4f9_360x451.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:451,&quot;width&quot;:360,&quot;resizeWidth&quot;:308,&quot;bytes&quot;:70581,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/193041491?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf5af70d-3a1d-4356-837f-5261a6d0e4f9_360x451.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!vnCt!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf5af70d-3a1d-4356-837f-5261a6d0e4f9_360x451.jpeg 424w, https://substackcdn.com/image/fetch/$s_!vnCt!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf5af70d-3a1d-4356-837f-5261a6d0e4f9_360x451.jpeg 848w, https://substackcdn.com/image/fetch/$s_!vnCt!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf5af70d-3a1d-4356-837f-5261a6d0e4f9_360x451.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!vnCt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf5af70d-3a1d-4356-837f-5261a6d0e4f9_360x451.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><a href="https://www.manning.com/books/grokking-data-structures?utm_source=AG-DEV&amp;utm_medium=affiliate&amp;utm_campaign=book_larocca3_grokking_9_28_23&amp;a_aid=AG-DEV&amp;a_bid=89b98f8d&amp;chan=sbs">Sharing the link with you</a>.</p>]]></content:encoded></item><item><title><![CDATA[SwiftUI: Charts Axis Scale Promo]]></title><description><![CDATA[Scaling done right]]></description><link>https://antongubarenko.substack.com/p/swiftui-charts-axis-scale-promo</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/swiftui-charts-axis-scale-promo</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Mon, 30 Mar 2026 08:22:04 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/60a0c37f-bbbe-4a67-b095-20457ddbd1c1_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Happy to share a new article with you.</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;46bd0400-fab2-4a39-a5ae-5335cf833b76&quot;,&quot;caption&quot;:&quot;We&#8217;ve all had this feeling at some point in a developer&#8217;s life. You picked the right architecture, managed navigation, fixed isolation. The only thing left is a small specification detail: display data on a chart exactly as described and according to Figma. You try one modifier, then another, then different parameters, and it still doesn&#8217;t work. What is&#8230;&quot;,&quot;cta&quot;:&quot;Read full story&quot;,&quot;showBylines&quot;:true,&quot;size&quot;:&quot;lg&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;SwiftUI: Charts Axis Scale&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:123970311,&quot;name&quot;:&quot;Anton Gubarenko&quot;,&quot;bio&quot;:&quot;&#128075; I am a Senior iOS Engineer / Contractor with 10+ &#119858;&#119838;&#119834;&#119851;&#119852; &#119848;&#119839; &#119838;&#119857;&#119849;&#119838;&#119851;&#119842;&#119838;&#119847;&#119836;&#119838;, my objective is to assist organizations worldwide in conceptualizing and executing their mobile strategies.&quot;,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1eb672de-f6cd-47a2-98b2-6de3f5be6ffe_2316x3088.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null}],&quot;post_date&quot;:&quot;2026-03-30T08:20:12.489Z&quot;,&quot;cover_image&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7a771cb9-2b19-432e-99a9-6ab5b8428b7e_1024x1024.png&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://antongubarenko.substack.com/p/swiftui-charts-axis-scale&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:192009305,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:0,&quot;comment_count&quot;:0,&quot;publication_id&quot;:2928350,&quot;publication_name&quot;:&quot;Anton&#8217;s Substack&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!mzA2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1eb672de-f6cd-47a2-98b2-6de3f5be6ffe_2316x3088.jpeg&quot;,&quot;belowTheFold&quot;:false,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[SwiftUI: Charts Axis Scale]]></title><description><![CDATA[Scaling done right]]></description><link>https://antongubarenko.substack.com/p/swiftui-charts-axis-scale</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/swiftui-charts-axis-scale</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Mon, 30 Mar 2026 08:20:12 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/7a771cb9-2b19-432e-99a9-6ab5b8428b7e_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>We&#8217;ve all had this feeling at some point in a developer&#8217;s life. You picked the right architecture, managed navigation, fixed isolation. The only thing left is a small specification detail: display data on a chart exactly as described and according to Figma. You try one modifier, then another, then different parameters, and it still doesn&#8217;t work. What is domain? Why is range not working? There should be no barriers to building a nice chart.</p><p>Here is where <code>chartXScale(range:type:)</code> becomes interesting. Apple describes it as the modifier that configures &#8220;the range of x positions that correspond to the scale domain.&#8221; By default, that range is determined by the size of the plot area. </p><blockquote><p>The same applies to <code>chartYScale(range:type:)</code>. No extra diff spotted.</p></blockquote><p>In other words, this modifier is not about changing your data. It is about changing how the x-axis uses the available horizontal space.</p><h2>Why Would You Use It?</h2><p>Most charts look fine with the default x-scale behavior. But sometimes the chart feels just a little off:</p><p>&#9;&#8226;&#9;the first bar is too close to the leading edge</p><p>&#9;&#8226;&#9;the last point looks glued to the trailing border</p><p>&#9;&#8226;&#9;the whole plot feels cramped even though the data itself is correct</p><p>That is usually not a domain problem. It is a range problem.</p><p>A useful mental model is this:</p><p>&#9;&#8226;&#9;<strong>domain</strong> = what values exist on the axis</p><p>&#9;&#8226;&#9;<strong>range</strong> = where those values are drawn in the plot area</p><p>Apple&#8217;s chart scale docs make exactly that separation: domain is about possible data values, while range is about the axis positions used to render them.</p><div><hr></div><h2>Basic Chart</h2><p>Let&#8217;s start with a simple bar chart:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;73dc2bc2-3e83-450a-bf0b-1bb657312552&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">import SwiftUI
import Charts

struct SalesPoint: Identifiable {
    let id = UUID()
    let month: String
    let value: Int
}

struct ChartRangeDemoView: View {
    let data: [SalesPoint] = [
        .init(month: "Jan", value: 12),
        .init(month: "Feb", value: 19),
        .init(month: "Mar", value: 7),
        .init(month: "Apr", value: 15),
        .init(month: "May", value: 23)
    ]

    var body: some View {
        Chart(data) { item in
            BarMark(
                x: .value("Month", item.month),
                y: .value("Sales", item.value)
            )
        }
        .frame(height: 220)
        .padding()
    }
}</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lAtb!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F924ade55-ea74-410f-aaba-614e00ed575b_774x496.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lAtb!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F924ade55-ea74-410f-aaba-614e00ed575b_774x496.png 424w, https://substackcdn.com/image/fetch/$s_!lAtb!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F924ade55-ea74-410f-aaba-614e00ed575b_774x496.png 848w, https://substackcdn.com/image/fetch/$s_!lAtb!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F924ade55-ea74-410f-aaba-614e00ed575b_774x496.png 1272w, https://substackcdn.com/image/fetch/$s_!lAtb!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F924ade55-ea74-410f-aaba-614e00ed575b_774x496.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lAtb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F924ade55-ea74-410f-aaba-614e00ed575b_774x496.png" width="512" height="328.1033591731266" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/924ade55-ea74-410f-aaba-614e00ed575b_774x496.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:496,&quot;width&quot;:774,&quot;resizeWidth&quot;:512,&quot;bytes&quot;:37815,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/192009305?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F924ade55-ea74-410f-aaba-614e00ed575b_774x496.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!lAtb!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F924ade55-ea74-410f-aaba-614e00ed575b_774x496.png 424w, https://substackcdn.com/image/fetch/$s_!lAtb!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F924ade55-ea74-410f-aaba-614e00ed575b_774x496.png 848w, https://substackcdn.com/image/fetch/$s_!lAtb!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F924ade55-ea74-410f-aaba-614e00ed575b_774x496.png 1272w, https://substackcdn.com/image/fetch/$s_!lAtb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F924ade55-ea74-410f-aaba-614e00ed575b_774x496.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Basic Bar Chart</figcaption></figure></div><p>With no explicit range configuration, Swift Charts uses the plot area dimension automatically. That is the default behavior Apple documents for <code>chartXScale(range:type:)</code>.</p><div><hr></div><h2>The Most Practical Use of <code>plotDimension</code></h2><p>The easiest and most useful version is applying padding to the plotting dimension.</p><p>Apple documents <code>plotDimension(padding:)</code> as &#8220;a scale range that fills the plot area with the given padding value at start and end.&#8221;</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;57545bd4-25d9-4151-8f9f-6f18393e4b9d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Chart(data) { item in
    BarMark(
        x: .value("Month", item.month),
        y: .value("Sales", item.value)
    )
}
.chartXScale(
    range: .plotDimension(padding: 20)
)
.frame(height: 220)
.padding()</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!RThm!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a5a342c-ccad-40df-bea7-3c508a49740d_774x496.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!RThm!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a5a342c-ccad-40df-bea7-3c508a49740d_774x496.png 424w, https://substackcdn.com/image/fetch/$s_!RThm!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a5a342c-ccad-40df-bea7-3c508a49740d_774x496.png 848w, https://substackcdn.com/image/fetch/$s_!RThm!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a5a342c-ccad-40df-bea7-3c508a49740d_774x496.png 1272w, https://substackcdn.com/image/fetch/$s_!RThm!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a5a342c-ccad-40df-bea7-3c508a49740d_774x496.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!RThm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a5a342c-ccad-40df-bea7-3c508a49740d_774x496.png" width="511" height="327.4625322997416" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5a5a342c-ccad-40df-bea7-3c508a49740d_774x496.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:496,&quot;width&quot;:774,&quot;resizeWidth&quot;:511,&quot;bytes&quot;:38022,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/192009305?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a5a342c-ccad-40df-bea7-3c508a49740d_774x496.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!RThm!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a5a342c-ccad-40df-bea7-3c508a49740d_774x496.png 424w, https://substackcdn.com/image/fetch/$s_!RThm!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a5a342c-ccad-40df-bea7-3c508a49740d_774x496.png 848w, https://substackcdn.com/image/fetch/$s_!RThm!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a5a342c-ccad-40df-bea7-3c508a49740d_774x496.png 1272w, https://substackcdn.com/image/fetch/$s_!RThm!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a5a342c-ccad-40df-bea7-3c508a49740d_774x496.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">range: .plotDimension(padding: 20)</figcaption></figure></div><p>This is the version I&#8217;d reach for first. It is simple, visual, and immediately improves charts that feel too tight near the edges.</p><div><hr></div><h2>Different Padding at the Start and End</h2><p>Sometimes equal padding is not enough. You may want the chart to start tighter on the left, but leave a bit more breathing room on the right for a label, annotation, or simply for better visual balance.</p><p>Apple also provides a <code>PositionScaleRange</code> option that fills the plot area with different padding values at the start and end.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;e765413d-8eec-4ffd-af1c-4d1a0167e4c1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">Chart(data) { item in
    BarMark(
        x: .value("Month", item.month),
        y: .value("Sales", item.value)
    )
}
.chartXScale(
    range: .plotDimension(startPadding: 0, endPadding: 28)
)
.frame(height: 220)
.padding()</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!juzx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd66d3dd4-8a87-40c4-bfb1-052a1341a8de_774x496.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!juzx!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd66d3dd4-8a87-40c4-bfb1-052a1341a8de_774x496.png 424w, https://substackcdn.com/image/fetch/$s_!juzx!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd66d3dd4-8a87-40c4-bfb1-052a1341a8de_774x496.png 848w, https://substackcdn.com/image/fetch/$s_!juzx!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd66d3dd4-8a87-40c4-bfb1-052a1341a8de_774x496.png 1272w, https://substackcdn.com/image/fetch/$s_!juzx!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd66d3dd4-8a87-40c4-bfb1-052a1341a8de_774x496.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!juzx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd66d3dd4-8a87-40c4-bfb1-052a1341a8de_774x496.png" width="512" height="328.1033591731266" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d66d3dd4-8a87-40c4-bfb1-052a1341a8de_774x496.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:496,&quot;width&quot;:774,&quot;resizeWidth&quot;:512,&quot;bytes&quot;:38991,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/192009305?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd66d3dd4-8a87-40c4-bfb1-052a1341a8de_774x496.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!juzx!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd66d3dd4-8a87-40c4-bfb1-052a1341a8de_774x496.png 424w, https://substackcdn.com/image/fetch/$s_!juzx!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd66d3dd4-8a87-40c4-bfb1-052a1341a8de_774x496.png 848w, https://substackcdn.com/image/fetch/$s_!juzx!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd66d3dd4-8a87-40c4-bfb1-052a1341a8de_774x496.png 1272w, https://substackcdn.com/image/fetch/$s_!juzx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd66d3dd4-8a87-40c4-bfb1-052a1341a8de_774x496.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">range: .plotDimension(startPadding: 0, endPadding: 28)</figcaption></figure></div><p>This is where the modifier starts feeling less like &#8220;some axis API&#8221; and more like a visual tuning tool.</p><div><hr></div><h2>Example with an Actual Range</h2><p>The interesting part is that <code>chartXScale(range:type:)</code> is not only for categorical data like month names. It also applies when your x-axis contains real continuous values such as dates or numbers.</p><p>Here is a date-based line chart:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;43a354ca-129c-41ab-8653-0f3ac77e5711&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">import SwiftUI
import Charts

struct TemperaturePoint: Identifiable {
    let id = UUID()
    let day: String
    let temperature: Double
}

struct TemperatureChartView: View {
    let data: [TemperaturePoint] = [
        .init(day: "Mon", temperature: 18),
        .init(day: "Tue", temperature: 21),
        .init(day: "Wed", temperature: 19),
        .init(day: "Thu", temperature: 24),
        .init(day: "Fri", temperature: 22)
    ]

    var body: some View {
        Chart(data) { item in
            LineMark(
                x: .value("Day", item.day),
                y: .value("Temperature", item.temperature)
            )

            PointMark(
                x: .value("Day", item.day),
                y: .value("Temperature", item.temperature)
            )
        }
        .chartXScale(
            range: .plotDimension(startPadding: 12, endPadding: 12),
            type: .category
        )
        .frame(height: 220)
        .padding()
    }
}</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!kXzc!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03966bfc-c3b6-4e88-ac41-c838b8357412_774x496.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!kXzc!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03966bfc-c3b6-4e88-ac41-c838b8357412_774x496.png 424w, https://substackcdn.com/image/fetch/$s_!kXzc!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03966bfc-c3b6-4e88-ac41-c838b8357412_774x496.png 848w, https://substackcdn.com/image/fetch/$s_!kXzc!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03966bfc-c3b6-4e88-ac41-c838b8357412_774x496.png 1272w, https://substackcdn.com/image/fetch/$s_!kXzc!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03966bfc-c3b6-4e88-ac41-c838b8357412_774x496.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!kXzc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03966bfc-c3b6-4e88-ac41-c838b8357412_774x496.png" width="512" height="328.1033591731266" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/03966bfc-c3b6-4e88-ac41-c838b8357412_774x496.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:496,&quot;width&quot;:774,&quot;resizeWidth&quot;:512,&quot;bytes&quot;:41187,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/192009305?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03966bfc-c3b6-4e88-ac41-c838b8357412_774x496.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!kXzc!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03966bfc-c3b6-4e88-ac41-c838b8357412_774x496.png 424w, https://substackcdn.com/image/fetch/$s_!kXzc!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03966bfc-c3b6-4e88-ac41-c838b8357412_774x496.png 848w, https://substackcdn.com/image/fetch/$s_!kXzc!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03966bfc-c3b6-4e88-ac41-c838b8357412_774x496.png 1272w, https://substackcdn.com/image/fetch/$s_!kXzc!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F03966bfc-c3b6-4e88-ac41-c838b8357412_774x496.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Basic Line Chart</figcaption></figure></div><p>Even though the x-axis is now based on Date, the job of range is still the same: define how the domain maps into the horizontal plot area. Apple&#8217;s documentation for the modifier does not limit it to categorical charts; it defines the range generally as the x positions that correspond to the scale domain.</p><div><hr></div><h2>What is Type?</h2><p>This is the part that often feels vague at first.</p><p>Apple&#8217;s <code>ScaleType</code> documentation describes it as &#8220;the ways you can scale the domain or range of a plot.&#8221;</p><p>That means the type parameter tells Swift Charts <strong>how to interpret the scale</strong>, while range tells it <strong>how much horizontal plotting space to use and how to distribute it</strong>.</p><p>A typical explicit example looks like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;1d19038d-24ec-46e9-af0f-1585ae45c0ce&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">.chartXScale(
    range: .plotDimension(startPadding: 12, endPadding: 12),
    type: .category
)</code></pre></div><p>If your x-axis values are strings like weekdays, month labels, product names, or enum-backed display values, .category makes the intent very clear. Apple exposes category as one of the available scale types.</p><div><hr></div><h2>Why Might Type be Needed?</h2><p>In many charts, you do not have to set type manually because Swift Charts can infer the right behavior from the values you pass in. But there are still good reasons to use it.</p><p><strong>1. It makes the axis behavior explicit</strong></p><p>If you are using strings as x-axis values, they are typically meant to be treated as categories, not as a continuous scale. Writing type: <code>.category</code> makes that obvious in code. That follows directly from Apple&#8217;s scale model and the presence of <code>.category</code> in <code>ScaleType</code>.</p><p><strong>2. It can make chart code easier to reason about</strong></p><p>Even if inference works, explicit scale configuration can make the chart easier to read and maintain. This is not a direct Apple quote, but it is a practical consequence of how the API separates the idea of scale type from the scale range itself. </p><p><strong>3. It helps you think in the correct model</strong></p><p>When you pass categorical values, you are working with discrete positions. When you pass dates or numbers, you are usually working with a continuous axis. type reinforces that distinction.</p><div><hr></div><h2>Category Example with Explicit Type</h2><p>Here&#8217;s a complete example that makes the scale type explicit.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;df4df37b-62e2-4bdd-a793-1b89749a759a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">import SwiftUI
import Charts

struct TemperaturePoint: Identifiable {
    let id = UUID()
    let day: String
    let temperature: Double
}

struct TemperatureChartView: View {
    let data: [TemperaturePoint] = [
        .init(day: "Mon", temperature: 18),
        .init(day: "Tue", temperature: 21),
        .init(day: "Wed", temperature: 19),
        .init(day: "Thu", temperature: 24),
        .init(day: "Fri", temperature: 22)
    ]

    var body: some View {
        Chart(data) { item in
            LineMark(
                x: .value("Day", item.day),
                y: .value("Temperature", item.temperature)
            )

            PointMark(
                x: .value("Day", item.day),
                y: .value("Temperature", item.temperature)
            )
        }
        .chartXScale(
            range: .plotDimension(startPadding: 12, endPadding: 12),
            type: .category
        )
        .frame(height: 220)
        .padding()
    }
}</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!XVl_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8ec08bbc-1c8a-4601-aab2-9fbadd8a0903_774x496.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!XVl_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8ec08bbc-1c8a-4601-aab2-9fbadd8a0903_774x496.png 424w, https://substackcdn.com/image/fetch/$s_!XVl_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8ec08bbc-1c8a-4601-aab2-9fbadd8a0903_774x496.png 848w, https://substackcdn.com/image/fetch/$s_!XVl_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8ec08bbc-1c8a-4601-aab2-9fbadd8a0903_774x496.png 1272w, https://substackcdn.com/image/fetch/$s_!XVl_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8ec08bbc-1c8a-4601-aab2-9fbadd8a0903_774x496.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!XVl_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8ec08bbc-1c8a-4601-aab2-9fbadd8a0903_774x496.png" width="510" height="326.8217054263566" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8ec08bbc-1c8a-4601-aab2-9fbadd8a0903_774x496.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:496,&quot;width&quot;:774,&quot;resizeWidth&quot;:510,&quot;bytes&quot;:40894,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/192009305?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8ec08bbc-1c8a-4601-aab2-9fbadd8a0903_774x496.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!XVl_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8ec08bbc-1c8a-4601-aab2-9fbadd8a0903_774x496.png 424w, https://substackcdn.com/image/fetch/$s_!XVl_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8ec08bbc-1c8a-4601-aab2-9fbadd8a0903_774x496.png 848w, https://substackcdn.com/image/fetch/$s_!XVl_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8ec08bbc-1c8a-4601-aab2-9fbadd8a0903_774x496.png 1272w, https://substackcdn.com/image/fetch/$s_!XVl_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8ec08bbc-1c8a-4601-aab2-9fbadd8a0903_774x496.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Type with .category</figcaption></figure></div><p>This is a good example of where range and type work together:</p><p>&#9;&#8226;&#9;range adds breathing room inside the plot</p><p>&#9;&#8226;&#9;type: <code>.category</code> makes the discrete x-axis behavior explicit</p><div><hr></div><h2>How This Differs From Domain?</h2><p>This is the distinction that matters most.</p><p>If you write:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;6bc99367-300a-4a11-8deb-2c541bfab168&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">.chartXScale(domain: [&#8221;Jan&#8221;, &#8220;Feb&#8221;, &#8220;Mar&#8221;, &#8220;Apr&#8221;, &#8220;May&#8221;])</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Pxff!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9eee0da-369b-412e-9436-f0fe310cc0f5_774x496.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Pxff!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9eee0da-369b-412e-9436-f0fe310cc0f5_774x496.png 424w, https://substackcdn.com/image/fetch/$s_!Pxff!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9eee0da-369b-412e-9436-f0fe310cc0f5_774x496.png 848w, https://substackcdn.com/image/fetch/$s_!Pxff!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9eee0da-369b-412e-9436-f0fe310cc0f5_774x496.png 1272w, https://substackcdn.com/image/fetch/$s_!Pxff!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9eee0da-369b-412e-9436-f0fe310cc0f5_774x496.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Pxff!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9eee0da-369b-412e-9436-f0fe310cc0f5_774x496.png" width="510" height="326.8217054263566" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f9eee0da-369b-412e-9436-f0fe310cc0f5_774x496.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:496,&quot;width&quot;:774,&quot;resizeWidth&quot;:510,&quot;bytes&quot;:31172,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/192009305?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9eee0da-369b-412e-9436-f0fe310cc0f5_774x496.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Pxff!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9eee0da-369b-412e-9436-f0fe310cc0f5_774x496.png 424w, https://substackcdn.com/image/fetch/$s_!Pxff!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9eee0da-369b-412e-9436-f0fe310cc0f5_774x496.png 848w, https://substackcdn.com/image/fetch/$s_!Pxff!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9eee0da-369b-412e-9436-f0fe310cc0f5_774x496.png 1272w, https://substackcdn.com/image/fetch/$s_!Pxff!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9eee0da-369b-412e-9436-f0fe310cc0f5_774x496.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Domain override</figcaption></figure></div><p>you are configuring which values belong to the x-axis domain.</p><p>If you write:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;ddc31337-8884-40d5-a06d-15f5b04c9038&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">.chartXScale(range: .plotDimension(padding: 20))</code></pre></div><p>you are configuring where those values are drawn horizontally.</p><p>And if you write:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;68af4dd7-792c-491f-a549-446d5f303312&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">.chartXScale(
    range: .plotDimension(padding: 20),
    type: .category
)</code></pre></div><p>you are also telling Swift Charts how that scale should behave.</p><p>Apple&#8217;s chart scale documentation separates these concerns clearly: domain is about possible axis values, range is about axis positions, and <code>ScaleType</code> describes how the domain or range of a plot is scaled.</p><h3>A Practical Rule of Thumb</h3><p>Here&#8217;s the version I&#8217;d keep in mind while building charts:</p><p>&#9;&#8226;&#9;use <strong>domain</strong> when you want to control what values are visible</p><p>&#9;&#8226;&#9;use <strong>range</strong> when you want to control spacing and plotting space</p><p>&#9;&#8226;&#9;use <strong>type</strong> when you want to make the scale behavior explicit</p><p>That makes <code>chartXScale(range:type:)</code> much easier to understand.</p><div><hr></div><h2>Final Thoughts</h2><p><code>chartXScale(range:type:)</code> is one of those modifiers that does not look exciting at first glance, but it becomes very useful the moment you start polishing a chart. The biggest win is usually visual: better edge spacing, more balanced composition, and a cleaner plot without changing any data at all.</p><p>And the nice part is that it scales from simple to advanced quite naturally:</p><p>&#9;&#8226;&#9;start with <code>.plotDimension(padding:)</code></p><p>&#9;&#8226;&#9;move to <code>startPadding</code> and <code>endPadding</code> when you need more control</p><p>&#9;&#8226;&#9;add <code>Type</code> when you want the axis model to be explicit</p><blockquote><p>Once you think of it that way, the modifier stops feeling obscure. It is really just a small but important piece of chart layout.</p></blockquote><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h2>References</h2><ul><li><p><a href="https://developer.apple.com/documentation/swiftui/view/chartxscale(range:type:)">chartXScale(range:type:)</a></p></li><li><p><a href="https://developer.apple.com/documentation/swiftui/view/chartxscale(domain:range:type:)">chartXScale(domain:range:type:)</a></p></li><li><p><a href="https://developer.apple.com/documentation/Charts/PositionScaleRange">PositionScaleRange</a></p></li><li><p><a href="https://developer.apple.com/documentation/Charts/ScaleType">ScaleType</a></p></li><li><p><a href="https://developer.apple.com/documentation/charts/scaletype/category">ScaleType.category</a></p></li><li><p><a href="https://developer.apple.com/documentation/Charts">Swift Charts</a></p></li></ul>]]></content:encoded></item><item><title><![CDATA[Swift Bits: Restore ChatGPT macOS App Chats]]></title><description><![CDATA[ChatGPT app chats restore guide]]></description><link>https://antongubarenko.substack.com/p/swift-bits-restore-chatgpt-macos</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/swift-bits-restore-chatgpt-macos</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Fri, 13 Mar 2026 10:20:25 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/a4eb9cf3-185f-45c6-874b-41d22793ab1d_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It&#8217;s been a while since I&#8217;ve posted Bits, all because of some physical obstacles. However, the recent ChatGPT macOS app update and the 5.4 model reveal made my interaction with AI (OpenAI only) more complicated. Let me tell you why&#8230;</p><h3>Problem Begins</h3><p>After happily updating to Version 1.2026.048 (1771630681) </p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!oWr2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd33b9203-f8eb-413a-a2c3-ba164e33cc97_560x318.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!oWr2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd33b9203-f8eb-413a-a2c3-ba164e33cc97_560x318.png 424w, https://substackcdn.com/image/fetch/$s_!oWr2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd33b9203-f8eb-413a-a2c3-ba164e33cc97_560x318.png 848w, https://substackcdn.com/image/fetch/$s_!oWr2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd33b9203-f8eb-413a-a2c3-ba164e33cc97_560x318.png 1272w, https://substackcdn.com/image/fetch/$s_!oWr2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd33b9203-f8eb-413a-a2c3-ba164e33cc97_560x318.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!oWr2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd33b9203-f8eb-413a-a2c3-ba164e33cc97_560x318.png" width="386" height="219.19285714285715" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d33b9203-f8eb-413a-a2c3-ba164e33cc97_560x318.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:318,&quot;width&quot;:560,&quot;resizeWidth&quot;:386,&quot;bytes&quot;:41685,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/190653213?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd33b9203-f8eb-413a-a2c3-ba164e33cc97_560x318.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!oWr2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd33b9203-f8eb-413a-a2c3-ba164e33cc97_560x318.png 424w, https://substackcdn.com/image/fetch/$s_!oWr2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd33b9203-f8eb-413a-a2c3-ba164e33cc97_560x318.png 848w, https://substackcdn.com/image/fetch/$s_!oWr2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd33b9203-f8eb-413a-a2c3-ba164e33cc97_560x318.png 1272w, https://substackcdn.com/image/fetch/$s_!oWr2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd33b9203-f8eb-413a-a2c3-ba164e33cc97_560x318.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><p>the main view welcomed me with this:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!vG3o!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fbc1517-bf27-469d-86d1-1f159b5850d2_2712x1750.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vG3o!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fbc1517-bf27-469d-86d1-1f159b5850d2_2712x1750.png 424w, https://substackcdn.com/image/fetch/$s_!vG3o!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fbc1517-bf27-469d-86d1-1f159b5850d2_2712x1750.png 848w, https://substackcdn.com/image/fetch/$s_!vG3o!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fbc1517-bf27-469d-86d1-1f159b5850d2_2712x1750.png 1272w, https://substackcdn.com/image/fetch/$s_!vG3o!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fbc1517-bf27-469d-86d1-1f159b5850d2_2712x1750.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!vG3o!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fbc1517-bf27-469d-86d1-1f159b5850d2_2712x1750.png" width="1456" height="940" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2fbc1517-bf27-469d-86d1-1f159b5850d2_2712x1750.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:940,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:545958,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/190653213?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fbc1517-bf27-469d-86d1-1f159b5850d2_2712x1750.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!vG3o!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fbc1517-bf27-469d-86d1-1f159b5850d2_2712x1750.png 424w, https://substackcdn.com/image/fetch/$s_!vG3o!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fbc1517-bf27-469d-86d1-1f159b5850d2_2712x1750.png 848w, https://substackcdn.com/image/fetch/$s_!vG3o!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fbc1517-bf27-469d-86d1-1f159b5850d2_2712x1750.png 1272w, https://substackcdn.com/image/fetch/$s_!vG3o!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fbc1517-bf27-469d-86d1-1f159b5850d2_2712x1750.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">ChatGPT macOS app</figcaption></figure></div><p>Chats were not loading and new ones were blocked from being created. Not very appealing, so I went to check the web version at ChatGPT.com, where there was a list of my chats: pinned and unpinned. After checking the latest ones, everything seemed fine. Now it was time to check the pinned ones. And it was the correct guess! One of them was not loading, and I got a toast at the top of the screen:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;de573d88-3026-4075-827a-bee1fce57cb5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Could not load conversation &lt;uuid&gt;!</code></pre></div><p>The Safari console looked like this:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!X_IV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f877daa-9fd8-47ca-b521-546d811deaf1_2876x578.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!X_IV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f877daa-9fd8-47ca-b521-546d811deaf1_2876x578.png 424w, https://substackcdn.com/image/fetch/$s_!X_IV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f877daa-9fd8-47ca-b521-546d811deaf1_2876x578.png 848w, https://substackcdn.com/image/fetch/$s_!X_IV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f877daa-9fd8-47ca-b521-546d811deaf1_2876x578.png 1272w, https://substackcdn.com/image/fetch/$s_!X_IV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f877daa-9fd8-47ca-b521-546d811deaf1_2876x578.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!X_IV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f877daa-9fd8-47ca-b521-546d811deaf1_2876x578.png" width="1456" height="293" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8f877daa-9fd8-47ca-b521-546d811deaf1_2876x578.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:293,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:278839,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/190653213?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f877daa-9fd8-47ca-b521-546d811deaf1_2876x578.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!X_IV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f877daa-9fd8-47ca-b521-546d811deaf1_2876x578.png 424w, https://substackcdn.com/image/fetch/$s_!X_IV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f877daa-9fd8-47ca-b521-546d811deaf1_2876x578.png 848w, https://substackcdn.com/image/fetch/$s_!X_IV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f877daa-9fd8-47ca-b521-546d811deaf1_2876x578.png 1272w, https://substackcdn.com/image/fetch/$s_!X_IV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f877daa-9fd8-47ca-b521-546d811deaf1_2876x578.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">500 Server error on conversation load</figcaption></figure></div><h3>Applied Fixes</h3><p>Googling and prompting led to a couple of suggestions. First of all, report it to OpenAI via the help form or chat in the app</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!kQez!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F738b3e49-50df-4afd-94bd-a7686f60dbde_838x1230.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!kQez!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F738b3e49-50df-4afd-94bd-a7686f60dbde_838x1230.png 424w, https://substackcdn.com/image/fetch/$s_!kQez!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F738b3e49-50df-4afd-94bd-a7686f60dbde_838x1230.png 848w, https://substackcdn.com/image/fetch/$s_!kQez!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F738b3e49-50df-4afd-94bd-a7686f60dbde_838x1230.png 1272w, https://substackcdn.com/image/fetch/$s_!kQez!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F738b3e49-50df-4afd-94bd-a7686f60dbde_838x1230.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!kQez!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F738b3e49-50df-4afd-94bd-a7686f60dbde_838x1230.png" width="372" height="546.0143198090692" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/738b3e49-50df-4afd-94bd-a7686f60dbde_838x1230.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1230,&quot;width&quot;:838,&quot;resizeWidth&quot;:372,&quot;bytes&quot;:90689,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/190653213?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F738b3e49-50df-4afd-94bd-a7686f60dbde_838x1230.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!kQez!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F738b3e49-50df-4afd-94bd-a7686f60dbde_838x1230.png 424w, https://substackcdn.com/image/fetch/$s_!kQez!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F738b3e49-50df-4afd-94bd-a7686f60dbde_838x1230.png 848w, https://substackcdn.com/image/fetch/$s_!kQez!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F738b3e49-50df-4afd-94bd-a7686f60dbde_838x1230.png 1272w, https://substackcdn.com/image/fetch/$s_!kQez!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F738b3e49-50df-4afd-94bd-a7686f60dbde_838x1230.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">OpenAI help form from web</figcaption></figure></div><p>This may trigger a possible fix from the server side.</p><blockquote><p>At the moment of publication, no fixes have been made.</p></blockquote><p>Form client-side:</p><ul><li><p><strong>Refresh/Restart:</strong> Hard refresh your browser or restart the app</p></li><li><p><strong>Log Out/In:</strong> Sign out and back into your account to refresh the session.</p></li><li><p><strong>Clear Data:</strong> Clear your browser&#8217;s cache and cookies for <code>chat.openai.com</code>.</p></li><li><p><strong>Check Extensions:</strong> Disable browser extensions (especially ad blockers) that might interfere with page loading.</p></li><li><p><strong>Try Incognito/New Browser:</strong> Use an incognito/private window or a different browser to rule out extension issues.</p></li><li><p><strong>Switch Network:</strong> Toggle your VPN off or switch to a different network.</p></li></ul><p>Would I wrote a post if any of this worked?) </p><h3>Tracing the Problem</h3><p>As a developer, I can see how the loading of chats might be implemented. For a conversation ID, the app should load the list of chats and retrieve the pinned ones. If any of them fail &#8594; show a warning sign with a popup on tap containing details and a &#8220;Reload&#8221; button. Yeah, right.</p><p>Instead, if any chat fails &#8594; the macOS app shows none of them. That is because they are not calling each conversation with a <strong>GET Conversation ID</strong> request. That brings speed, but the batch request fails. What can we do about it? Exclude the problematic chat. Pin/Unpin will not work. The only option is to archive it. This will prevent it from loading and move it further into Settings:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!H0QN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57add250-844f-4722-a02b-34bfc718cdf1_394x554.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!H0QN!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57add250-844f-4722-a02b-34bfc718cdf1_394x554.png 424w, https://substackcdn.com/image/fetch/$s_!H0QN!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57add250-844f-4722-a02b-34bfc718cdf1_394x554.png 848w, https://substackcdn.com/image/fetch/$s_!H0QN!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57add250-844f-4722-a02b-34bfc718cdf1_394x554.png 1272w, https://substackcdn.com/image/fetch/$s_!H0QN!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57add250-844f-4722-a02b-34bfc718cdf1_394x554.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!H0QN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57add250-844f-4722-a02b-34bfc718cdf1_394x554.png" width="270" height="379.6446700507614" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/57add250-844f-4722-a02b-34bfc718cdf1_394x554.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:554,&quot;width&quot;:394,&quot;resizeWidth&quot;:270,&quot;bytes&quot;:33588,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/190653213?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57add250-844f-4722-a02b-34bfc718cdf1_394x554.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!H0QN!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57add250-844f-4722-a02b-34bfc718cdf1_394x554.png 424w, https://substackcdn.com/image/fetch/$s_!H0QN!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57add250-844f-4722-a02b-34bfc718cdf1_394x554.png 848w, https://substackcdn.com/image/fetch/$s_!H0QN!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57add250-844f-4722-a02b-34bfc718cdf1_394x554.png 1272w, https://substackcdn.com/image/fetch/$s_!H0QN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F57add250-844f-4722-a02b-34bfc718cdf1_394x554.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Archive action for chat</figcaption></figure></div><p><br>Later you can find it here:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!-hoi!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8fa571e2-b102-4982-a997-fa7a0a64c3ec_1364x1146.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!-hoi!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8fa571e2-b102-4982-a997-fa7a0a64c3ec_1364x1146.png 424w, https://substackcdn.com/image/fetch/$s_!-hoi!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8fa571e2-b102-4982-a997-fa7a0a64c3ec_1364x1146.png 848w, https://substackcdn.com/image/fetch/$s_!-hoi!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8fa571e2-b102-4982-a997-fa7a0a64c3ec_1364x1146.png 1272w, https://substackcdn.com/image/fetch/$s_!-hoi!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8fa571e2-b102-4982-a997-fa7a0a64c3ec_1364x1146.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!-hoi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8fa571e2-b102-4982-a997-fa7a0a64c3ec_1364x1146.png" width="528" height="443.61290322580646" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8fa571e2-b102-4982-a997-fa7a0a64c3ec_1364x1146.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1146,&quot;width&quot;:1364,&quot;resizeWidth&quot;:528,&quot;bytes&quot;:143173,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/190653213?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8fa571e2-b102-4982-a997-fa7a0a64c3ec_1364x1146.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!-hoi!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8fa571e2-b102-4982-a997-fa7a0a64c3ec_1364x1146.png 424w, https://substackcdn.com/image/fetch/$s_!-hoi!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8fa571e2-b102-4982-a997-fa7a0a64c3ec_1364x1146.png 848w, https://substackcdn.com/image/fetch/$s_!-hoi!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8fa571e2-b102-4982-a997-fa7a0a64c3ec_1364x1146.png 1272w, https://substackcdn.com/image/fetch/$s_!-hoi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8fa571e2-b102-4982-a997-fa7a0a64c3ec_1364x1146.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Archived chats in Data Control</figcaption></figure></div><p>And that&#8217;s it! The chat is prevented from loading on the initial start.</p><blockquote><p>Quite interesting that in <a href="https://developers.openai.com/api/reference/resources/conversations/methods/create/">API</a> chats are called conversations. And even browser has a <a href="https://chatgpt.com/c/">https://chatgpt.com/c/</a> prefix. </p></blockquote><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[Fortify Your App: Essential Strategies to Strengthen Security Q&A]]></title><description><![CDATA[Answers from Apple Engineers about security]]></description><link>https://antongubarenko.substack.com/p/fortify-your-app-essential-strategies</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/fortify-your-app-essential-strategies</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Mon, 09 Mar 2026 07:05:14 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/4c9c618f-f3de-45e9-a9b8-10c4074845af_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Meetups and webinars continue, and this time Apple hosted a session on <strong><a href="https://developer.apple.com/videos/play/meet-with-apple/265/">Security Strategies</a></strong>, covering topics such as Enhanced Security, Memory Integrity, and writing secure code with Swift. Previously, I was already surprised by a four-hour duration. Now it&#8217;s almost six hours (six!) with three breaks, and it requires a lot of focus.</p><blockquote><p>In the survey after the session, I mentioned that splitting it into separate sections would be better, in my opinion. Finding that much time during either the night or regular working hours is not always possible. Let&#8217;s hope they are actually reading the feedback.</p></blockquote><ul><li><p><a href="https://developer.apple.com/videos/play/meet-with-apple/265/">Apple Video source</a></p></li><li><p><a href="https://www.youtube.com/watch?v=UZeSyodAszc">YouTube version</a></p></li></ul><p>As usual, questions are splitted into more or less common section. Grammar and punctuation are kept too.</p><div><hr></div><h2>&#128272; Security &amp; Memory Safety</h2><h3>What strategies can we use to evaluate third-party libraries for security vulnerabilities?</h3><p>Hi! Evaluating third-party libraries can be tricky and require a lot of domain expertise as there are a wide variety of issues to look for. This is especially true if you only have access to the libraries in a binary form. Certain features&#8212;like EMTE&#8212;serve as mitigations and can be applied (in some cases) without a recompile, which can help in cases where you cannot easily audit the source. When auditing the source, some good hints for finding security relevant bugs are: 1. Memory unsafe code (C/C++, Swift could which uses unsafe, etc.), especially decoders and deserialization routines (e.g. bespoke file/request format parsing). 2. File path construction and archive expansion (zip files, etc.) are a common source of problems due to path traversal 3. Whether or not the code is memory safe, take special care to understand where it comes from and whether you trust it.</p><div><hr></div><h3>What are the potential security vulnerabilities associated with storing data in <code>UserDefaults</code> and plist files, and what best practices should be followed to protect against tampering or exposure?</h3><p>In terms of pure data security, both approaches are fairly similar. The data is being stored in the file system and is protected the same way any other file does. However, using your own plist file does mean you can directly set the file&#8217;s protection level, which is another reason to avoid storing sensitive data in <code>UserDefaults</code>. Similarly, both are using the same on-disk format and data parser. Theoretically, that&#8217;s an attack vector just like any file parser. However, this particular parser is relatively simple compared to other formats (for example, SQL) and so widely used throughout the entire system that I&#8217;d consider it quite safe.</p><div><hr></div><h3>For a standalone iOS app with no backend, what&#8217;s Apple&#8217;s recommended baseline for protecting sensitive user-entered data at rest? Is &#8216;Complete Protection&#8217; (<code>NSFileProtectionComplete</code>) plus Keychain for secrets the correct default?</h3><p>The baseline for most data should be <code>NSFileProtectionCompleteUntilFirstUserAuthentication</code> (for files) and <code>kSecAttrAccessibleAfterFirstUnlock</code> (keychain). The issue with storing data at higher security levels is that data stored at &#8220;Complete&#8221; can really only be safely accessed while your app is in the foreground. More specifically, the data will only be accessible if the device is unlocked, but that state changes independently from background execution, meaning data can suddenly become inaccessible anytime your app is running in the background. You can and should use &#8220;Complete&#8221; whenever you can, but it&#8217;s a choice that needs to be made as part of your app&#8217;s overall design, not as an automatic default.</p><div><hr></div><h3>Is it safe to use the Data type&#8217;s <code>withUnsafeBytes</code> method? I find myself using it a lot when parsing values from Bluetooth devices.</h3><p>Methods with &#8220;unsafe&#8221; in the name can be used safely, but it requires more care and attention to do so. You won&#8217;t always be able to avoid using such methods, but you can carefully isolate such code and of course make sure to devote more effort to reviewing and verifying any code that uses unsafe methods or types.</p><div><hr></div><h3>I&#8217;m confused of the purpose of blast door, given the validation of the image was ultimately down to messages&#8230; surely blast door would protect and / or sanitise the object/data recieved? Or have I missed the point?</h3><p>Properly validating images can itself be a perilous task and so it&#8217;s one best not performed inside a (relatively) privileged process like Messages. BlastDoor validates and unpacks attachments in a highly restricted environment and then passes a much safer, simpler version of it to Messages. So, for example, Messages may give BlastDoor a JPG and BlastDoor will ultimately hand back a simple bitmap representation of it. This indemnifies Messages against memory corruption bugs in the JPG parser.</p><div><hr></div><h3>Apple on messages for example never denies uploads of different file types, but then how does it protect against file-based vulnerabilities?</h3><p>Messages allows sending (mostly) arbitrary files but&#8212;notably&#8212;it does not parse/preview/unpack arbitrary files. Messages and BlastDoor work together to ensure that attachments which are processed are processed in a way that&#8217;s safe. It is generally safe to accept arbitrary files so long as they are not processed.</p><div><hr></div><h3>Memory safe languages like Swift or Rust have sometimes to bypass security checks (e.g. to access the kernel). Which protections and tools can be used in that case?</h3><p>On Apple platforms, EMTE is a great way to mitigate issues caused by this gap. Even kernel accesses to user memory are tag checked, and so out-of-bounds/use-after-free accesses to user memory will still be reported according to the process&#8217; enforcement mode. Other techniques such as fuzzing with libfuzzer and either ASan or EMTE on can also serve as strategies to gain confidence that unsafe code has the desired memory safety properties.</p><div><hr></div><h3>This question may be covered today, but I&#8217;m curious what resources and guidance exists for maximizing security in apps when we build across platforms. SwiftUI is write once, tweak, deploy. How about security? Sandboxing is different as an example on Mac vs iPhone.</h3><p>Hi! Thanks for joining. The event today covers many topics about security and memory safety. Most of the guidance is platform agnostic, and some are programming language agnostic. For additional resources specific to your app and project environments, please refer to Apple Platform Security: <a href="https://support.apple.com/guide/security/welcome/web">https://support.apple.com/guide/security/welcome/web</a></p><div><hr></div><h3>What techniques help verify if a third-party app has adopted security enhancements, hardware memory tagging, security extensions?</h3><p>If you have the application bundle available to you, you can use the codesign tool to view an application&#8217;s entitlements: <code>codesign -d --entitlements - /path/to/application/binary</code> As EMTE is controlled by entitlement, you can use this technique to see if EMTE is enabled for a given executable in the app.</p><div><hr></div><h3>What additional features (settings) required for fully Swift apps? is Enhanced Memory safety required (new Capability + config)? How much of security guarantees Swift provides? anything in the talk required to be enabled or is enabled by default?</h3><p>Enabling enhanced security features like PAC, EMTE, and typed allocations is still useful in Swift apps. In certain language modes, Swift apps which do not use <code>unsafe</code> can still have memory corruption issues due to data races (concurrently modifying a reference can cause reference counting errors). Similarly, although your application may be fully safe Swift, it may interact with libraries (provided by Apple or third parties) which are not fully memory safe, and so turning on enhanced security features will help protect you against issues not caused by your code.</p><div><hr></div><h2>&#128737;&#65039; Enhanced Security &amp; Capabilities</h2><h3>For the Pointer Authentication feature, supporting arm64e is required. I understand this applies to third party SDKs, but how about an app that is completely modularized (with all modules in the same workspace), do we need to configure this per module or only the main app target?</h3><p>arm64e is indeed required, and every target that contributes binary code that&#8217;s linked or dynamically loaded into an app does need to have arm64e added as an architecture. When enabling the Enhanced Security capability, Xcode adds the <code>ENABLE_POINTER_AUTHENTICATION</code> build setting (that adds arm64e) as needed, but you may need to add that separately as well.</p><div><hr></div><h3>Is there a <code>cSettings</code> SPM construct to enable PAC, MTE (and other features that do not require the final top-level signing/capabilities entitlement) in a Swift package that has C/C++ targets, so that compiled libraries can be included in an Xcode project using these enhanced security capabilities?</h3><p>Any clang compiler option can be included in cSettings. Caveat: Prior to Swift 6.2, only some clang options were allowed, but starting with Swift 6.2, you can put any option.</p><div><hr></div><h3>Does PAC specifically (enhanced security include PAC and more) work for iOS 14+ deployment targets? You can enable each part of enhanced security individually, so like we know that typed memory allocator compiles nicely for iOS16 or 17+, but we can get PAC compiled for iOS 14+?</h3><p>PAC is tied to the arm64e architecture. arm64e is first supported in iOS 17.4, and generally supported starting with iOS 26 and macOS 26. Universal binaries can be built for arm64e + arm64, and arm64 will be used when arm64e isn&#8217;t supported. When building the universal binary, both architectures can be compiled for an older deployment target, but keep in mind that arm64e will only be used on newer iOS.</p><div><hr></div><h3>Do these features require Xcode 26.4 to debug, run and test with? I&#8217;ve noticed the change in entitlements mentioned in the Xcode 26.4 beta release notes, does that mean that you need to test and run on Apple 26.4 OSes to try out Enhanced Security capabilities now?</h3><p>Most of the capabilities of Enhanced Security are supported on and can be tested on Apple OS versions starting from 26.0. The &#8220;hard mode&#8221; of MTE (under which tag-check violations result in an immediate crash) is only supported beginning in 26.4, so to test with that capability on hardware with MTE support you&#8217;ll need to test with 26.4 or later OS versions. (described in 26.4 release notes [1]) The new enhanced-security-version-string and platform-restrictions-string string version entitlements you noted described in the Xcode 26.4 release notes [2] are set automatically by Xcode 26.4, but can be set manually in your entitlements plists using a text editor if you need to stay on an earlier Xcode version. <br>[1] <a href="https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-26_4-release-notes">https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-26_4-release-notes</a> [2] <a href="https://developer.apple.com/documentation/xcode-release-notes/xcode-26_4-release-notes">https://developer.apple.com/documentation/xcode-release-notes/xcode-26_4-release-notes</a></p><div><hr></div><h3>I&#8217;m having trouble finding the advanced security&#8230; is it under signing in capabilities or is it under the settings of Xcode? Also. Can you build an outlet already contained like I was building a journal app I don&#8217;t want it to be connected to any network services by default or any services</h3><p>Hi Elijah, please refer to the documentation below for steps to enable the Enhanced Security capability: <a href="https://developer.apple.com/documentation/xcode/enabling-enhanced-security-for-your-app">https://developer.apple.com/documentation/xcode/enabling-enhanced-security-for-your-app</a></p><div><hr></div><h3>I&#8217;ve noticed that when enabling MTE, my app keeps crashing on launch with reports pointing to 3rd party SDKs. Whenever I take out one SDK, it just crashes in the next. It seems the reported tag is always 0, which makes me believe it&#8217;s not just violations but maybe a configuration problem on my end?</h3><p>Additionally, arm64 binaries produced by older versions of clang may have issues where the tag is incorrectly stripped from the pointer. Recompiling the binary with a recent compiler should remediate the issue.</p><div><hr></div><h3>Will enabling enhanced security extension from code signing help to find bugs for debug builds on simulators as well?</h3><p>Yes, starting in the 26.4 OS versions, applications that enable MTE (checked-allocations) as part of Enhanced Security will run with MTE enabled in the simulator when running on macOS hardware that supports MTE.</p><div><hr></div><h3>Is enabling standard library hardening recommended for all build types not just for debug? Will it cause any latency issues when enabled for prod configuration? Does <code>$(__LIBRARY_HARDENING_DEFAULT_VALUE)</code> mean no hardening set for project?</h3><p>Yes, we recommend enabling at least the fast hardening mode, even in production builds. It has been designed to have minimal performance impact, but if you have very performance sensitive workloads, as always: benchmark before and after. For configurations with optimization disabled (level set to 0 &#8212; normally Debug configs), <code>__LIBRARY_HARDENING_DEFAULT_VALUE</code> defaults to &#8220;debug&#8221; (the more extensive checks). For optimized configurations (e.g. Release), <code>__LIBRARY_HARDENING_DEFAULT_VALUE</code> defaults to &#8220;fast&#8221; if Enhanced Security is enabled, &#8220;none&#8221; otherwise.</p><div><hr></div><h2>&#128269; Hardware Support &amp; Compatibility</h2><h3>Which of the devices announced this week support memory <a href="https://security.apple.com/blog/memory-integrity-enforcement/">integrity enforcement</a>? </h3><p>Memory Integrity Enforcement is supported on A19, A19 Pro, M5, M5 Pro, and M5 Max, which power iPhone 17e, the new MacBook Air (M5), and the new MacBook Pro (M5 Pro or M5 Max).</p><div><hr></div><h3>Is the memory tag exposed in memgraphs or Instruments in any way?</h3><p>When running under the Allocations template in Instruments, the memory tags do show up in object addresses. In memgraphs and the CLI tools like heap, these tags aren&#8217;t currently exposed, with the addresses correlating directly to their containing VM regions. If you&#8217;re interested to see which regions have tagging enabled, this is available with <code>vmmap --attributes</code></p><div><hr></div><h3>Does Memory Integrity Enforcement support pre-Swift 6.0?</h3><p>Yes, Memory Integrity Enforcement can be used with any Swift version.</p><div><hr></div><h2>&#128260; C/C++ Interoperability</h2><h3>In some C/C++ functions the Swift interop uses unsafe pointers to bytes. Is there something like a <code>Span</code> API over the bytes of a Swift <code>var</code> to ensure a memory-safe and bounds-enforced buffer pointer to pass through to C/C++ code instead of using unsafe methods such as <code>withUnsafeMutableBytes()</code>?</h3><p>Great question! Automatically generated wrapper functions that safely unwrap Span types and pass along the pointer to C/C++ is a feature available since Xcode 26 when the experimental feature SafeInteropWrappers is enabled. This requires annotating std::span parameters with __noescape, or pointer parameters with both __noescape and __counted_by/__sized_by, directly in the header or using API notes. Note that this is only safe if Swift can accurately track the lifetime of the unwrapped pointer, which is why the Span wrapper is not generated without the __noescape annotation. More resources are available at <a href="https://developer.apple.com/videos/play/wwdc2025/311/">https://developer.apple.com/videos/play/wwdc2025/311/</a> and <a href="https://www.swift.org/documentation/cxx-interop/safe-interop/">https://www.swift.org/documentation/cxx-interop/safe-interop/</a>. Since this is an experimental feature with ongoing development, questions and feedback on https://forums.swift.org are extra welcome to help us shape and stabilize this feature!</p><div><hr></div><h2>&#128137; Pointer Authentication</h2><h3>How does PAC work with ObjC method swizzling at runtime?</h3><p>When you use the functions provided by the ObjC runtime, they ensure that any necessary pointer signing is correctly handled.</p><div><hr></div><h3>How does pointer authentication different from other memory defense ways mentioned as part of whole app protection?</h3><p>Pointer authentication makes it more difficult to create a pointer (from an integer) or to modify an existing pointer. This complements technologies such as MTE (which can catch many bound and lifetime errors) and typed allocation (which mitigates the effects of memory re-use).</p><div><hr></div><h3>Why is Pointer Authentication a compile-time, opt-in feature, and not a platform-enforced, runtime-enabled feature? I imagine something like that should be possible if something as magical as Rosetta is also possible &#128578;</h3><p>Additionally, PAC is a compile time change as it requires different instructions throughout the program.</p><div><hr></div><h3>Where are the cryptographic tags used for pointer authentication stored on Apple devices? Are they kept in the Secure Enclave or in another hardware component?</h3><p>The signatures are, however, stored in the upper bits of the pointer itself.</p><div><hr></div><h2>&#128230; Allocators &amp; Memory Management</h2><h3>Are type and alignment independent for typed allocators?</h3><p>Mostly, yes, but not entirely. The typed allocators segregate and isolate allocations by size class and, within each size class, by type space partition. Let&#8217;s call the (size class, type space partition) combination the &#8220;type bucket&#8221; that serves a particular allocation. Requesting aligned allocations (e.g. via <code>aligned_alloc()</code>) can change the effective size class of an allocation because of implementation details of the allocators, and so can change the type bucket that the allocation is served from.</p><div><hr></div><h3>What is best approach to use system allocator for third party SDK e.g. Braze if enabled enhanced security extension for app and memory corruption is noticed?</h3><p>Third-party SDKs linked in to your app/program will generally be using the system allocator automatically and benefit from Memory Integrity Enforcement automatically. If there are memory corruption bugs in those SDKs that Memory Integrity Enforcement features like MTE detect and turn into crashes, you would want to work with the developers of those SDKs to have them fix the underlying bugs. You could use MTE soft mode [1] to avoid having those memory corruptions crash your app while you wait for fixes from the developers, at the cost of the relative reduction in security that that entails. <a href="https://developer.apple.com/documentation/BundleResources/Entitlements/com.apple.security.hardened-process.checked-allocations.soft-mode">https://developer.apple.com/documentation/BundleResources/Entitlements/com.apple.security.hardened-process.checked-allocations.soft-mode</a></p><div><hr></div><h2>&#128207; Bounds Safety</h2><h3>How do bounds safety checks prevent out-of-bounds (OOB) accesses in practice? Is this implemented as a built-in feature in Clang? When developers add annotations, what mechanisms are applied internally to enforce or adjust the bounds checks?</h3><p>Yes, this is built into Clang. With -fbounds-safety enabled Clang will emit bounds checks wherever pointers are dereferenced or reassigned (exception: assigning to <code>__bidi_indexable</code> does not trigger a bounds check, since <code>__bidi_indexable</code> can track the fact that the pointer is out of bounds and defer the bounds check). If the bounds check fails the program will jump to an instruction that traps the process. Clang uses a combination of static analysis and runtime checks to enforce that pointer bounds are respected.</p><div><hr></div><h3>Why is CoreFoundation missing all bounds-checking annotations? Do I have to use <code>__unsafe_forge_single</code> for all initializers?</h3><p>Yes, that is the recommended approach when interoperating with libraries that do not have bounds annotations, when you want to be explicit about the fact that you&#8217;re interacting with unsafe code. This makes it easy to grep for &#8220;unsafe&#8221; in your code base when doing a security audit. If you are confident that the API adheres to a bounds safe interface but simply lacks the annotations, you can redeclare the signature in your local header with added bounds annotations, like this: </p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;ec37d596-eccb-4274-a500-599d87604af2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">//--- system_header.h bar_t * /* 
implicitly __unsafe_indexable */ foo(); 
//--- project_header.h 
#include #include bar_t * __single foo();</code></pre></div><div><hr></div><h2>&#128241; App Store &amp; Privacy</h2><h3>Are there common App Store review pitfalls for apps that store sensitive contact data locally (even if it&#8217;s not medical)? Anything you recommend we avoid to prevent rejection or privacy concerns?</h3><p>Hi Gloria! Please see the following pages on user privacy and data use: </p><p>- <a href="https://developer.apple.com/app-store/user-privacy-and-data-use/">https://developer.apple.com/app-store/user-privacy-and-data-use</a><br>- <a href="https://developer.apple.com/app-store/app-privacy-details/">https://developer.apple.com/app-store/app-privacy-details/</a> <br>- <a href="https://developer.apple.com/documentation/uikit/protecting-the-user-s-privacy/">https://developer.apple.com/documentation/uikit/protecting-the-user-s-privacy/</a> <br>- <a href="https://developer.apple.com/documentation/uikit/requesting-access-to-protected-resources">https://developer.apple.com/documentation/uikit/requesting-access-to-protected-resources</a> <br>- <a href="https://developer.apple.com/documentation/uikit/encrypting-your-app-s-files">https://developer.apple.com/documentation/uikit/encrypting-your-app-s-files</a></p><div><hr></div><h2><strong>&#127942; </strong>Acknowledgments</h2><p>A huge thank-you to everyone who joined in and shared thoughtful, insightful, and engaging questions throughout the session &#8212; your curiosity and input made the discussion truly rich and collaborative.</p><p><strong>Special thanks to:</strong></p><p>Larry Wang, Chandrachud Patil, Christopher Sheats, Gloria Glazebrook, Patrick Hoekstra, Chris CL, Eric Dorphy, AAron Wangugi, Quinten Johnson, Nicholas Levin, Kim Ahlberg, Jason Brooks, Patrick Cousot, Alex Infanti, Ilia, nikolay dubina, Danylo, Pablo Butron, Elijah Cody Bain Black, Paul Floyd, Kim Kyoungsu, Zaid Al-Timimi, Rupinder, Tanner Bennett, Sanket P, Steven Joubanian and Gordon Leete.</p><p>Finally, a heartfelt thank-you to the <strong>Apple team and moderators</strong> for leading session, sharing expert guidance, and offering such clear explanations of the apps optimization techniques. Your contributions made this session an outstanding learning experience for everyone involved.</p><div><hr></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[SwiftUI Foundations: Build Great Apps with SwiftUI Q&A Promo]]></title><description><![CDATA[Answers from Apple Engineers]]></description><link>https://antongubarenko.substack.com/p/swiftui-foundations-build-great-apps-163</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/swiftui-foundations-build-great-apps-163</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Tue, 17 Feb 2026 07:04:17 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/97215df7-e012-41c7-8eda-11959a276e3b_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently Apple returned with webinars format where engineers answered questions directly from developers while hosts were talking about different parts of some topic. </p><p>This time <a href="https://developer.apple.com/videos/play/meet-with-apple/267/">it was SwiftUI</a>: from basics to state behavior. If the last session about &#8220;Coding Agents in Xcode&#8220; went for 16 mins and had 3 question - this one is totally different. It took more than 3 hours (!) and more than a 150 question. </p><blockquote><p>But! Questions were not always linked to the session topic. You can find info about Concurrency or SwiftData also.</p></blockquote><p>Even formatting and session questions gathering took hours for me. So, this is truly unique content. There even was a question regarding where can I found the Q&amp;A session transcript later. The answer is: nowhere except here) </p><p>This <strong>huge amount</strong> with a threads (this also were not so common previously) is taking out-of-email size. Follow the link below for the full list of answers:<br><a href="https://antongubarenko.substack.com/p/swiftui-foundations-build-great-apps?r=21t43r&amp;utm_campaign=post&amp;utm_medium=web&amp;triedRedirect=true">SwiftUI Foundations: Build Great Apps with SwiftUI Q&amp;A</a></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[SwiftUI Foundations: Build Great Apps with SwiftUI Q&A]]></title><description><![CDATA[Answers from Apple Engineers]]></description><link>https://antongubarenko.substack.com/p/swiftui-foundations-build-great-apps</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/swiftui-foundations-build-great-apps</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Tue, 17 Feb 2026 06:58:41 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/2133454a-be6c-4464-bb74-37f19628e325_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently, Apple returned to the webinar format, where engineers answer developers&#8217; questions directly while hosts cover different parts of a topic.</p><p>This time it was <strong><a href="https://developer.apple.com/go/?id=swiftui-foundations-feb-10">SwiftUI foundations: Build great apps with SwiftUI</a></strong>. If the previous session about &#8220;Coding Agents in Xcode&#8221; lasted 16 minutes and had only 3 questions, this one was completely different. It ran for more than 3 hours (!) and included over 100 questions.</p><blockquote><p>But! The questions were not always tied strictly to the session topic. You&#8217;ll also find discussions about Concurrency and SwiftData.</p></blockquote><p>Even formatting and gathering the session questions took hours for me, so this is truly unique content. Grammar and punctuation were slightly adjusted, but the authors&#8217; formatting was kept as is.</p><p>And without further ado &#8212; here&#8217;s the session Q&amp;A.</p><div><hr></div><h2><strong>&#128190;</strong> SwiftData &amp; Backend</h2><h3>Do you think it would be possible to write a ModelContainer targeting a backend other than iCloud, such as Firebase, and still be compatible with SwiftData framework annotations?</h3><p>This is such an awesome question, the answer personally is I do not know, but would love to try to see if can be done, I think it is possible to write a ModelContainer targeting a backend other than SwiftData, however Firebase is a 3rd party framework so we can&#8217;t provide compatibility with SwiftData framework. SwiftData provides a way to abstract the backend and focus on defining models and relationships.</p><p>Start by defining your SwiftData models with the necessary annotations to specify relationships and attributes. Configure the ModelContainer to use your chosen backend.</p><p>It sounds like you can try to make it work In your application, configure the ModelContainer to use Firebase as the persistent store but then will not be store in SwiftData. You can now perform CRUD operations on your models using SwiftData, leveraging the backend of your choice, but I do not work if that can work. I would love to see that working, personally.</p><p>Implement synchronization and conflict resolution mechanisms between SwiftData and your backend sounds like a hard project. This may require additional configuration or logic tailored to your use case. While challenging, I believe the customization and migration could way too complex. Migration is the key word and being able to work in the future versions.</p><div><hr></div><h3>Does SwiftData support data virtualization for large row counts to avoid loading all items</h3><p>SwiftData <code>Query</code> doesn&#8217;t currently support partial fetching. When rendering a large SwiftData dataset, consider using <code>FetchDescriptor</code> with appropriate predicates to paginate the data so you only load the data of the current page.</p><div><hr></div><h3>Can the sorting option in Swift Data queries take a user sort preference from <code>@AppStorage</code>?</h3><p>Yes, <code>SortDescriptor</code> is codable, and so you can definitely persist a sort descriptor to AppStorage, or other kind of storage, and then retrieve it from the storage when needed, and use it with a SwiftData query.</p><div><hr></div><h3>What if I want to store the sort property per trip, so each different trip&#8217;s sort setting is preserved</h3><p>In that case, If your trips grow unbounded you might want to consider persisting the sort options in your data model using SwiftData.</p><div><hr></div><h3>SwiftData and SwiftUI....I&#8217;ve found using <code>@Model</code> and <code>@Query</code> a reliable and easy to use approach for simple CRUD operations for a view. Is this the first go-to recommendation for implementing SwiftData in SwiftUI apps, when basics only are needed?</h3><p>That&#8217;s correct. You can start with <code>@Model</code> and <code>@Query</code>. If you have questions when going deeper, consider asking in the Developer Forums.</p><div><hr></div><h2>&#129695; SwiftUI Views &amp; Layout</h2><h3>Is List backed by UICollectionView? What is the most &#8220;CollectionView&#8221;- like <code>View</code> for SwiftUI?</h3><p><code>List</code> is the most &#8220;CollectionView&#8221;-like View for SwiftUI.</p><div><hr></div><h3>Given a list of cards that are mixed media (list of text rows, images, charts). Should you use Collection, List, ScrollView, or something else?</h3><p>It&#8217;s possible you would need all of them! for direct answers that are relevant to your project, I would make a post on the developer forums and link your code.</p><div><hr></div><h3>What happens with a <code>ViewThatFits</code> when no option actually fits? Does it render neither?</h3><p>Are you talking about <a href="https://developer.apple.com/documentation/swiftui/viewthatfits">https://developer.apple.com/documentation/swiftui/viewthatfits</a> ?</p><p>If a <code>ViewThatFits</code> option in SwiftUI does not find any view that fits within the specified size constraints, it typically renders nothing. This behavior is designed to conserve space by not displaying any content if no suitable view can be accommodated. The <code>ViewThatFits</code> view automatically adjusts its layout based on the available space, but when no view fits, it simply does not render. Check the link I sent you as the documentation is pretty good at how ViewThatFits works.</p><p>And check the sample code in the doc <a href="https://developer.apple.com/documentation/swiftui/viewthatfits">https://developer.apple.com/documentation/swiftui/viewthatfits</a></p><div><hr></div><h3>What might cause Text to sometimes be truncated? Using <code>.fixedSize(horizontal: false, vertical: true)</code> seems to always fix this issue. Is it due to ambiguous layout?</h3><p>It might not have a frame large enough to contain the content until you give it one. You can share the code and project on the forums to get input from engineers around the world. See here <a href="https://developer.apple.com/forums/">https://developer.apple.com/forums/</a></p><div><hr></div><h3>Silly question, for adaptive views why cannot I use <code>ZStack</code> over <code>.overlay()</code> or <code>.background()</code>?</h3><p>I don&#8217;t know what you mean, can you provide an example? Oh sorry use the forums to provide the example as will be a better answer if we can see what you are trying to accomplish as using <code>ZStack</code> over <code>.overlay()</code> or <code>.background()</code> for adaptive views isn&#8217;t necessary because both <code>ZStack</code> and these modifiers are part of SwiftUI&#8217;s declarative layout system and can work seamlessly together to manage stacking and overlaying of views.</p><p>Please provide your sample at the apple forums <a href="https://developer.apple.com/forums/">https://developer.apple.com/forums/</a></p><div><hr></div><h3>What is the best way to make a view inside a <code>ScrollView</code> at least the content size of the <code>ScrollView</code> itself and then it&#8217;s scrollable if larger than that. (Ex. I want to centre some empty/error state text).</h3><p>To achieve this effect, you&#8217;ll want to ensure that the view inside the has a minimum height equal to the height of the itself and grows if the content exceeds this size. With <code>GeometryReader</code> once outside the to capture the device&#8217;s screen height available for scrolling, and once inside to measure the content height. This setup will ensure the text is centered and the view is scrollable if the content exceeds the default screen/scrollable area height.</p><p>I can&#8217;t write code in chat very well, but something like that?</p><pre><code>GeometryReader { geometry in
    ScrollView {
        VStack {
            Spacer() Text("test test.")
                .multilineTextAlignment(.center).padding()
            Spacer() }
    }
}</code></pre><h2>&#128311; Observable &amp; State Management</h2><blockquote><p>Question about project from webinar</p></blockquote><h3>Why is <code>Trip</code> and <code>Activity</code> a class? I would have made them structs and only decorate DataSource with <code>@Observable</code>.</h3><p>You can only place <code>@Observable</code> with a class (not struct). This is because the <code>@Observable</code> macro is designed specifically for reference types (class) to enable observation of property changes. For value types like struct, you should use <code>@State</code> in your view to manage data changes, as structs are designed to be copied rather than shared.</p><div><hr></div><h3>To rephrase my question from earlier. I understand, that only classes can be annotated with <code>@Observable</code>. My question was more pointing towards why you chose class also for <code>Trip</code> and <code>Activity</code> in the sense of that DataSource is already observable, so all its properties are. Or am I getting it wrong?</h3><p>For the sample code, this allowed to pass the objects to views as references instead of copies, so that they could be mutated in place (when editing a <code>Trip</code>, adding activities to a trip, or marking an activity as completed). This made it so we wouldn&#8217;t need to mutate the whole model when a change was made.</p><div><hr></div><h3>Will <code>@StateObject</code> be updated to work with <code>@Observable</code> class without needing to conform the class to Combine&#8217;s <code>ObservableObject</code> protocol? We can&#8217;t use <code>@State</code> because it leaks heap memory, i.e. it inits a new initial object on every View init. And using optional State that&#8217;s init .<code>onAppear</code> is a painful</h3><p>If the object is created in View init and set to a <code>@State</code> property via <code>State(initialValue:)</code>, the new object instance is expected to be immediately freed if the view&#8217;s identity didn&#8217;t change, so leaking heap memory is not expected, and the leak might have been caused by other reasons like retain cycles. If there are concerns to create the <code>@Observable</code> object many times, an alternative would be to create the object higher up and pass it down to this view via a parameter in the init or via the environment.</p><div><hr></div><h3><code>@Environment</code> causes all views that contain it to update? So it is hard to make the views that use it stable? Any and every change causes all views to update ... Correct?</h3><p><code>@Environment</code> in SwiftUI causes views containing it to re-evaluate their state when the associated value changes.</p><p>If the environment value changes frequently or across the app, consider using or directly within the relevant view hierarchy to encapsulate logic and data handling. Use conditional rendering to avoid unnecessary re-renders.</p><p>Yes they cause to update but while provides a powerful mechanism for data sharing, manage its usage carefully to maintain UI stability and responsiveness. By leveraging SwiftUI&#8217;s declarative nature and state management capabilities, minimize unnecessary re-renders and ensure a smooth user experience.</p><p>Main benefit it to be used to share state objects accessed by multiple views.</p><div><hr></div><h3>Can you subclass an <code>@Observable</code> object so that parent and child would have the macro? Example: </h3><pre><code>@Observable
class Trip {}

@Observable class ExtendedTrip: Trip {}</code></pre><p>Are you talking about the sample or in general on SwiftUI? You can subclass an in Swift to create a custom observable object that can be used across the view hierarchy. This allows you to encapsulate your data and logic in a way that can be observed by any views that depend on it.</p><p>This pattern allows you to encapsulate your data and logic in a reusable observable object that can be shared across different views in your SwiftUI app. Like this? </p><pre><code>@ObservedObject var observableObject: CustomObservableObject</code></pre><div><hr></div><h3>In SwiftUI lists, should item model be value types (struct) and rely on parent ViewModel updates, or reference types (<code>Observable/ObservableObject</code>) so each row can update independently? What&#8217;s the recommended trade-off for performance and architecture?</h3><p>This question is being answered in the current session. Please tune into the live Data Flow session to learn more.</p><p>The answer is&#8230; it depends! Use structs when your data is of a value type, and use class when your data contains reference types. When using a class, you typically make it Observable, so a change on the data can update related SwiftUI views.</p><div><hr></div><h3>I want to ask a quite basic question. If we are creating a model we should always be using class instead of struct? And there is no such thing as &#8220;View Model&#8221; then?</h3><p>You normally use struct when your data is of a value type, and use class when your data contains reference types. When using a class, you typically make it <code>Observable</code> so a change on the data can trigger relevant SwiftUI updates.</p><div><hr></div><h3>I have an <code>ObservedObject</code> with Published Properties associated with a View. This is extended into 3 <code>ObservedObjects</code> conforming to a common Protocol. But the properties of the protocol would actually be Published Properties, which is not possible? Can we define a Protocol with such properties?</h3><p>Swift doesn&#8217;t allow adding a stored property to a protocol extension, so if you tend to add published properties to a protocol extension so the types conforming the protocol automatically gain the properties, you can&#8217;t do so. You might consider using an Observable class instead. For more information, see mentioned in the following doc.</p><div><hr></div><h3>Could you explain the difference and why using <code>let</code> and <code>var</code> inside Views. It looks like with <code>let</code> the whole View is redrawn when the value change in the caller View.</h3><p>When developing SwiftUI views, understanding the differences between and is crucial for efficient view updates and correct behavior. <code>Let</code>: Creates a constant value that cannot be modified after initialization. <code>Var</code>: Allows properties to change over time, triggering view updates.</p><p>Use let for stable values that do not change during view updates, ensuring predictable view behavior. Dynamic data in SwiftUI can be managed using <code>var</code> with <code>@State</code> for interactive or changing data, enabling reactive UI updates.</p><p>Mastering <code>let</code> and <code>var</code> in SwiftUI views is crucial for efficient, responsive user interfaces. Use <code>let</code> for immutable data and <code>var</code> with <code>@State</code> for mutable state</p><p>I would recommend to post this question into the forums as the basics of let and var in the views is probably not enough for you.</p><p>Visit Apple Developer Forums here: <a href="https://developer.apple.com/forums/">https://developer.apple.com/forums/</a></p><div><hr></div><h2>&#128655; Navigation</h2><h3>How did you migrate from UIKit to SwiftUI specifically in navigation? Did you have a coordinator patter?</h3><p>It depends on what exactly you&#8217;re trying to achieve here. The coordinator patter works as one way to handle navigation but it&#8217;s not required. The best thing to note, is that if you&#8217;re in UIKit <code>UINavigationController</code> you will want to use the standard push/present calls and present a <code>UIHostingController</code>. And inside a SwiftUI context you will want to use SwiftUI presentation. Mixing the navigation stacks can have unintended consequences. If a coordinator patter helps manage that, that works perfectly but you can also just call the views directly depending on the complexity of the UI and what fits best.</p><div><hr></div><h3>Migrating from UIKit to SwiftUI has two major difficulties, and they are not on View, it&#8217;s navigation and database. Do you have a presentation or a study case for this? How to move from segue navigation to SwiftUI? How to move from Core Data context to Swift Data?</h3><p>A great video for that and resource I like is: <a href="https://developer.apple.com/videos/play/wwdc2023/10189/">https://developer.apple.com/videos/play/wwdc2023/10189/</a></p><p>Migrating from UIKit&#8217;s segue-based navigation to SwiftUI&#8217;s navigation system involves understanding the differences between the two approaches and adapting your navigation logic accordingly.</p><p>To migrate to SwiftUI, you&#8217;ll need to create a SwiftUI view that represents the same flow. Instead of using a navigation controller, you&#8217;ll use SwiftUI&#8217;s navigation views.</p><p>Also this is my go to <a href="https://developer.apple.com/documentation/swiftui/building-a-document-based-app-using-swiftdata">https://developer.apple.com/documentation/swiftui/building-a-document-based-app-using-swiftdata</a></p><p>Bring core data to swiftUI <a href="https://developer.apple.com/videos/play/wwdc2021/10017/">https://developer.apple.com/videos/play/wwdc2021/10017/</a></p><p>And remember to model your data <a href="https://developer.apple.com/videos/play/wwdc2023/10195/">https://developer.apple.com/videos/play/wwdc2023/10195/</a></p><p>I am not an expert in SwiftData so I would recommend to ask that question and be specific of what you want migrate in another question for our expert to pick up.</p><div><hr></div><h3>How can we best provide async navigation links? This seems to be a fundamental requirement. I built a custom built <code>AsyncNavigationLink</code> that overlays a button on a <code>NavigationLink</code>, triggers an async call, overlays a <code>ProgressView()</code>. The async call triggers the <code>NavigationLink</code>. Is this the best way?</h3><p><code>NavigationLink</code>s themselves can&#8217;t be responsible for asynchronous handling. The destination view can have a <code>ProgressView</code> and react to asynchronous changes to your model. But, since <code>NavigationLink</code> is a view itself, it needs to be processed on the main actor. If you want to present the <code>ProgressView</code> <em>before</em> moving to the new view, you can make a <code>Button</code> that triggers the asynchronous callback, and then update your navigation path (that you can set with <code>NavigationStack(path: $path)</code>) when it is finished. This will use the <code>.navigationDestination(for:)</code> modifiers to decide the views to move to. Not sure if this entirely answers your question (</p><div><hr></div><h3>Navigation in Swift is one of the topics I&#8217;ve struggled with for a while. Whether using <code>NavigationDestination</code> or building your own Router to navigate. What is your recommendation for doing such, especially when you have multiple views that you want to push? Any suggestions?</h3><p>The developer website has tons of documentation on recommended navigation styles: See <a href="https://developer.apple.com/documentation/swiftui/bringing-robust-navigation-structure-to-your-swiftui-app">https://developer.apple.com/documentation/swiftui/bringing-robust-navigation-structure-to-your-swiftui-app</a> and <a href="https://developer.apple.com/documentation/swiftui/navigation">https://developer.apple.com/documentation/swiftui/navigation</a></p><div><hr></div><h2>&#128736;&#65039; UIKit &amp; SwiftUI Integration</h2><h3><code>HostingConfiguration</code> or <code>HostingController</code> ? On <code>TableView</code> and <code>CollectionView</code> , what&#8217;s best way to wrap SwiftUI code in cells? Is there difference between hosting config and hosting controller?</h3><p>For wrapping a SwiftUI hierarchy in <code>UICollectionViewCell</code> or <code>UITableViewCell</code> objects, <code>UIHostingConfiguration</code> is the intended class. Please see the documentation for details: <a href="https://developer.apple.com/documentation/swiftui/uihostingconfiguration/">https://developer.apple.com/documentation/swiftui/uihostingconfiguration/</a> UIHostingController is intended to be used more broadly for SwiftUI views in general.</p><div><hr></div><h3>What is the best way to integrate SwiftUI into an existing UIKit App, keeping the performance of SwiftUI Previews high? I can use Swift Package Manager to integrate packages that contain SwiftUI components into the project. Is this the best way or are there simpler paths to go for best speed?</h3><p>To use SwiftUI views in your UIKit app. See UIKit integration <a href="https://developer.apple.com/documentation/swiftui/uikit-integration">https://developer.apple.com/documentation/swiftui/uikit-integration</a></p><div><hr></div><h3>I&#8217;d like to re-ask the question by Joshua Arnold about resizing with <code>UIViewRepresentable</code>, as the link provided does not give an answer as to how to animate the size change</h3><p>The reference is at the bottom of the linked page, see Adding UIKit to SwiftUI View Hierarchies <a href="https://developer.apple.com/documentation/swiftui/uiviewrepresentable#Adding-UIKit-views-to-SwiftUI-view-hierarchies">https://developer.apple.com/documentation/swiftui/uiviewrepresentable#Adding-UIKit-views-to-SwiftUI-view-hierarchies</a></p><p>You can also post your code, images and more on the forums to receive direct help <a href="https://developer.apple.com/forums/">https://developer.apple.com/forums/</a></p><div><hr></div><h3>When using <code>UIViewRepresentable</code>, is it possible for the <code>UIView</code> to internally resize itself, and have SwiftUI animate the change in size? If I call <code>invalidateIntrinsicContentSize()</code> on the view, SwiftUI resizes the view immediately without an animation.</h3><p>You must communicate size changes back to SwiftUI state for animations to work. For info how see Adding UIKit views to SwiftUI view hierarchies <a href="https://developer.apple.com/documentation/swiftui/uiviewrepresentable">https://developer.apple.com/documentation/swiftui/uiviewrepresentable</a></p><div><hr></div><h3>What is Apple&#8217;s recommended approach for detecting when a <code>UIHostingConfiguration</code> / <code>ContentView</code> changes size internally? If I update the a <code>UIHostingConfiguration</code> with a larger view, the view will just truncate / clip instead of bubbling up to UIKit to resize itself.</h3><p>When working with a hybrid UIKit and SwiftUI environment, SwiftUI views may change size internally without automatically updating their containing UIKit views. This is because SwiftUI views are declarative and may not notify UIKit of size changes beyond their initial setup. To address this, you can use SwiftUI view modifiers and UIKit mechanisms to propagate size changes.</p><p><code>GeometryReader</code> to read the current size of a SwiftUI view and communicate changes back to UIKit.</p><p>I would personally wrap a SwiftUI view in a <code>UIViewRepresentable</code> and use a coordinator to listen for size preference changes and update the UIKit view size accordingly.</p><p>in a UIKit view controller or view to adjust the layout so observe notifications if possible</p><p>Visit Apple Developer Forums here: <a href="https://developer.apple.com/forums/">https://developer.apple.com/forums/</a></p><div><hr></div><h2>&#128311; ScrollView &amp; Visibility</h2><h3>Is there any further documentation or reliable ways to know if something is actually visible on screen? Based our experiences, onAppear in <code>VStack</code> in ScrollView, List, and LazyVStack all have different onAppear behaviors for their elements.</h3><p>Please check out the <code>.onScrollVisibilityChange</code> <a href="https://developer.apple.com/documentation/swiftui/view/onscrollvisibilitychange(threshold:_:)">modifier</a>.</p><div><hr></div><h3>Is there a suggested way to determine the difference between <code>.onAppear</code> being called in a &#8216;forward&#8217; transition vs a &#8216;return&#8217; transition? i.e. list displayed vs returning to list from details view.</h3><p>Yes, it is possible to differentiate between when <code>.onAppear</code> is called during a forward transition versus a return transition in SwiftUI, though it requires some manual state management since SwiftUI itself does not provide built-in distinction for these cases directly within <code>.onAppear.</code> I got some sample code so you can provide a more detailed information about your question.:</p><pre><code>.onAppear { 
    if appearedOnce {
            print(&#8221;Main View reappeared (back navigation)&#8221;) 
    } else { 
        print(&#8221;Main View appeared (forward navigation)&#8221;)
        appearedOnce = true 
    } 
}</code></pre><div><hr></div><h3>If I have a database with 100k+ rows, how do I manage paging/infinite scroll in SwiftUIs &#8220;List&#8221;, to manage memory appropriately. Similar to <code>UITableViews</code> prefetch handler.</h3><p>Similar the the <code>VStack</code> and <code>HStack</code> shown on stage, there is a <code>LazyVStack</code> and <code>LazyHStack</code> which automatically allocate and deallocates views as they are needed on screen. For more info see &#8220;Creating performant scrollable stacks&#8220;: <a href="https://developer.apple.com/documentation/swiftui/creating-performant-scrollable-stacks">https://developer.apple.com/documentation/swiftui/creating-performant-scrollable-stacks</a></p><div><hr></div><h2>&#129419; Animation &amp; Effects</h2><h3>For the <code>symbolEffect(_:options:value:)</code> method for SF symbols, the animation only runs when the value changes. Is there a way to implement this behavior (only run something once for each unique value) idiomatically in SwiftUI?</h3><p>Depending on the use case, this might not require explicit treatment to run an animation (e.g. the <code>animation(value:)</code> modifier might be all you need). But if you do need to perform side effects based on value changes, <code>onChange(of:...)</code> might be right to reach for.</p><div><hr></div><h3>I&#8217;ve added an animation to a Button but when it&#8217;s placed inside a <code>NavigationStack&#8217;s</code> <code>ToolbarItem</code>, the animation doesn&#8217;t run. When the <code>Button</code> is used elsewhere, the animation works as expected. Can you share how to use this animating <code>Button</code> inside a <code>.toolbar</code>?</h3><p>This is a good question for the forums. Given a <code>Button</code> inside a <code>.toolbar { }</code> modifier, we&#8217;d need to see a fuller code sample to know things like where the animation is declared.</p><div><hr></div><h3>How can I start an animation after another animation? (without using hard coded durations)</h3><p>A <code>KeyframeAnimator</code> or <code>PhaseAnimator</code> can be used for more complex animations! In <code>KeyframeAnimator</code>, for example, you can have separate steps in a <code>KeyframeTrack</code>, and separate tracks in a <code>KeyframeTimeline</code>. Helpful docs: <a href="https://developer.apple.com/documentation/swiftui/controlling-the-timing-and-movements-of-your-animations">https://developer.apple.com/documentation/swiftui/controlling-the-timing-and-movements-of-your-animations</a> <a href="https://developer.apple.com/documentation/swiftui/keyframetimeline">https://developer.apple.com/documentation/swiftui/keyframetimeline</a></p><div><hr></div><h2>&#9935;&#65039; Modifiers &amp; Customization</h2><h3>Is it possible to write modifier functions that can configure a custom view property even if it is contained in other container views? This would be similar to how <code>.font</code> works, in that it can be applied to a view and any contained <code>Text</code> will be reconfigured?</h3><p>One possible way to achieve this is to use a type such as <code>Group</code> that can create a collection of views, such that its modifiers apply to all of its member views: <a href="https://developer.apple.com/documentation/swiftui/group">https://developer.apple.com/documentation/swiftui/group</a></p><div><hr></div><h3>UIKit is inheritance-driven, so it is possible to subclass an <code>UIButton</code>, for example. The same is not possible with SwiftUI. What is the recommended alternative for a similar approach (mainly to customize appearance)? Style, viewModifiers, custom view, something else?</h3><p>Custom view modifiers are one way in which you can reuse multiple view modifiers across different views. You can even extend the View protocol with a function that applies your custom modifiers. Check out &#8220;Reducing view modifier maintenance&#8221; for how to do that. <br><a href="https://developer.apple.com/documentation/swiftui/reducing-view-modifier-maintenance">https://developer.apple.com/documentation/swiftui/reducing-view-modifier-maintenance</a></p><div><hr></div><h3>Is it possible to add a view modifier for the text width and set it to expanded as well as making it italic? The following code does not work, and italic is only applied if the width is .<code>standard:</code></h3><pre><code>.font(.caption2.width(.expanded).italic())
   .fontWeight(.bold)
   .frame(maxWidth: width, alignment: .leading)
   .italic(italic)</code></pre><p>Something like that too: <code>.font(.caption2)</code></p><p>it is possible to combine text width and italic styling in SwiftUI using a custom view modifier. The issue with your current code is that the modifier in SwiftUI does not support applying both and modifiers directly in a combined way like you&#8217;re attempting. Instead, you can create a custom view modifier to achieve this behavior</p><p>If I put it together should be something like:</p><pre><code>content .font(.caption2) .fontWeight(.bold).frame(maxWidth: width, alignment: .leading) .italic(italic)</code></pre><p>Disclamer I didn&#8217;t test it, but I think is the idea.</p><p>Allows you to flexibly adjust the text&#8217;s width and apply italic styling independently within a single view modifier.</p><div><hr></div><h3>What&#8217;s the best approach to building modifiers in SwiftUI? I am specifically interested in how to handle the inert state. Is it better to apply the modifier conditionally depending whether it&#8217;s inert state or not. Or is it better to deal with the sate inside the modifier implementation.</h3><p>Generally, try to avoid conditionally applying modifiers (specifically when the condition can change at runtime). It is better to handle an &#8220;inert&#8221; state from within the modifier. If you have a specific question about building a modifier that handles some state, feel free to send a question for that as well!</p><div><hr></div><h2>&#128270; Performance &amp; Debugging</h2><h3>I&#8217;m trying to debug a log about &#8220;Attribute Graph Cycle&#8221;. I&#8217;ve tried instruments, but it&#8217;s hard to identify the source. In my case, I think it has to do with frames/geometry negotiating sizes. I don&#8217;t notice a major slowdown - Is this even a problem?</h3><p>Check if you have a custom layout or <code>onGeometryChange</code> that could be affecting your View&#8217;s sizing. Instruments can help show the catalyst of an update (through the Cause-and-Effect graph). Hitting a cycle may lead to unexpected behavior (aside from just performance issues/hangs), so it is still important to identify if you can. If you don&#8217;t see anything in your program that could be causing a loop, consider filing a bug report on Feedback Assistant!</p><div><hr></div><h3>Is there a way to see SwiftUI&#8217;s dependency graph for a view, so we can figure out whats causing excessive updates etc? Is instruments the best way to do this?</h3><p>Instruments is the best way to figure out what is causing excessive updates and other issues with performance. Check out &#8220;<a href="https://developer.apple.com/videos/play/wwdc2025/306/">Optimize SwiftUI performance with Instruments</a>&#8221; for a lot of great information about SwiftUI and how to use Instruments to profile and optimize your app.</p><div><hr></div><h3>Can we see the view&#8217;s dependency graph in Xcode? Via debugging?</h3><p>If you profile using SwiftUI template in Instruments, it shows you information such as long view body updates, cause-and-effect graph, update groups etc. please review the <a href="https://developer.apple.com/documentation/Xcode/understanding-and-improving-swiftui-performance">Understanding and improving SwiftUI performance</a> article to learn how to use instruments to understand and debug your SwiftUI Views.</p><div><hr></div><h3>When is it better to extract a view into a separate <code>ViewBuilder</code> function/computed property versus creating a separate struct View? What are the performance implications of each approach?</h3><p>Extracting your views into a separate view structs is a good way to, reduce dependencies to only those that matter to the view. This makes your code easier to read, and your dependencies become apparent at its use site. Ultimately you want to use your best judgement here because not every dependency deserves to be scoped to a separate view struct.</p><p>When views are scoped to separate view struct, try reducing view values to only the data they actually depend on. This could mean fewer updates and that implies better performance when data changes in your app.</p><div><hr></div><h3>During SwiftUI view reordering or layout updates, what typically causes temporary disappearance of views, and how can state consistency be preserved during rapid UI changes?</h3><p>Having views temporarily disappear isn&#8217;t typical. Check out the tips in &#8220;<a href="https://developer.apple.com/videos/play/wwdc2025/306/">Optimize SwiftUI performance with Instruments</a>&#8221; to determine if there are some performance issues or tuning that you can do to improve the view. If that doesn&#8217;t help, please post your specific issue on the Developer Forums for more assistance.</p><div><hr></div><h3>One of the great things I find in working with SwiftUI is that you can build most of your app staying in Xcode with Preview. I loved the last section about using <code>print</code> in view, but it is only at runtime. Is there a library that let you use a debug overlay modifier to display debug data in a view?</h3><p>You can output into a Label? And there are many views where output is allowed. But the NSLOG is a way to go to the console. You can add a GeometryReader within a view&#8217;s <code>.overlay()</code></p><p>SwiftUI provides a convenient way to display debug data</p><p>There is a video that may help you: <a href="https://developer.apple.com/videos/play/wwdc2023/10226">WWDC23: Debug with structured logging </a></p><p>I would recommend this doc as well <a href="https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/DebuggingTricksandTips.html">https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/DebuggingTricksandTips.html</a></p><div><hr></div><h3>Are there tools for diagnosing layout issues? On macOS, my app behaves very strangely, growing the window arbitrarily even though subviews&#8217; sizes aren&#8217;t changing.</h3><p>Hi, you might find the following guide helpful: &#8220;<a href="https://developer.apple.com/documentation/xcode/diagnosing-issues-in-the-appearance-of-your-running-app">Diagnosing issues in the appearance of a running app</a>&#8221;.</p><div><hr></div><h2>&#127969; Dismissal &amp; Environment</h2><h3>What would be the difference in using the <code>@Enviroment(.dismiss)</code> or making it a binding when dismissing a sheet? Is there performance benefit?</h3><p>Both and make sheet dismissal in SwiftUI, but they differ in functionality and performance. It is a property wrapper that exposes the modifier applied to a view. It&#8217;s often used to dismiss views or sheets from outside their hierarchy, like from a parent view or navigation stack. It is efficient as it doesn&#8217;t create additional state or UI components. It simply exposes an existing closure in the view hierarchy, involves defining a closure for dismissal and wrapping it in a . This is useful for more control or integrating with custom views that require explicit dismissal but creating a binding has some overhead, but it&#8217;s usually negligible. The impact is minimal compared to other UI operations.</p><p>In summary, and are efficient is straightforward for dismissing views from outside their hierarchy, while binding provides more control and flexibility for custom dismissal logic. Performance differences are minimal I think, and the choice depends on your application&#8217;s requirements.</p><div><hr></div><h2><strong>&#127963;&#65039;</strong> Architecture &amp; Patterns</h2><h3>Is MVVM organization recommended for a SwiftUI project?</h3><p>Different apps need different architectures. An app with heavily server-driven content should make architectural decisions specific to that design. An app focused more on displaying data locally (and might not be updating its content as frequently) should use an architecture that makes that use case easier to build for. SwiftUI is architecture agnostic to allow deliberate architectures that work for your app.</p><h3>When passing in an action into a view is it better to use closure or function/method reference? Example:  </h3><pre><code>SomeView(saveAction: { viewModel.performSave() }) 
vs
SomeView(saveAction: viewModel.performSave)</code></pre><p>Great question and thanks for providing the code. What I think is whether to use a closure or a function/method reference when passing an action into a view depends on your specific use case and personal preference. Both approaches have their advantages, and the choice might be influenced by factors like readability, performance, and Swift features. You can capture specific variables from the surrounding context using a capture list if needed, providing more flexibility. It allows for quick inline modifications or additional logic without needing to create a new method elsewhere. Cleaner syntax, especially for simple method calls, improving readability. Potentially more performant since it directly references the function without additional closure overhead?</p><div><hr></div><h2>&#128030; iOS Issues &amp; Bug Reporting</h2><h3>Since iOS26, dismissing the keyboard stopped working as expected. If I have a <code>TextField</code> focused in a <code>navigationDestination</code> view or in the content view of a <code>fullScreenCover</code>, and then dismiss the view, the keyboard would sometimes leave a white bar at the bottom in the safeArea. Is this a known issue?</h3><p>If you come across any visual issues while using SwiftUI, please feel free to file a bug report via Feedback Assistant. Thanks for letting us know about this! <a href="https://developer.apple.com/bug-reporting/">https://developer.apple.com/bug-reporting/</a></p><div><hr></div><h2>&#127891; Learning Resources &amp; Miscellaneous</h2><h3>What&#8217;s the best way to learn how to use Metal, particularly for visual effects rather than graphics?</h3><p>This is just my approach, I like to take apart and study sample code. For Metal, there is an entire database of sample code here <a href="https://developer.apple.com/metal/sample-code/">https://developer.apple.com/metal/sample-code/</a></p><p>Also, please check out the segment on Metal in this talk from WWDC24, &#8220;<a href="https://developer.apple.com/videos/play/wwdc2024/10151/?time=1180">Create custom visual effects with SwiftUI</a>&#8221;.</p><div><hr></div><h3>Love to learn more about SwiftUI and Metal - for video rendering would the performance be faster using a custom shader with [[stitchable]] and SwiftUI vs NSView or MkitView?</h3><p>It depends on the purpose! <code>[[stitchable]]</code> Metal shaders can be used to apply an effect to an existing SwiftUI view via the <code>colorEffect(_:isEnabled:)</code>, <code>distortionEffect(_:,maxSampleOffset:isEnabled)</code>, and <code>layerEffect(_:maxSampleOffset:isEnabled:)</code> modifiers. If you&#8217;re looking for a more flexible view that displays Metal objects (i.e. drawing your own shapes with <code>vertex</code> and <code>fragment</code> shaders), you should probably lean on <code>MTKView</code>. <code>MTKView</code> is provided by MetalKit and can be placed in an <code>NSViewRepresentable</code> or <code>UIViewRepresentable</code>. One is not strictly faster than the other, and you should choose whichever method matches your vision! MTKView: <a href="https://developer.apple.com/documentation/metalkit/mtkview">https://developer.apple.com/documentation/metalkit/mtkview</a> SwiftUI Shader: <a href="https://developer.apple.com/documentation/swiftui/shader">https://developer.apple.com/documentation/swiftui/shader</a></p><div><hr></div><h3>What do your designers (in Apple) use to design for passing on to developers? Whenever I&#8217;ve had designs from Figma, Sketch, Azure, Miro, whatever, they aren&#8217;t true to platform and don&#8217;t correctly confine the designer to use native components.</h3><p>This is a good question! It can be hard for design tools to match the fidelity of the look and feel of Apple platforms. Apple provides design resources for Sketch and Figma at <a href="https://developer.apple.com/design/resources/">https://developer.apple.com/design/resources/</a> and the materials are adjusted to resemble iOS as closely as possible.</p><p>In your partnership with developers, check in with the engineers and ensure that the designs are well understood before they are implemented. It also helps when everyone communicates using the same language, so we recommend that both developers and designers (and even other members of the team) familiarize themselves with the Human Interface Guidelines for the platform your app supports. The Human Interface Guidelines are available at <a href="https://developer.apple.com/design/human-interface-guidelines/">https://developer.apple.com/design/human-interface-guidelines/</a></p><div><hr></div><h3>I had some trouble finding the entry point of sessions like &#8220;Code-along: Start building with Swift and SwiftUI&#8221; on the developer website. Is there an entry point where I can navigate thru all the sessions like &#8220;code along&#8221; and today&#8217;s or previous sessions?</h3><p>Thanks for the question. I recommend to start with this video: <a href="https://developer.apple.com/videos/play/wwdc2021/10062/">https://developer.apple.com/videos/play/wwdc2021/10062</a>, then a code along is always good like: <a href="https://developer.apple.com/videos/play/meet-with-apple/237/">https://developer.apple.com/videos/play/meet-with-apple/237/</a> and do not forget the what&#8217;s new to avoid using the older API <a href="https://developer.apple.com/videos/play/wwdc2024/10136/">https://developer.apple.com/videos/play/wwdc2024/10136/</a></p><div><hr></div><h3>As an absolute beginner, what would be the first go-to step to go for training? or documentation? on Apple.</h3><p>A great place to learn how to develop for Apple is through Pathways (<a href="https://developer.apple.com/pathways/?cid=ht-pathways">https://developer.apple.com/pathways/?cid=ht-pathways</a>), your first step to creating for Apple platforms! For documentation, here is the top-level page: <a href="https://developer.apple.com/documentation/">https://developer.apple.com/documentation/</a></p><div><hr></div><h3>Do you have any advice for someone with Flutter/Dart experience starting SwiftUI?</h3><p>Thanks for the question. I think we got something similar from someone else, but welcome to Swift coming from Flutter! To start I would recommend to start with this video: <a href="https://developer.apple.com/videos/play/wwdc2021/10062/">https://developer.apple.com/videos/play/wwdc2021/10062/</a> then a code along is always good like: <a href="https://developer.apple.com/videos/play/meet-with-apple/237/">https://developer.apple.com/videos/play/meet-with-apple/237/</a> and do not forget the what&#8217;s new to avoid using the older API <a href="https://developer.apple.com/videos/play/wwdc2024/10136/">https://developer.apple.com/videos/play/wwdc2024/10136/</a> But welcome to Xcode! You are going to love it.</p><div><hr></div><h3>Any recommendations for resources (books, WWDC sessions, sample repos, or articles) on doing test-driven development with Swift/SwiftUI&#8212;especially patterns for testing view models/presenters and keeping UI code thin?</h3><p>There&#8217;s several WWDC sessions that may be beneficial to you, <a href="https://developer.apple.com/videos/play/wwdc2024/10195">https://developer.apple.com/videos/play/wwdc2024/10195</a> and <a href="https://developer.apple.com/videos/play/wwdc2019/413">https://developer.apple.com/videos/play/wwdc2019/413</a></p><div><hr></div><h3>There are tons of modifiers, how we suppose to memorize all of them or even know that they are exists? &#128522;</h3><p>The Developer Documentation is always a great place to start! <a href="https://developer.apple.com/documentation/swiftui">https://developer.apple.com/documentation/swiftui</a> You can also look into bundling your commonly-used modifiers into custom view modifiers- check out &#8220;<a href="https://developer.apple.com/documentation/swiftui/reducing-view-modifier-maintenance">Reducing view modifier maintenance</a>&#8221;.</p><div><hr></div><h2>&#128311; Wishlist App Specific (App from the webinar)</h2><h3>Where can I download the wishlist app xcode project?</h3><p>Thank you for your question and attending the event. The sample code for &#8220;SwiftUI foundations: Build great apps with SwiftUI&#8221; will be made available on the Apple developer website sometime after the event concludes. Stay tuned!</p><div><hr></div><h3>Will the code for Wishlist app be made available?</h3><p>Thank you for your question and attending the event. The sample code for &#8220;SwiftUI foundations: Build great apps with SwiftUI&#8221; will be made available on the Apple developer website sometime after the event concludes. Stay tuned!</p><div><hr></div><h3>Why cardsize is provided for <code>TripCard</code> in example of Wishlist struct presented in motion presentation ?</h3><p><code>TripCard</code> could either be represented in different sizes, either as a compact or expanded card with a larger width.</p><div><hr></div><h3>I noticed the Wishlist sample app has a customized font in the <code>NavigationBar&#8217;s</code> title. Curious how this was accomplished?</h3><p>For a navigation bar title with styling, you can create a custom view containing a Text with styling modifiers, then wrap it in <code>ToolbarItem(placement: .largeTitle)</code> or <code>ToolbarItem(placement: .title)</code> on the <code>.toolbar</code>.</p><div><hr></div><h3>Is the Wishlist app MultiPlatform for iPhone, Watch, TV, Mac, etc?</h3><p>Thanks for the question. The app is currently an iOS / iPadOS app. You can run it on macOS and visionOS in Designed for iPad mode.</p><div><hr></div><h2>&#128172; Text &amp; Fonts</h2><h3>Any recommendations to have a more readable or idiomatic alternative to <code>.frame(maxWidth: .infinity, alignment: .leading)</code>?</h3><p>Certainly! If you&#8217;re looking for a more readable or idiomatic alternative to , it depends on the context and the layout you&#8217;re working with. If you&#8217;re using Auto Layout, you can define constraints programmatically. This might be more readable if you&#8217;re comfortable with constraints, use that. If you have a container view that naturally supports full-width content, you might use a container view to manage the layout. If you need more control over the frame, you might define a custom frame Choose the method that best fits your design system and the constraints of your project. Auto Layout is often preferred for its flexibility and control, especially in UIKit. But I personally like adding a <code>Spacer()</code> into the view!.</p><div><hr></div><h3>I would like to follow up on the question on how to add a different font in the <code>navigationTitle</code>. While it is true, that you can add a styled text to the modifier. The result will will compile but not change its appearance and show an error: &#8220;Only unstyled text can be used with <code>navigationTitle(_:)</code>&#8221;.</h3><p>You can create a <code>ToolbarItem(placement: .title/.largeTitle)</code> that is a <code>Text</code> styled with <code>AttributedString</code>.</p><div><hr></div><h2>&#128208; Geometry &amp; Sizing</h2><h3>What if a subview prefers a bigger HxW than what is offered by the parent view? How are the conflicts handled?</h3><p>I&#8217;d recommend testing this out, then adding other modifiers to build intuition for SwiftUI&#8217;s layout system! </p><pre><code>struct BiggerChild: View { 
  var body: some View {
     VStack {
       Color.orange .frame(width: 200, height: 200) .opacity(0.3) 
      }
      .frame(width: 100, height: 100).background(.purple) 
    } 
}</code></pre><p>You&#8217;ll find that the parent won&#8217;t clip the child, and the child will extend beyond the parent. From there, check out the <code>clipShape()</code> modifier.</p><div><hr></div><h3>Is there a canonical way to set .frame sizes that are dynamic to device sizes? For example, <code>.frame(height: oneThirdOfDeviceHeight)</code>? <code>UIScreen</code>? Geometry reader? Is there a preferred approach?</h3><p>To set dynamic frame sizes in Swift for different devices, you can calculate dimensions based on screen width and a predefined ratio to maintain aspect ratios. The GeometryReader approach is modern and powerful, defining views in terms of available space. For complex layouts, Auto Layout provides flexible constraints that adapt to device sizes. For simplicity and direct access, using or similar methods works well for basic dynamic sizing. For more complex layouts and better responsiveness, Auto Layout is more powerful. Constants and computed properties help keep code organized and maintainable.</p><p>I think I understand your question with the frames utilization but please feel free to ask the question again with more details.</p><div><hr></div><h3>I have the following: </h3><pre><code>LazyVGrid { 
  ForEach { 
    VStack { 
      Image() 
      Text() 
    } 
   } 
}</code></pre><h3>Now, because the text of each view differs (sometimes it takes 2 lines, sometimes 1 line and sometimes 3 lines) the views are no longer aligned on a base-line. How can I get the views aligned again at a common base-line.</h3><p>It could be the way your <code>LazyVStack</code> is deallocating views. I think this would be a great question for the Developer Forums. I encourage you to post this question there so we can more easily share code and others with the same issue can find help for the same issue. Visit Apple Developer Forums here: <a href="https://developer.apple.com/forums/">https://developer.apple.com/forums/</a></p><div><hr></div><h3>If I have a view thats 50px x 50px, and I want to display many of them in a HStack, evenly spaced so they are edge to edge. Whats the most performant way to achieve this? I&#8217;ve been using a &#8220;<code>Spacer()</code>&#8221; inbetween each, but wondering if thats causing overhead</h3><p>Best way to see the best perform control is to debug and use instruments to check your UI as has SwiftUI instruments as well. Using between views in an is a common approach, but if you&#8217;re concerned about performance due to a large number of views, you might consider a more direct calculation to distribute space evenly. Lately I have been a huge fan of <code>GeometryReader</code> as It is used to get the available width of the parent view. Calculate the total width occupied by the views and subtract it from the available width to get the total spacing needed. Divide this by the number of gaps (which is one less than the number of views).</p><p>But Space is really good, I would really recommend you to make Instruments your best friend.</p><p>Visit Apple Developer Forums here: <a href="https://developer.apple.com/forums/">https://developer.apple.com/forums/</a></p><div><hr></div><h3>Any performant alternatives to geometry reader? I have to put a vertically centered textfield, with a helper text below it, but also add pull to refresh, so I have added a scrollview.refreshable, one alternative is to create a manual drag gesture, but can we achieve this using scrollview?</h3><p>This depends on what you&#8217;re trying to achieve. For example, if you&#8217;re trying to respond to scroll view changes you should checkout the <a href="https://developer.apple.com/documentation/swiftui/view/onscrollgeometrychange(for:of:action:)">onScrollGeometryChange</a>, <a href="https://developer.apple.com/documentation/swiftui/view/onscrolltargetvisibilitychange(idtype:threshold:_:)">onScrollTargetVisibilityChange</a>, <a href="https://developer.apple.com/documentation/swiftui/view/onscrollvisibilitychange(threshold:_:)">onScrollVisibilityChange</a><br> and <a href="https://developer.apple.com/documentation/swiftui/view/onscrollphasechange(_:)">onScrollPhaseChange API</a>.</p><div><hr></div><h2>&#128252; Images &amp; Media</h2><h3>I&#8217;d like to have a zoomable and draggable image within a scrollview, like you would see in the photos app. Can this be done with SwiftUI?</h3><p>We can provide information how Apple apps controls work, you can achieve a zoomable and draggable image within a in SwiftUI using the modifiers and the and views.</p><pre><code>.gesture( DragGesture() .onChanged { value in 
  print(&#8221;Dragging at: (value.location)&#8221;) 
}.onEnded { value in 
  print(&#8221;Drag ended at: (value.location)&#8221;)
})</code></pre><p>Other this are <code>ScrollView</code>, creates a horizontal <code>ScrollView</code> without showing the scroll indicator bar.</p><h3>If I present a series of Image views in a <code>LazyVStack</code> -- Image views that need to be resized. How can I make sure all images get resized to the same width/hight (a common denominator) and don&#8217;t end-up looking different?</h3><p>To ensure consistent sizing of views in a series, explicitly set modifiers on each view to define a specific width and height for all images. Decide if you want to maintain the aspect ratio by using modifiers to control how images fit within their frames. This makes images scalable, sets a fixed size for each image, and clips images that exceed frame bounds for neatness.</p><p>Great question for sure! Make sure when displaying a series of views in a and ensuring they all have the same dimensions so you do not need to set constraints.</p><p>Visit Apple Developer Forums here: <a href="https://developer.apple.com/forums/">https://developer.apple.com/forums/</a></p><div><hr></div><h2>&#127912; Colors &amp; Themes</h2><h3>How do create themes in a SwiftUI app. A theme that can hold typography, colors, spacing. How do we handle a case with multiple themes. And a case where the theme is fetched from the backend.</h3><p>You can create a file with all your application constants, and use conditional logic in the View to select which constant you need on a view by view basis, or pick up on environments such as light mode, dark mode or orientations.</p><p>If you need help, make a post on the forums!</p><div><hr></div><h3>If you use a non-semantic color how do you make sure it works in dark mode?</h3><p>I&#8217;d consider checking the colorScheme environment and using an appropriate color based on the current dark/light mode. See the <a href="https://developer.apple.com/documentation/swiftui/environmentvalues/colorscheme">following doc</a> for more details.</p><div><hr></div><h3>For designing is there a best practice for example the title in the trip detail is white but what if the photo is having a white color where the text is going to render, or there is a way to make it adaptable depending on the background to create the right contrast</h3><p>If the title sits on top of scrollable content, for example a toolbar or safe area bar, the color automatically adapts to the content beneath it.</p><div><hr></div><h3>Maho mentioned a neon color. Since it is not part of the system colors, how do we create it? Do we have to provide the light, dark and increased contrast versions?</h3><p>How to add a color literal to an Xcode project is discussed here: After adding a color to your asset catalogs, you can set up the color variants, and also set the hex value of the color. The Wishlist sample will be made available on the Apple developer website sometime after the event concludes. You can download it and look into the details of the color.</p><div><hr></div><h3>What impact does use of Color have on battery life? How can we override dark mode?</h3><p>Have you use the Instruments from Xcode3? I think like colors with higher saturation tend to require more brightness to be perceived clearly, which can drain a battery faster? But you can use the battery impact into the Instruments app to give you clear answers but using color and avoiding color. Remember that while overriding dark mode can help with visibility and comfort in bright environments, it may increase battery consumption but Instruments here are your best friend.</p><p>Visit Apple Developer Forums here: <a href="https://developer.apple.com/forums/">https://developer.apple.com/forums/</a></p><div><hr></div><h2>&#128202; Charts &amp; Performance</h2><h3>Following up on my Chart question. The chart is showing live data showing over time. I tested with instruments and tracked it down to the chart that is causing it. My current plan is to use Canvas instead for better performance but wanted to confirm I&#8217;m not missing something with Swift Charts</h3><p>If you have spotted out that Swift Charts is the performance bottleneck, yeah, drawing with Canvas with your own code may help, because that way you can only draw the content specific to your needs. But again, we will be interested in looking into more details about your use case. I suggest that you ask this question in our Developer Forums with your code snippets and analysis so SwiftUI folks can follow up from there. Thanks.</p><div><hr></div><h3>I have been trying to test Swift Charts with some sample tests. My use case requires updating the chart continually (at least a few times a second). For 200 data points I&#8217;m seeing around 40% CPU usage which is a lot for my use case where I might have multiple charts displaying at the same time.</h3><p>For a performance issue, I&#8217;d typically start with using Instruments to profile the app and figure out the performance bottleneck, and then see how it can be improved. The following doc may be worth reading: - Understanding and improving SwiftUI performance In your case, you might also consider if &#8220;at least a few times a second&#8221; is really needed and brings a lot of value. Refreshing a chart that frequently does consume resources.</p><div><hr></div><h2>&#128671; Platform Specific</h2><h3>I have a SwiftUI view which displays a List, with a few labels and an image. The list row height should be consistent for each item. When the list items grows large, the same code performs just fine on iOS, but starts lagging on macOS (26.2). What explains this?</h3><p>It is difficult to say for a specific app what would cause the difference. In general, first use Instruments to profile the app and optimize its performance. Check out &#8220;<a href="https://developer.apple.com/videos/play/wwdc2025/306/">Optimize SwiftUI performance with Instruments</a>&#8221;  for detailed information and tips. If you&#8217;re still having issues, please post on the Developer Forums for more assistance.</p><div><hr></div><h3>When building a native Mac app using SwiftUI how do I debug the title bar looking different on my machine (built via Xcode or exported) vs a testers machine even when on the same macOS version (26.2)? Ex: The title bar being transparent as expected in some places or just greyed at the top instead.</h3><p>Hello, you can troubleshoot visual inconsistencies across different devices by making sure all of your Appearance settings are the same between them, e.g. Dark Mode, Liquid Glass, Accessibility display settings, etc. Also make sure that the content underneath the titlebar is consistent. If you are still experiencing issues despite this (or the appearance is not what you would expect), please file a bug report via Feedback Assistant: <a href="https://developer.apple.com/bug-reporting/">https://developer.apple.com/bug-reporting/</a></p><div><hr></div><h3>What is the best way to make an app that works for all the devices (iPhone, iPad, Mac, TV, Watch, Vision Pro)? Making a single &#8220;empty&#8221; project in Xcode then adding each platform target, or a separate Xcode project per platform?</h3><p>Create a new Multiplatform app. Then in the target list in Xcode, add a new target; Add a Watch app that is a companion to the existing app target. In the Watch app target properties, check or uncheck the box for &#8220;Supports Running without iOS App Installation&#8221; depending on your app&#8217;s requirements.</p><div><hr></div><h3>In macOS, SwiftUI provides some default menus. How can I specify keyboard equivalents for default menu items without having to replace the implementation, specifically for Edit-&gt;Delete? Even if I provide an implementation, it seems I&#8217;m forced to replace all of the pasteboard commands, not just Delete.</h3><p>You can find a full guide on configuring the Mac menu bar here: <a href="https://developer.apple.com/documentation/swiftui/building-and-customizing-the-menu-bar-with-swiftui">https://developer.apple.com/documentation/swiftui/building-and-customizing-the-menu-bar-with-swiftui</a></p><div><hr></div><h3>Is SwiftUI ideal for extensions?</h3><p>Hello, if you&#8217;re interested in building app extensions with SwiftUI, it&#8217;s a snap when paired with WidgetKit!<br> <a href="https://developer.apple.com/documentation/swiftui/app-extensions">https://developer.apple.com/documentation/swiftui/app-extensions</a></p><div><hr></div><h3>Can SwiftUI be used to develop a 2D game? If not what is the suitable apple framework to use? (SwiftUI or SpriteKit or ARKit or something else?)</h3><p>SpriteKit is a framework you could use to add characters to your game. For setting up the game and its logic, I would start with GameKit! See this documentation to get started: <a href="https://developer.apple.com/documentation/GameKit/creating-real-time-games">https://developer.apple.com/documentation/GameKit/creating-real-time-games</a></p><p>And ARKit is if you want to make an immersive 3D app using the device camera.</p><div><hr></div><h3>Will the custom shape overlay MKMultiPolygon from UIKit MapKit be included in SwiftUI MapKit? It&#8217;s a major polygon type overlay for map engineering and developers currently have access to only Polygon,Circle,Polyline etc&#8230; any graceful suggestions in the mean time?</h3><p>Depending on how you&#8217;re currently using <code>MKMultiPolygon</code>, you could use a <code>ZStack</code> with multiple Polygons in your Map, and apply your style modifiers to the <code>ZStack</code> to apply them to all of the Polygons in the ZStack.</p><div><hr></div><h2>&#127929; TextField &amp; Keyboard</h2><h3>Is there any way to present an editable <code>TextField</code> that never presents a keyboard or responds to a USB keyboard? I&#8217;m building a calculator app with its own keypad that appends to an expression. But I&#8217;d like to give the user the ability to select parts of the expression or place the insertion point.</h3><p>SwiftUI doesn&#8217;t provide a way to replace the system-provided keyboard with an input view. I&#8217;d suggest that you file a feedback report for that. As a rescue, you can use <code>UITextField</code> + <code>UIViewRepresentable</code> (assuming iOS / iPadOS), and give the <code>UITextField</code> instance an inputView.</p><div><hr></div><h3><code>Textfield</code> - inline formatting. Currently, <code>Textfield</code> format API doesn&#8217;t support inline formatting . What is recommended way to format inline forms on swift ui text fields?</h3><p>I would start by looking at the prompt of a <code>TextField</code>, see: <a href="https://developer.apple.com/documentation/swiftui/textfield/init(_:value:format:prompt">https://developer.apple.com/documentation/swiftui/textfield/init(_:value:format:prompt</a>:)</p><div><hr></div><h2>&#128737;&#65039; Security</h2><h3>Where is the best place to keep Access Token and Refresh token/s handling most secure session for Swift Apps?</h3><p>Use the Keychain to store tokens and other secrets. See the <a href="https://developer.apple.com/documentation/security/keychain-items">API reference for Keychain items</a> for more information.</p><div><hr></div><h2>&#127950;&#65039; Concurrency</h2><h3>Say the code is already on Main queue, I used a check Thread.isMainThread and then dispatch on main if false, if true, execute as it is. I want to achieve similar behaviour using modern concurrency. Is it possible? Task {@MainActor} will always behave like main.async</h3><p>You can await <code>MainActor.run</code> to execute a synchronous piece of code on the MainActor, which should avoid suspending execution if you are already on the MainActor. If you need to know specifically <em>what actor you are on</em>, you can use the <code>#isolation</code> function argument to introspect the current execution context (which returns a type of <code>isolated (any Actor)?</code>). When you are nonisolated, this will be nil. Conversely, when you are isolated--and on the MainActor--it will be <code>.some(MainActor)</code>.</p><div><hr></div><h2>&#128396;&#65039; Icons &amp; App Design</h2><h3>Can I build an app icon from scratch in Icon Composer app or I&#8217;d need to first design the icon in Figma/Sketch first than import to Icon Composer to add liquid glass design?</h3><p>Hi, have you checked out the guide <a href="https://developer.apple.com/documentation/Xcode/creating-your-app-icon-using-icon-composer">Creating your app icon using Icon Composer</a>? You can download an App Icon Template to create the layers in Figma and Sketch, before importing to Icon Composer. Template available here: <a href="https://developer.apple.com/design/resources/">https://developer.apple.com/design/resources/</a></p><div><hr></div><h3>I am having trouble getting the new app icon process to work. My icons in the assets catalog work fine, but icons that I create in IconMaker do not appear at runtime after I add them to my project, whether iOS or macOS.</h3><p>Hi, you can review the Creating your app icon using Icon Composer article. It&#8217;s a great resource to learn about <a href="https://developer.apple.com/documentation/Xcode/creating-your-app-icon-using-icon-composer">using Icon Composer</a> and adding your Icon Composer file to your Xcode project.</p><div><hr></div><h2>&#127916; Toolbar &amp; Actions</h2><h3>Is there a heuristic for the number of toolbar actions to show before moving them into a &#8220;more&#8221; menu in the toolbar?</h3><p>You can&#8217;t predict the number of toolbar actions that can be shown before they will be displayed in a &#8220;more&#8221; menu. This will be highly dependent on the size of the device, the size of the window or view, and the size of the toolbar items.</p><div><hr></div><h3>On iPad, DocumentGroup&#8217;s title/rename UI gets clipped when I fill <code>.topBarLeading</code>, <code>.principal</code>, and <code>.topBarTrailing</code> (trailing is an HStack). Any recommended toolbar structure to preserve the system rename affordance without dropping <code>.principal</code> or using a menu?</h3><p>I think this would be a great question for the Developer Forums. I encourage you to post this question there so we can more easily share code and others with the same issue can find help for the same issue. Visit Apple Developer Forums here: <a href="https://developer.apple.com/forums/">https://developer.apple.com/forums/</a></p><div><hr></div><h3>Apple encourages us to use the navigation bar, but some Apple apps are starting to use more controls in the bottom of the screen, i.e. Safari. Should we focus on using the navigation bar or try to add buttons at the bottom. Thank you!</h3><p>I would encourage you to find what it works for you, recommendations are one thing, but we do not know your requirements, if the navigation bar fulfills your requirement</p><p>Your custom buttons have their merits and can be effective depending on the context and design goals of your app.</p><p>The navigation bar is a traditional element in iOS apps and is widely recognized by users. Typically occupies less screen space at the top, providing more room for content than buttons.</p><p>Consider how users naturally interact with the device. I don&#8217;t think this what you wanted to hear but personal requirements are hard to fulfill in one quick answer. I would recommend the forums if you have examples of what you trying to accomplish.</p><div><hr></div><h2>&#128311; Miscellaneous</h2><h3>Is swift UI good at managing memory within messages or keyboard extensions? Lots of bandwidth issues causing crashes when loading a lot of data. Host app is fine&#8230;anything helps!</h3><p>Extensions typically have a hard memory limit. The system doesn&#8217;t allow extensions to consume more memory than the limit. Simply switching to SwiftUI doesn&#8217;t change the limit, and probably doesn&#8217;t help reduce the memory consumption either. Memory consumption is typically related to data management, and so focusing on loading and using data more effectively may better help. I&#8217;d suggest that you ask this question in our Developer Forums with more details of how your extensions consumes memory, and we can have a deeper discussion from there. Thanks.</p><div><hr></div><h3>How to best abstract the model for views? A custom view like <code>SearchRow(tripName:, photoName:)</code> is already its own view model. But SwiftUI components like Button don&#8217;t give us access to their model. The scene would be like <code>ForEach(searchRows)</code> and <code>ForEach(buttons)</code> or more generally <code>ForEach(rows)</code></h3><p>Trying to understand your question and hope I got exactly what you mean with the best abstract model for a view as is hard to answer, I would recommend you to go to the Apple forums as well to follow up. When designing the SwiftUI views with models, for lists with interactive components like buttons, clearly separate and manage state effectively, a clear data model representing the information you display and interact with, view model holding instances of your data model and necessary business logic or state. Define custom views presentation logic for each item with state in the view model or pass closures to handle actions as your main view to iteratee over your view model&#8217;s data, passing state and actions to child views. Ensure state changes in interactive elements reflect in the view model, which updates the view due to properties.</p><div><hr></div><h3>I want to add a half screen mostly transparent dialog over the main window of a UI photo editing app. Examples or suggestions?</h3><p>I&#8217;d probably consider using ZStack to lay out a view above the main window and to the target position, make it almost transparent, and use it as the dialog.</p><div><hr></div><h3>In the example of <code>let _ = print()</code>, will that print get called every time the view updates or just on initial creation?</h3><p>It depends on where you put the <code>print</code>. If you put it at the beginning of the body, every time the SwiftUI recalculates the view body, the print will be called.</p><div><hr></div><h3>I use SwiftData and the @Query annotation to display my data. Each Item is rendered in a subview. Is there a way to pass the Item to be modifiable using either an annotation or something else? Let&#8217;s say a pinned attribute is being flipped and updating my @Query without an explicit call to SwiftData</h3><p>Not sure if I understand your question correctly, but if you intent is to pass a SwiftData model to a view and change the model in the subview, you can definitely do so, and <code>@Query</code> will automatically detect the changes on the model. If you are seeking a way to change an attribute of the model without calling the setter of the attribute, no, there is no way to do that.</p><div><hr></div><h3>Why are views discarded? The dependency tracking- is this feature&#8217;s efficiency directly dependent on the discarded views? The Apple Developer Library-the extent of the resources pulled from there?(Customizable?)</h3><p>SwiftUI efficiently manages memory by discarding views that are no longer visible or needed. This process optimizes performance by keeping only visible or recalculated views in memory. SwiftUI uses a declarative syntax to define the UI state, and the framework automatically updates views when the state changes. Instead of manually updating each view, SwiftUI only updates the affected parts of the view hierarchy. Views are part of a hierarchy, and when their state changes, SwiftUI recalculates the entire affected hierarchy. If a view is no longer visible or necessary, it is removed from memory, reducing memory usage and improving rendering performance.</p><p>There are numerous examples and code snippets demonstrating how to implement various features in SwiftUI and related technologies.</p><p>Yes, the efficiency of SwiftUI&#8217;s dependency tracking and rendering can be dependent on how effectively views are discarded. SwiftUI keeps track of which views depend on which other views</p><p>I don&#8217;t know what you want exactly from the library.</p><div><hr></div><h3>Is there a way to only draw a portion of a view at a time? With AppKit I&#8217;d use <code>-[NSView drawRect:]</code> to only draw visible parts of the view. I have a View whose body is a large Path, and I&#8217;d like to reduce the amount of drawing I need to do at once by only drawing the section of the Path visible.</h3><p>SwiftUI is a different approach, and the view body is updated as a whole based on changes to its data. You might be able to split this into multiple views that each represent part of the Path which can each be individually updated when the underlying data changes. The Developer Forums are a great resource to get more assistance with your specific use case.</p><div><hr></div><h2><strong>&#127942; </strong>Acknowledgments<strong> </strong></h2><p>A huge thank-you to everyone who joined in and shared thoughtful, insightful, and engaging questions throughout the session &#8212; your curiosity and input made the discussion truly rich and collaborative.</p><p><strong>Special thanks to:</strong></p><p>Aap, Aaron Brooks, Abdalla Mohammed, Alexis Schotte, Andrew Hall, Arpad Larrinaga Aviles, Ash, Ativ Patel, Brian Leighty, Cyril Fonrose, Dan Fabulich, Dan Stoian, Dan Zeitman, David Ko, David Mehedinti, David Veeneman, Dipl.-Ing. Wilfried Bergmann, Eleanor Spenceley, Elif Arifoglu, Elilan, Etkin, Evelyn, Farhana Mustafa, Fred Baker, Greg C, Ian Shimabukuro, James Clarke, Jan Armbrust, Jason Howlin, Jay Graham, Jesse Wesson, Jobie, John Ellenich, John Gethoefer, John Lawter, Jon Bailey, Jon Judelson, Jonas Helmer, Joshua Arnold, Julien, Kaan, Kaylee, Lloyd W Sykes, Lucy, Luis Rebollo, Maic Lopez Saenz, Maisomage, Malcolm Hall, Marin Joe Garcia, Markus, Matthew Brown, Michael Bachand, Michael K., mustii, Muthuveerappan Alagappan, Nathan Odendahl, Nicolas Dunning, Oliver Dieke, Omar zu&#241;iga, Oshua Arnold, Pete Hoch, Rayan Khan, Ricardo Moreno Martinez, Rick Mann, Rupinder kaur, Sam Wu, Satish Muthali, Siddhartha Khanna, Simon McLoughlin, SRINIVASA Potnuru, Tom Brodhurst-Hill, Tula W, Ulien, Vijay Ram, Walid Mohamed Nadim, Will Loew-Blosser, Willian Zhang, Yafes Duygulutuna, Yassin El Mouden, Yuriy Chekan and Zulfi Shah.</p><p>Now that&#8217;s a list! </p><p>Finally, a heartfelt thank-you to the <strong>Apple team and moderators</strong> for leading session, sharing expert guidance, and offering such clear explanations of the apps optimization techniques. Your contributions made this session an outstanding learning experience for everyone involved.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Swift Bits: Git Worktree]]></title><description><![CDATA[Branching the modern way]]></description><link>https://antongubarenko.substack.com/p/swift-bits-git-worktree</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/swift-bits-git-worktree</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Tue, 10 Feb 2026 08:17:36 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/127ed1d7-d9c8-456b-ba0f-bf86a77e4b1b_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It&#8217;s funny how a tiny CLI command can shift your entire development workflow. For years I was stuck in the same pattern: git stash, git checkout, git stash pop, repeat and branching. Then I worked a lot with the ChatGPT macOS app and its connectors. Next, the Codex macOS app came along and, along with the Skills tab, there was a picker for the task run destination. Bottom bar is very informative:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!TUnZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6de3658-f27d-4bc3-b917-2ec603cd2418_554x358.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!TUnZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6de3658-f27d-4bc3-b917-2ec603cd2418_554x358.png 424w, https://substackcdn.com/image/fetch/$s_!TUnZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6de3658-f27d-4bc3-b917-2ec603cd2418_554x358.png 848w, https://substackcdn.com/image/fetch/$s_!TUnZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6de3658-f27d-4bc3-b917-2ec603cd2418_554x358.png 1272w, https://substackcdn.com/image/fetch/$s_!TUnZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6de3658-f27d-4bc3-b917-2ec603cd2418_554x358.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!TUnZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6de3658-f27d-4bc3-b917-2ec603cd2418_554x358.png" width="324" height="209.37184115523465" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b6de3658-f27d-4bc3-b917-2ec603cd2418_554x358.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:358,&quot;width&quot;:554,&quot;resizeWidth&quot;:324,&quot;bytes&quot;:37769,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/187137558?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6de3658-f27d-4bc3-b917-2ec603cd2418_554x358.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!TUnZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6de3658-f27d-4bc3-b917-2ec603cd2418_554x358.png 424w, https://substackcdn.com/image/fetch/$s_!TUnZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6de3658-f27d-4bc3-b917-2ec603cd2418_554x358.png 848w, https://substackcdn.com/image/fetch/$s_!TUnZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6de3658-f27d-4bc3-b917-2ec603cd2418_554x358.png 1272w, https://substackcdn.com/image/fetch/$s_!TUnZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6de3658-f27d-4bc3-b917-2ec603cd2418_554x358.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a><figcaption class="image-caption">Task destination in Codex macOS app</figcaption></figure></div><p>You can see a &#8220;New worktree&#8220; and that attracted my attention. But that was not the first time I had used it. I just didn&#8217;t know about it.</p><p>Claude app also use it for parallel session:</p><blockquote><h3>Work in parallel with sessions</h3><p>Click <strong>+ New session </strong>in the sidebar to work on multiple tasks in parallel. For Git repositories, each session gets its own isolated copy of your project using worktrees, so changes in one session don&#8217;t affect another until you commit them. Worktrees are stored in<code>~/.claude-worktrees/</code>by default.<br><a href="https://code.claude.com/docs/en/desktop">Docs</a></p></blockquote><p>And that&#8217;s why worktrees are hitting the spotlight lately because AI-powered tools (like <strong><a href="https://docs.conductor.build/?utm_source=chatgpt.com">Conductor</a>) </strong> and AI coding assistants: increasingly rely on running parallel workflows. And worktrees are the unsung hero that make those workflows fast and sane.</p><div><hr></div><h2>What Is Git Worktree, Really?</h2><p>Imagine this: you could check out <strong>multiple branches of the same repo at the same time</strong> without cloning, without stashing, and without jumping between contexts.</p><p>That&#8217;s exactly what <strong><a href="https://git-scm.com/docs/git-worktree">Git Worktrees</a></strong> let you do. Technically, worktrees create <strong>separate working directories</strong> that are all linked back to the same .git repository. You can have one directory on main, another on feature/foo, and another on hotfix/bar, all open at once. Each has its own working set of files, but they share the same Git history and object database &#8212; so commits, tags, and branches stay in sync without duplication.</p><p>It&#8217;s not magic! It&#8217;s Git&#8217;s native way of handling multiple working copies without wasting space. If you&#8217;ve ever cloned the same repo three times just to manage multitasking, consider this your apology from the universe.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!mZ5S!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fe9a0a4-03ae-4b0a-bce2-35f069e69bb3_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mZ5S!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fe9a0a4-03ae-4b0a-bce2-35f069e69bb3_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!mZ5S!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fe9a0a4-03ae-4b0a-bce2-35f069e69bb3_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!mZ5S!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fe9a0a4-03ae-4b0a-bce2-35f069e69bb3_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!mZ5S!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fe9a0a4-03ae-4b0a-bce2-35f069e69bb3_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!mZ5S!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fe9a0a4-03ae-4b0a-bce2-35f069e69bb3_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6fe9a0a4-03ae-4b0a-bce2-35f069e69bb3_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:383659,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/187137558?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fe9a0a4-03ae-4b0a-bce2-35f069e69bb3_1536x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!mZ5S!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fe9a0a4-03ae-4b0a-bce2-35f069e69bb3_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!mZ5S!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fe9a0a4-03ae-4b0a-bce2-35f069e69bb3_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!mZ5S!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fe9a0a4-03ae-4b0a-bce2-35f069e69bb3_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!mZ5S!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6fe9a0a4-03ae-4b0a-bce2-35f069e69bb3_1536x1024.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Flow diagram</figcaption></figure></div><p>This approach also solve the old merging problem when you need to commit fast to the feature branch to jump back to <code>main/master</code>. Just make a worktree from the problematic branch and keep working.</p><p>Also, you might now that we can&#8217;t have multiple branches for a repo. Well, seems that we can do that. Not sure why you might need it? Next section will tell you more.</p><div><hr></div><h2>How You Can Use It (Beyond &#8220;Just Another Git Trick&#8221;)</h2><p>At first glance, worktrees are an efficiency hack for the multitasker. But their real superpower emerges in two increasingly common workflows:</p><h3>&#128736; Parallel Feature Development</h3><p>Instead of constantly switching context, you can open separate features in separate worktrees. Want to build a UI component in one and debug an urgent bug in another? Done.</p><blockquote><p>Our multiple branches issues from above</p></blockquote><h3>&#129302; AI-Powered Development</h3><p>With AI tools that spin up separate agents to run against your codebase (like Conductor does with <strong>parallel Claude Codes</strong>, each in its own sandboxed copy), worktrees make it possible to do this locally without cloning or spinning up separate VMs. Essentially, each agent gets a dedicated branch + working copy &#8212; without you having to manage massive clones.</p><p>This paradigm is quickly becoming <strong>the backbone of AI-augmented workflows</strong>: by avoiding stash chaos and enabling isolated contexts, worktrees let you let the machines do their thing without disrupting your flow.</p><div><hr></div><h2>A Simple Worktree Example: Create &amp; Remove</h2><p>Here&#8217;s the minimal set of commands you actually need. Think of these as your &#8220;git worktree starter pack&#8221;.</p><h3>Create a Worktree for a Feature</h3><pre><code># Create a new worktree for a feature branch
git worktree add ../feature-awesome -b feature/awesome</code></pre><p>What this does:</p><ul><li><p>Makes a new folder ../feature-awesome</p></li><li><p>Creates and checks out feature/awesome there</p></li><li><p>Links it back to your central .git repository</p><p>No stashing, no switching &#8212; everything stays in your current directory.</p></li></ul><h3>When You&#8217;re Done: Remove It</h3><pre><code>git worktree remove ../feature-awesome</code></pre><p>Boom &#8212; worktree gone. Git cleans up references, and the branch stays in your repo just like any other.</p><blockquote><p>&#128161;If you are keen to working with IDEs: Codex app have an task location picker at the bottom bar and Claude have a &#8220;New Session&#8220; which will all too use Worktree as we know now.</p></blockquote><div><hr></div><h2>Final Thoughts</h2><p>Git Worktree feels like one of those features that <em>s</em>hould have existed all along. It solves a real pain point: <strong>context switching chaos</strong>. And now that AI tools and agent managers are building their workflows on top of it, it&#8217;s fast becoming a tooling standard.</p><p>If you&#8217;re still in the mindset of git stash &#8594; git checkout &#8594; git stash pop, give Git Worktree a try. Your future self (and your future AI tools) will thank you.</p><p>Happy branching &#128640;!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h2>References</h2><ul><li><p><a href="https://git-scm.com/docs/git-worktree">Git worktree</a></p></li><li><p><a href="https://code.claude.com/docs/en/desktop">Claude Session Info</a></p></li><li><p><a href="https://developers.openai.com/codex/app/worktrees/">Codex Worktree Info</a></p></li></ul><p></p>]]></content:encoded></item><item><title><![CDATA[Swift Bits: Transition vs Transaction]]></title><description><![CDATA[Animate appearance and more!]]></description><link>https://antongubarenko.substack.com/p/swift-bits-transition-vs-transaction</link><guid isPermaLink="false">https://antongubarenko.substack.com/p/swift-bits-transition-vs-transaction</guid><dc:creator><![CDATA[Anton Gubarenko]]></dc:creator><pubDate>Mon, 02 Feb 2026 07:01:32 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/0b4c8ebd-00e8-4219-ba26-fe61ef6af269_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>SwiftUI&#8217;s animation system is powerful but often misunderstood. Even Xcode, when you start typing a keyword, shows a load of almost identical options. What about when, in a tech interview, you are asked about the difference?</p><p>Two concepts that frequently come up when building polished, animated interfaces are <strong>Transitions</strong> and <strong>Transactions</strong>. They sound similar (and they&#8217;re both related to motion and state changes) but they serve very different purposes.</p><div><hr></div><h2>Transition</h2><p>A <strong><a href="https://developer.apple.com/documentation/swiftui/view/transition(_:)-5h5h0">Transition</a></strong> in SwiftUI defines <strong>how a view appears and disappears</strong> when it is inserted into or removed from the view hierarchy. It&#8217;s about <em>enter and exit animation</em> &#8212; the visual effect used when a view changes state and is added or removed.</p><p>Transitions don&#8217;t control timing or easing. They describe the <em>type of animation</em> that happens. The actual animation timing comes from a surrounding animation context.</p><h3>Built-in Transitions</h3><p>SwiftUI provides several built-in transitions:</p><pre><code>Text("Hello SwiftUI!")
    .transition(.opacity)</code></pre><p>You can also combine transitions:</p><pre><code>.transition(.scale.combined(with: .opacity))</code></pre><p>Or define asymmetric transitions, where insertion and removal behave differently:</p><pre><code>.transition(.asymmetric(
    insertion: .slide,
    removal: .scale
))</code></pre><p>Or create a custom Transition relying on <a href="https://developer.apple.com/documentation/swiftui/anytransition">AnyTransition</a> class. It&#8217;s pretty straightforward. How about blur and scale transition?<br>Need to make a transition modifier (yeah):</p><pre><code>struct BlurAndScaleTransition: ViewModifier {
    let progress: CGFloat

    func body(content: Content) -&gt; some View {
        content
            .scaleEffect(progress)
            .blur(radius: (1 - progress) * 10)
            .opacity(progress)
    }
}</code></pre><p>Then wrap in AnyTransition:</p><pre><code>extension AnyTransition {
    static var blurAndScale: AnyTransition {
        .modifier(
            active: BlurAndScaleTransition(progress: 0),
            identity: BlurAndScaleTransition(progress: 1)
        )
    }
}

// and use it! A plain example is shown in the next section
.transition(.blurAndScale)</code></pre><h3>How SwiftUI Applies Transitions</h3><p>Transitions take effect only when views are inserted into or removed from the view hierarchy. This usually happens inside conditional statements such as if blocks or when modifying collections in <code>List</code> or <code>ForEach</code>.</p><p>SwiftUI compares the previous and current view tree, detects insertions and removals, and applies the transition accordingly.</p><p>Importantly, <strong>a transition alone does not animate</strong>. It must be wrapped in an animation context using <code>withAnimation</code> or <code>.animation(_:)</code>.</p><h3>Example: Using a Transition</h3><pre><code>struct MyView: View {
    @State private var show = false

    var body: some View {
        VStack {
            if show {
                RoundedRectangle(cornerRadius: 20)
                    .fill(.blue)
                    .frame(height: 100)
                    .transition(.move(edge: .top))
            }

            Button("Toggle") {
                withAnimation(.easeInOut) {
                    show.toggle()
                }
            }
        }
    }
}</code></pre><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;86a56da0-e9a6-459c-b2e3-8139c70b93ca&quot;,&quot;duration&quot;:null}"></div><p>Here:</p><ul><li><p>The rectangle is inserted and removed from the hierarchy</p></li><li><p>The transition defines the movement</p></li><li><p>The animation defines timing and easing</p></li></ul><div><hr></div><h2>Transaction</h2><p>A <strong><a href="https://developer.apple.com/documentation/swiftui/transaction">Transaction</a></strong> in SwiftUI represents the <strong>context of a state change</strong>. It carries information about how SwiftUI should process that change, including animation behavior.</p><p>Every time SwiftUI processes a state update, it creates a Transaction and propagates it through the view hierarchy.</p><p>A transaction can include:</p><ul><li><p>The animation associated with the update</p></li><li><p>Whether animations are disabled</p></li><li><p>Metadata used internally by SwiftUI during rendering</p></li></ul><h3>Why Transactions Matter</h3><p>Transitions define <em>what</em> happens visually. Transactions define <em>how</em> that visual change is executed.</p><p>If you use <code>withAnimation</code>, SwiftUI attaches the animation to the transaction. That transaction then flows down to child views unless explicitly modified.</p><p>This mechanism explains why animations sometimes affect views you didn&#8217;t expect &#8212; they are all responding to the same transaction.</p><p>Let&#8217;s track what the transaction does in our example:</p><pre><code>struct SwiftBitsTransactionView: View {
    
    @State private var show = false
    
    var body: some View {
        VStack {
            if show {
                RoundedRectangle(cornerRadius: 20)
                    .fill(.blue)
                    .frame(height: 200)
                    .transition(.move(edge: .top))
                    //Tracking transaction for animation
                    .transaction { thx in
                        print(thx as Any)
                    }
            }
            
            Button("Toggle Rect") {
                withAnimation(.easeInOut) {
                    show.toggle()
                }
            }
        }
    }
}</code></pre><p>The console will reveal this:</p><pre><code>Transaction(plist: [TransactionPropertyKey&lt;AnimationKey&gt; = Optional(AnyAnimator(SwiftUI.BezierAnimation(duration: 0.35, curve: (extension in SwiftUI):SwiftUI.UnitCurve.CubicSolver(ax: 0.52, bx: -0.78, cx: 1.26, ay: -2.0, by: 3.0, cy: 0.0))))])</code></pre><p>And if we change it to Linear:</p><pre><code>Transaction(plist: [TransactionPropertyKey&lt;AnimationKey&gt; = Optional(AnyAnimator(SwiftUI.BezierAnimation(duration: 0.35, curve: (extension in SwiftUI):SwiftUI.UnitCurve.CubicSolver(ax: -2.0, bx: 3.0, cx: 0.0, ay: -2.0, by: 3.0, cy: 0.0))))])</code></pre><p>It really matches the result on screen. Nice!</p><h3>Modifying Transactions</h3><p>SwiftUI provides the <code>.transaction(_:) </code>modifier:</p><pre><code>Text("No animation")
    .transaction { thx in
        thx.animation = nil
    }</code></pre><p>this removes animation for that view and all of its children, even if the state change was wrapped in <code>withAnimation</code>.</p><p>Or, it can be used to override the original animation behavior:</p><pre><code>struct SwiftBitsTransactionView: View {
    
    @State private var show = false
    
    var body: some View {
        VStack {
            if show {
                RoundedRectangle(cornerRadius: 20)
                    .fill(.blue)
                    .frame(height: 200)
                    .transition(.move(edge: .top))
                    .transaction { thx in
                        thx.animation = thx
                            .animation?
                            .delay(2.0)
                            .speed(2)
                    }
            }
            
            Button("Toggle Rect") {
                withAnimation(.linear) {
                    show.toggle()
                }
            }
        }
    }
}</code></pre><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;54f8a87f-f2ad-4060-8636-9be4b3de5162&quot;,&quot;duration&quot;:null}"></div><h3>Important Transaction Properties</h3><ul><li><p><code>animation</code>: The animation applied to this update, if any</p></li><li><p><code>disablesAnimations</code>: A Boolean that disables animations for this subtree</p></li><li><p><code>addAnimationCompletion</code>: Completion closure to run when the animations created with this transaction are all complete</p></li></ul><p>Transactions allow very fine-grained control over animation behavior and are especially useful in complex view hierarchies.</p><div><hr></div><h2>Transition vs Transaction: Key Differences</h2><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!j0A9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb59b40a-9888-4a93-8dd4-13e0b9552291_1774x234.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!j0A9!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb59b40a-9888-4a93-8dd4-13e0b9552291_1774x234.png 424w, https://substackcdn.com/image/fetch/$s_!j0A9!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb59b40a-9888-4a93-8dd4-13e0b9552291_1774x234.png 848w, https://substackcdn.com/image/fetch/$s_!j0A9!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb59b40a-9888-4a93-8dd4-13e0b9552291_1774x234.png 1272w, https://substackcdn.com/image/fetch/$s_!j0A9!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb59b40a-9888-4a93-8dd4-13e0b9552291_1774x234.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!j0A9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb59b40a-9888-4a93-8dd4-13e0b9552291_1774x234.png" width="1456" height="192" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/db59b40a-9888-4a93-8dd4-13e0b9552291_1774x234.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:192,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:42118,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://antongubarenko.substack.com/i/186294444?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb59b40a-9888-4a93-8dd4-13e0b9552291_1774x234.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!j0A9!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb59b40a-9888-4a93-8dd4-13e0b9552291_1774x234.png 424w, https://substackcdn.com/image/fetch/$s_!j0A9!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb59b40a-9888-4a93-8dd4-13e0b9552291_1774x234.png 848w, https://substackcdn.com/image/fetch/$s_!j0A9!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb59b40a-9888-4a93-8dd4-13e0b9552291_1774x234.png 1272w, https://substackcdn.com/image/fetch/$s_!j0A9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdb59b40a-9888-4a93-8dd4-13e0b9552291_1774x234.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><h3>Conceptual Difference</h3><ul><li><p><strong>Transitions</strong> answer: &#8220;How should this view appear or disappear?&#8221;</p></li><li><p><strong>Transactions</strong> answer: &#8220;How should this state change be animated?&#8221;</p></li></ul><p>Transitions are declarative and visual. Transactions are contextual and behavioral.</p><div><hr></div><h2>When to Use Which</h2><h3>Use Transitions When:</h3><ul><li><p>A view is conditionally shown or hidden</p></li><li><p>A view is inserted or removed from a collection</p></li><li><p>You want a clear visual effect for appearance or disappearance</p></li></ul><p>Examples include modals, banners, expanding panels, or list rows.</p><h3>Use Transactions When:</h3><ul><li><p>You need to override or suppress animations</p></li><li><p>You want different animation behavior in specific subtrees</p></li><li><p>You need programmatic control over animation propagation</p></li><li><p>Default animation behavior is too broad or unpredictable</p></li></ul><p>Transactions are more advanced and typically used when standard <code>.animation</code> and <code>.transition</code> modifiers are not enough.</p><p>Happy coding!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://antongubarenko.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Anton&#8217;s Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item></channel></rss>