<?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[Emre Degirmenci]]></title><description><![CDATA[iOS Engineer with 8+ years of experience. Writing about building apps with Swift, SwiftUI, and UIKit for Apple platforms, mainly iOS.]]></description><link>https://emredegirmenci.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!ozjy!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F40cb062b-0d35-4584-a19e-6dc2d73314ef_3546x3546.jpeg</url><title>Emre Degirmenci</title><link>https://emredegirmenci.substack.com</link></image><generator>Substack</generator><lastBuildDate>Mon, 17 Aug 2026 00:11:34 GMT</lastBuildDate><atom:link href="https://emredegirmenci.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Emre Degirmenci]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[emredegirmenci@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[emredegirmenci@substack.com]]></itunes:email><itunes:name><![CDATA[Emre Degirmenci]]></itunes:name></itunes:owner><itunes:author><![CDATA[Emre Degirmenci]]></itunes:author><googleplay:owner><![CDATA[emredegirmenci@substack.com]]></googleplay:owner><googleplay:email><![CDATA[emredegirmenci@substack.com]]></googleplay:email><googleplay:author><![CDATA[Emre Degirmenci]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Why does iCloud sync get stuck sometimes?]]></title><description><![CDATA[A silent sync problem that can haunt anyone with two Apple devices]]></description><link>https://emredegirmenci.substack.com/p/why-does-icloud-sync-get-stuck-sometimes</link><guid isPermaLink="false">https://emredegirmenci.substack.com/p/why-does-icloud-sync-get-stuck-sometimes</guid><dc:creator><![CDATA[Emre Degirmenci]]></dc:creator><pubDate>Mon, 03 Aug 2026 08:47:16 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!_qBH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31d7ef06-0047-4a38-8a5f-bd44b82fa909_536x398.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Have you ever wondered why does iCloud data sync stuck and sometimes not move forward while you are transferring your photos between MacOS Photos app to the Desktop or other Finder files by dragging? I had this too and I&#8217;ve been having almost everyday. Also, since I have no backend development experience and a hater of a vibe-coding by completely relying on LLMs and don&#8217;t want to spend money for 3rd party server providers, I use iCloud and CloudKit public/private database as a backend which is fully native Apple framework and data syncing tool in my apps. When I re-install <a href="https://apple.co/4mz7vev">Walk Mate</a> on different simulators or physical iOS devices for testing purposes or in real life production, I see the following screen while data sync in progress and it sometimes get stuck and takes too long. Then, boom, where did my stuff go???</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!OHw7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33c8d319-2515-45b2-99c6-4c956400a049_534x224.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!OHw7!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33c8d319-2515-45b2-99c6-4c956400a049_534x224.png 424w, https://substackcdn.com/image/fetch/$s_!OHw7!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33c8d319-2515-45b2-99c6-4c956400a049_534x224.png 848w, https://substackcdn.com/image/fetch/$s_!OHw7!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33c8d319-2515-45b2-99c6-4c956400a049_534x224.png 1272w, https://substackcdn.com/image/fetch/$s_!OHw7!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33c8d319-2515-45b2-99c6-4c956400a049_534x224.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!OHw7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33c8d319-2515-45b2-99c6-4c956400a049_534x224.png" width="534" height="224" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/33c8d319-2515-45b2-99c6-4c956400a049_534x224.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:224,&quot;width&quot;:534,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:23436,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:&quot;&quot;,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://emredegirmenci.substack.com/i/208892130?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33c8d319-2515-45b2-99c6-4c956400a049_534x224.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_!OHw7!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33c8d319-2515-45b2-99c6-4c956400a049_534x224.png 424w, https://substackcdn.com/image/fetch/$s_!OHw7!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33c8d319-2515-45b2-99c6-4c956400a049_534x224.png 848w, https://substackcdn.com/image/fetch/$s_!OHw7!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33c8d319-2515-45b2-99c6-4c956400a049_534x224.png 1272w, https://substackcdn.com/image/fetch/$s_!OHw7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F33c8d319-2515-45b2-99c6-4c956400a049_534x224.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><p>In order to force that iCloud data syncing I even provided a forcing option to users.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_qBH!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31d7ef06-0047-4a38-8a5f-bd44b82fa909_536x398.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_qBH!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31d7ef06-0047-4a38-8a5f-bd44b82fa909_536x398.png 424w, https://substackcdn.com/image/fetch/$s_!_qBH!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31d7ef06-0047-4a38-8a5f-bd44b82fa909_536x398.png 848w, https://substackcdn.com/image/fetch/$s_!_qBH!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31d7ef06-0047-4a38-8a5f-bd44b82fa909_536x398.png 1272w, https://substackcdn.com/image/fetch/$s_!_qBH!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31d7ef06-0047-4a38-8a5f-bd44b82fa909_536x398.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_qBH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31d7ef06-0047-4a38-8a5f-bd44b82fa909_536x398.png" width="536" height="398" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/31d7ef06-0047-4a38-8a5f-bd44b82fa909_536x398.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:398,&quot;width&quot;:536,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:44286,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:&quot;&quot;,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://emredegirmenci.substack.com/i/208892130?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31d7ef06-0047-4a38-8a5f-bd44b82fa909_536x398.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_!_qBH!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31d7ef06-0047-4a38-8a5f-bd44b82fa909_536x398.png 424w, https://substackcdn.com/image/fetch/$s_!_qBH!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31d7ef06-0047-4a38-8a5f-bd44b82fa909_536x398.png 848w, https://substackcdn.com/image/fetch/$s_!_qBH!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31d7ef06-0047-4a38-8a5f-bd44b82fa909_536x398.png 1272w, https://substackcdn.com/image/fetch/$s_!_qBH!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F31d7ef06-0047-4a38-8a5f-bd44b82fa909_536x398.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 aria-hidden="true" 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><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>So the UI keeps smiling. You keep waiting. Nothing arrives. You start questioning your life choices &#129300;<br><br><a href="https://apple.co/4mz7vev">Walk Mate</a> is doing the normal responsible things: save locally first, mirror into iCloud-ish places, merge when the other device finally sees the data. For avoided segments I even have that &#8220;syncing from iCloud&#8230;&#8221; kind of waiting state because ubiquity files really can be &#8220;not downloaded yet&#8221;. But there is a ceiling to what an app can explain. So of course people blame the app. I would too. If your favorite route is missing, <a href="https://apple.co/4mz7vev">Walk Mate</a> is the face of the crime &#128373;&#127995;&#8205;&#9794;&#65039;<br><br>Then I heard a fix from a super talented software developer who basically said: once he understood Photos/iCloud were getting stuck because the UDP/QUIC path was unhealthy and Apple wasn&#8217;t falling back in a useful way, <strong>he basically just shut UDP down on his network and forced everything onto TCP.</strong> <br><br>Under the hood a lot of this content traffic goes through Apple&#8217;s system daemons; <em><strong>cloudd, nsurlsessiond. </strong></em>And modern Apple networking loves <strong>HTTP/3 over QUIC, </strong>which basically means UDP 443 instead of the old reliable TCP lane. QUIC is supposed to be the cool fast future. Until it isn&#8217;t.</p><p>There&#8217;s a whole Apple forums thread about this exact &#8220;silent upload deadlock&#8221; energy with stale HTTP/3 sessions. <br><br>If you want the deep technical rabbit hole:<em> <strong><a href="https://developer.apple.com/forums/thread/822534">Apple Developer forums thread</a>.</strong></em></p>]]></content:encoded></item><item><title><![CDATA[Is the mainQueue serial or concurrent? ]]></title><description><![CDATA[I&#8217;m here with another popular iOS interview question that I get asked frequently.]]></description><link>https://emredegirmenci.substack.com/p/is-the-mainqueue-serial-or-concurrent</link><guid isPermaLink="false">https://emredegirmenci.substack.com/p/is-the-mainqueue-serial-or-concurrent</guid><dc:creator><![CDATA[Emre Degirmenci]]></dc:creator><pubDate>Mon, 27 Jul 2026 14:20:07 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!mSue!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8a86ac3-4300-4395-9f5f-9d62da280d21_1608x358.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I&#8217;m here with another popular iOS interview question that I get asked frequently.<br><br>&#128104;&#127995;&#8205;&#127979; <em><strong>&#8221;Is the mainQueue serial or concurrent?&#8221;</strong></em></p><blockquote><p>Short answer: <strong>the main queue is serial. &#128512;</strong></p></blockquote><p>If you think about the mainQueue, it is designed for the UI. If you want to make changes on the UI, their order you dispatched asynchronous action in the mainQueue still matters. For instance, when you run an animation or update labels, images, or navigation, you don&#8217;t want those updates racing each other. So the main queue works like one-after-another: each task must complete before the next task is able to start. That&#8217;s exactly what a serial queue gives you. On the other hand the <strong>GlobalQueue</strong>s are in general concurrent with tasks with respect to each other&#8217;s completion. Because you can dispatch offline things of main thread and do something maybe super intensive (networking, decoding, file I/O etc.) and then when it finished you can update the UI. Those are usually concurrent on parallel depending on the needs. </p><p>A quick mental model that helps in interviews:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!mSue!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8a86ac3-4300-4395-9f5f-9d62da280d21_1608x358.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mSue!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8a86ac3-4300-4395-9f5f-9d62da280d21_1608x358.png 424w, https://substackcdn.com/image/fetch/$s_!mSue!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8a86ac3-4300-4395-9f5f-9d62da280d21_1608x358.png 848w, https://substackcdn.com/image/fetch/$s_!mSue!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8a86ac3-4300-4395-9f5f-9d62da280d21_1608x358.png 1272w, https://substackcdn.com/image/fetch/$s_!mSue!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8a86ac3-4300-4395-9f5f-9d62da280d21_1608x358.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!mSue!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8a86ac3-4300-4395-9f5f-9d62da280d21_1608x358.png" width="1456" height="324" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c8a86ac3-4300-4395-9f5f-9d62da280d21_1608x358.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:324,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:59147,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:&quot;&quot;,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://emredegirmenci.substack.com/i/197694914?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc715d75b-577d-4be8-aca1-2383c05bc484_1608x358.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_!mSue!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8a86ac3-4300-4395-9f5f-9d62da280d21_1608x358.png 424w, https://substackcdn.com/image/fetch/$s_!mSue!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8a86ac3-4300-4395-9f5f-9d62da280d21_1608x358.png 848w, https://substackcdn.com/image/fetch/$s_!mSue!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8a86ac3-4300-4395-9f5f-9d62da280d21_1608x358.png 1272w, https://substackcdn.com/image/fetch/$s_!mSue!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc8a86ac3-4300-4395-9f5f-9d62da280d21_1608x358.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><p>Also don&#8217;t mix up <strong>serial/concurrent</strong> with <strong>sync/async</strong>. Those are different axes:</p><ul><li><p><strong>Serial vs concurrent</strong> &#8594; how the <em>queue</em> runs tasks (one-by-one vs overlapping)</p></li><li><p><strong>sync vs async</strong> &#8594; whether the <em>caller waits</em></p><ul><li><p><code>sync</code> &#8594; wait until that work finishes</p></li><li><p><code>async</code> &#8594; submit the work and continue</p></li></ul></li></ul><p><span>So </span><em><strong>DispatchQueue.main.async { ... }</strong></em><span> still runs on the </span><strong><span>serial main queue</span></strong><span>. Async only means </span><strong><span>&#8220;don&#8217;t block the caller while waiting to schedule it.&#8221;</span></strong><span> It does </span><strong><span>not</span></strong><span> mean &#8220;run this in the background.&#8221;</span></p><p>That distinction also explains a classic crash/freeze: calling <em><strong>DispatchQueue.main.sync</strong></em> while you are already on the main thread. The main queue is waiting for the sync block to finish but the sync block <strong>can&#8217;t start until the main queue is free</strong>, it is <strong>deadlock</strong>. Prefer <em><strong>main.async</strong></em> for UI updates and if you must use <em><strong>sync</strong></em>, never sync a queue against itself.</p><h3>Where does this land with modern Swift? </h3><p>Actors protect mutable state by serializing access, one caller at a time. An actor gives us its own isolation domain and that work usually runs off the main thread unless the actor is main-actor isolated. So conceptually it&#8217;s similar energy to a serial queue: controlled, ordered access. But the main actor / main queue remains special because UIKit/SwiftUI UI updates still need that serial main-thread world.</p><div><hr></div><p><strong><span>Interview takeaway:</span></strong> Main queue = serial, UI-bound, order matters. Global queues = concurrent, good for heavy work. Sync/async is about waiting, not about which queue you are on.</p>]]></content:encoded></item><item><title><![CDATA[Are conditional view modifiers a good idea in SwiftUI?]]></title><description><![CDATA[I&#8217;ve been interviewing with companies for the past 8 months (still unemployed in the current tough market!]]></description><link>https://emredegirmenci.substack.com/p/are-conditional-view-modifiers-a</link><guid isPermaLink="false">https://emredegirmenci.substack.com/p/are-conditional-view-modifiers-a</guid><dc:creator><![CDATA[Emre Degirmenci]]></dc:creator><pubDate>Mon, 20 Jul 2026 08:53:30 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!gAHy!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9aa372d7-70c7-46f9-a30d-94c656543bfe_1780x728.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I&#8217;ve been interviewing with companies for the past 8 months (<strong>still unemployed in the current tough market! You can <a href="https://www.linkedin.com/in/aemrdgrmnci">hire me</a>!</strong>) now and nowadays I&#8217;m writing articles about my experiences around interview questions and situations that I faced while building my personal fully native iOS apps with Swift, SwiftUI and other modern approaches. And today&#8217;s one is one of the questions that I got recently <em><strong>&#8220;Are conditional view modifiers a good idea in SwiftUl?</strong></em></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!gAHy!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9aa372d7-70c7-46f9-a30d-94c656543bfe_1780x728.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!gAHy!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9aa372d7-70c7-46f9-a30d-94c656543bfe_1780x728.png 424w, https://substackcdn.com/image/fetch/$s_!gAHy!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9aa372d7-70c7-46f9-a30d-94c656543bfe_1780x728.png 848w, https://substackcdn.com/image/fetch/$s_!gAHy!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9aa372d7-70c7-46f9-a30d-94c656543bfe_1780x728.png 1272w, https://substackcdn.com/image/fetch/$s_!gAHy!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9aa372d7-70c7-46f9-a30d-94c656543bfe_1780x728.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!gAHy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9aa372d7-70c7-46f9-a30d-94c656543bfe_1780x728.png" width="1456" height="595" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9aa372d7-70c7-46f9-a30d-94c656543bfe_1780x728.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:595,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:85909,&quot;alt&quot;:&quot;The code that you never ever use!&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://emredegirmenci.substack.com/i/204654296?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9aa372d7-70c7-46f9-a30d-94c656543bfe_1780x728.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="The code that you never ever use!" title="The code that you never ever use!" srcset="https://substackcdn.com/image/fetch/$s_!gAHy!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9aa372d7-70c7-46f9-a30d-94c656543bfe_1780x728.png 424w, https://substackcdn.com/image/fetch/$s_!gAHy!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9aa372d7-70c7-46f9-a30d-94c656543bfe_1780x728.png 848w, https://substackcdn.com/image/fetch/$s_!gAHy!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9aa372d7-70c7-46f9-a30d-94c656543bfe_1780x728.png 1272w, https://substackcdn.com/image/fetch/$s_!gAHy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9aa372d7-70c7-46f9-a30d-94c656543bfe_1780x728.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 aria-hidden="true" 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><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">This screenshot was taken from objc.io</figcaption></figure></div><p>It is a short question with a long answer. Most people say it depends&#8230; and move on. Chris Eidhof wrote the classic take on this years ago: <em><a href="https://www.objc.io/blog/2021/08/24/conditional-view-modifiers/">Why Conditional View Modifiers are a Bad Idea (objc.io)</a></em>. Apple&#8217;s <a href="https://www.avanderlee.com/ai-development/using-xcode-27s-agent-skills-in-claude-codex-and-cursor/">Xcode 27 swiftui-expert-skill</a> now says the same thing under <em><strong>Prefer Modifiers Over Conditional Views</strong></em>. I have been applying both while reviewing <a href="https://apple.co/4mz7vev">Walk Mate&#8217;s</a> map screen and this post is the answer I would give in that interview. </p><h3>The Core Idea</h3><p>In SwiftUI, views are value types (struct). They do not have object identity like UIKit. SwiftUI figures out <em><strong>&#8220;sameness&#8221;</strong></em> from structure and type. When you animate, it needs to compare the view before/after and interpolate between those values.</p><blockquote><p><em>As it explained very well in objc.io&#8217;s blog post, an if/else branch produces _ConditionalContent. That is an enum: either the true branch or the false branch. When the condition flips, SwiftUI does not interpolate. It removes one view and inserts another. Default transition: fade. And any @State / @StateObject sitting on that position gets reset, because as far as SwiftUI is concerned a new view just appeared.</em></p></blockquote><h4>The Interview Trap: .if and applyIf</h4><p>A lot of codebases still have something like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;446406e1-2911-46aa-ae30-8725c497709e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">// Please don&#8217;t use this:
extension View {
    @ViewBuilder
    func `if`&lt;T: View&gt;(_ condition: Bool, transform: (Self) -&gt; T) -&gt; some View {
        if condition {
            transform(self)
        } else {
            self
        }
    }
}</code></pre></div><p>Or the <em><strong>applyIf</strong></em> variant from countless blog posts. It feels clever. It is also the exact pattern Apple calls out as problematic!</p><h4>Why? </h4><p>The return type changes per branch. One branch is <em><strong>T (the transformed view)</strong></em>. The other is <em><strong>Self</strong></em>. Outermost type becomes <em><strong>_ConditionalContent</strong></em>. Identity breaks. Animations break. State can reset.</p><p>objc.io&#8217;s example makes it obvious. Conditionally framing a Rectangle with applyIf fades. Writing .frame(width: myState ? 100 : nil) keeps one ModifiedContent type and animates smoothly.</p><h3>Apple&#8217;s guidance matches that:</h3><ul><li><p>Prefer always-present modifiers with ternary values:</p></li></ul><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;8ac51bc0-8ef2-46bd-8567-5ab0be6256d8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">Text(&#8221;Hello&#8221;)
    .opacity(isHighlighted ? 1 : 0.5)

Text(&#8221;Hello&#8221;)
    .foregroundStyle(isError ? .red : .primary)</code></pre></div><p>When writing new code, never reach for a <em><strong>.if</strong></em> modifier. When reviewing existing code that already uses one, point out the identity and animation risk and show the ternary alternative but don&#8217;t silently refactor it inside an unrelated PR. Swapping it can change behavior (state resets, transition timing). That belongs in its own focused edit.</p><ul><li><p>Prefer Modifiers Over Conditional Views</p></li></ul><p>When you introduce a branch, are you representing multiple views or two states of the same view?</p><p>Same view, different states &#8594; prefer a no-effect modifier:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;a34c11dc-759b-40d8-8328-3134d9851ac0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">SomeView()
    .opacity(isVisible ? 1 : 0)</code></pre></div><p>Avoid create/destroy for visibility:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;b50b41e3-c10d-4d31-8fd1-d00d6a62a0f7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">if isVisible {
    SomeView()
}</code></pre></div><h4>Why?</h4><p>Conditional inclusion can lose state, hurt animation, and break identity. Modifiers keep the same view across state changes.</p><p>Conditionals are appropriate when you truly have different views:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;c5ba3f42-405e-4a12-b638-822e0f046403&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">if isLoggedIn {
    DashboardView()
} else {
    LoginView()
}

if let user {
    UserProfileView(user: user)
}</code></pre></div><p>That last one matters. Optional content is fine. Fundamentally different screens are fine. <em><strong>&#8220;Hide this card for a second&#8221;</strong></em> is usually not, that is opacity (and often <em><strong>allowsHitTesting(false)</strong></em> when it should not receive taps).</p><h3>What I Found in Walk Mate</h3><p>I put breakpoints on a few conditionals in <em><strong>ContentView</strong></em> and <em><strong>AdventureCompletedView</strong></em> and asked myself: is this two states of the same view or truly different views?</p><ol><li><p><strong>Good: same view, different visual state</strong></p></li></ol><p>In <em><strong>MyStatsView</strong></em>, locked milestones stay in the tree and only dim:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;b60d108d-ea06-4939-a28c-cb62850b361d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">.opacity(milestone.unlocked ? 1.0 : 0.7)</code></pre></div><p>Same pattern in <em><strong>MapStyleView</strong></em> for non-interactive rows:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;27c75b77-c5a8-4a25-bacb-25acdad3f979&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">.opacity(isInteractive ? 1 : 0.55)</code></pre></div><p>That is exactly Apple&#8217;s <em><strong>&#8220;no-effect modifier&#8221;</strong></em> advice. One view identity. Value changes.</p><ol start="2"><li><p><strong>Correct conditionals: fundamentally different UI modes</strong></p></li></ol><p>In <em><strong>ContentForegroundStack</strong></em> I switch between drawing and the normal bottom chrome:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;5e1dfc93-bb31-474d-b1b2-3ad52e3493fd&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">if store.drawRoute.isDrawingRoute {
    DrawRouteOverlayView(store: store)
} else if !store.avoidance.isEditingAvoidedLocations {
    BottomControls(...)
}</code></pre></div><p><em><strong>DrawRouteOverlayView</strong></em> and <em><strong>BottomControls</strong></em> are not two opacities of the same thing. They are different modes of the map UI. Same idea for <em><strong>MapControlsView</strong></em> and <em><strong>AdventureStatsView</strong></em> disappearing while the user draw or mark avoidance. Those branches are appropriate.</p><p>Optional content is also fine:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;21218670-9949-4099-8cd1-74522c7dbfe2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">if let medal {
    MedalCollectionToast(...)
}</code></pre></div><p>That matches Apple&#8217;s <em><strong>if let user</strong></em> example.</p><ol start="3"><li><p><strong>The gray area: overlays that come and go</strong></p></li></ol><p><em><strong>ContentMainView</strong></em> still has:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;bf0d9ff2-ddc0-4400-8f65-0d23c9c90375&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">if store.routeGeneration.isGeneratingRoutes {
    LoadingOverlay()
}

if store.walkSession.showAdventureCompleted {
    AdventureCompletedView(store: store)
}</code></pre></div><p>And inside <em><strong>AdventureCompletedView</strong></em>, confetti is also gated:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;c5042d67-6b60-40e5-820a-6175fda56cf0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">if showConfetti {
    ConfettiView()
}</code></pre></div><p>Are these <em><strong>&#8220;wrong&#8221;</strong></em>? Not automatically. Loading and adventure-completed are closer to modal layers than to a button that toggles highlight. Creating them when needed is readable and for <em><strong>LoadingOverlay</strong></em> there is little state to preserve.</p><p>But <em><strong>AdventureCompletedView</strong></em> holds animation state:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;c32e6b91-4f0b-4816-bad3-88c968be225e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">@State private var showConfetti = false
@State private var cardScale: CGFloat = 0.8
@State private var cardOpacity: Double = 0</code></pre></div><p>The outer if in <em><strong>ContentMainView</strong></em> creates and destroys that whole view. That resets <em><strong>@State</strong></em> every time the overlay appears which we actually rely on because .<em><strong>onAppear</strong></em> kicks the spring for <em><strong>cardScale/cardOpacity</strong></em>. So the conditional is doing intentional lifecycle work not just <em><strong>&#8220;hide me&#8221;</strong></em>.</p><p>Still, there is a smell &#129448;: <em><strong>AdventureCompletedView</strong></em>&#8217;s body also checks <em><strong>showAdventureCompleted</strong></em> and <em><strong>selectedRoute</strong></em> again. Double gate. The parent already decided the overlay should exist. The child repeating the same if means an empty branch when state is inconsistent and it keeps the <em><strong>&#8220;presence&#8221;</strong></em> question inside a view that also owns animation state.</p><p>For confetti specifically, Apple&#8217;s opacity guidance fits better than create/destroy:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;57e91d75-3577-4b75-baa0-35322eef994c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">ConfettiView()
    .opacity(showConfetti ? 1 : 0)
    .allowsHitTesting(false)</code></pre></div><p>Same view. No identity flip when the Bool toggles. If <em><strong>ConfettiView</strong></em> ever grows internal state, you won&#8217;t reset it by accident.</p><blockquote><p><strong>Interview answer in one line:</strong> <em>use if when the view graph is genuinely different; use modifiers when you are only changing how the same view looks or whether it receives interaction.</em></p></blockquote><h3>Practical Checklist (What I Use in Code Reviews Now)</h3><p>1. Ask first: same view, two states or two different views?</p><p>2. Prefer ternary values on always-present modifiers (opacity, foregroundStyle, frame with nil etc.).</p><p>3. Never add a new <em><strong>.if / applyIf View</strong></em> extension. If one exists, call out the risk; refactor in a dedicated change.</p><p>4. Use <em><strong>if / else</strong></em> for real mode switches and <em><strong>if let</strong></em> for optional content.</p><p>5. Be careful when <em><strong>@State</strong></em> lives inside a view that is itself behind an if, otherwise appearance resets state. Sometimes you want that (entrance animation). Sometimes you just lost the user&#8217;s text field.</p><p>6. Do not &#8220;fix&#8221; every overlay if in the app overnight. Loading and completion modals can stay as conditionals when create/destroy is the lifecycle you want.</p><div><hr></div><h4>The Answer I Would Give In an Interview</h4><p>No, not the <em><strong>applyIf / .if</strong></em> extension kind. They wrap <em><strong>if/else</strong></em> around modifiers, change the return type per branch, break structural identity and turn smooth property animations into transitions. <strong>Prefer modifiers</strong> that are always applied with values that change.</p><p>Conditional views (if in a body) are fine when the branches are actually different views or optional content. They are the wrong tool when you only mean &#8220;this same view, hidden or dimmed.&#8221;</p><p>That is Chris Eidhof&#8217;s objc.io argument. It is also what Apple wrote into Xcode 27&#8217;s swiftui-expert-skill. In a map-heavy app like Walk Mate, the win is knowing which of your <em><strong>if</strong></em>s are mode switches, which are optional content and which are visibility hacks that should have been opacity all along.</p><p><strong>Sources</strong> </p><ul><li><p><a href="https://www.objc.io/blog/2021/08/24/conditional-view-modifiers/">Why Conditional View Modifiers are a Bad Idea</a></p></li><li><p>Prefer Modifiers Over Conditional Views - Apple / Xcode 27 swiftui-expert-skill (view-structure.md)</p><p></p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;cf6482d7-12db-44d8-91d8-5fbb0d9f8706&quot;,&quot;caption&quot;:&quot;Apple recently added this to their Xcode 27 coding skills guidance, and it started a good conversation on X. Vincent shared it and Shannon asked a fair question: if you split a large view, does @ViewBuilder help with identity?&quot;,&quot;cta&quot;:null,&quot;showBylines&quot;:true,&quot;showDescription&quot;:true,&quot;showImage&quot;:true,&quot;size&quot;:&quot;lg&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Splitting Large SwiftUI Views in the Apple's way&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:78728032,&quot;name&quot;:&quot;Emre Degirmenci&quot;,&quot;bio&quot;:&quot;iOS Engineer with 8+ years of experience. Writing about building apps with Swift, SwiftUI, and UIKit for Apple platforms, mainly iOS.&quot;,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/40cb062b-0d35-4584-a19e-6dc2d73314ef_3546x3546.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null}],&quot;post_date&quot;:&quot;2026-07-07T11:40:26.572Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/$s_!wOfj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fpbs.substack.com%2Fmedia%2FHMDvSVCbAAAfJY1.jpg&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://emredegirmenci.substack.com/p/splitting-large-swiftui-views-in&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:204279449,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:15,&quot;comment_count&quot;:1,&quot;publication_id&quot;:8687558,&quot;publication_name&quot;:&quot;Emre Degirmenci&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!ozjy!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F40cb062b-0d35-4584-a19e-6dc2d73314ef_3546x3546.jpeg&quot;,&quot;belowTheFold&quot;:true,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div></li></ul>]]></content:encoded></item><item><title><![CDATA[Spec Kit (Spec-Driven Development) in an iOS Project]]></title><description><![CDATA[From vibe coding to Spec-Driven Development: shipping a SwiftUI + TCA feature with GitHub Spec Kit inside Cursor]]></description><link>https://emredegirmenci.substack.com/p/spec-kit-spec-driven-development</link><guid isPermaLink="false">https://emredegirmenci.substack.com/p/spec-kit-spec-driven-development</guid><dc:creator><![CDATA[Emre Degirmenci]]></dc:creator><pubDate>Mon, 13 Jul 2026 12:42:27 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!6QYs!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c91d7ed-248c-4851-8e8f-1ea6896fbb64_1182x596.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this post, I wanna tell you something about a paradigm that I didn&#8217;t know existed, <strong>Spec-Driven Development</strong>. Last week, a friend of mine told me there is an open source library called <a href="https://github.com/github/spec-kit">Spec Kit</a> provided by GitHub and <span>at his company they don&#8217;t really &#8220;prompt&#8221; AI anymore. They run it against a spec.</span> I directly dug into it since it is way more automated than the casual vibe-coding and tried to apply it in my toy project. </p><p>The main difference between SDD and vibe-coding is vibe-coding excels at rapid prototyping and SDD is essential for complex production systems to prevent AI hallucinations, architectural drift and technical debt. <strong>Spec Kit</strong> <strong>is a workflow that executes scripts and very fine-tuned prompts to produce high quality context for your coding agent and the lightweight framework compared to the other ones.</strong> And this is what GitHub says:</p><blockquote><p>Spec-Driven Development <strong>flips the script</strong> on traditional software development. For decades, code has been king &#8212; specifications were just scaffolding we built and discarded once the &#8220;real work&#8221; of coding began. Spec-Driven Development changes this: <strong>specifications become executable</strong>, directly generating working implementations rather than just guiding them. </p></blockquote><p><span>AI-assisted iOS development for me is still typing a prompt in Cursor and pray </span>&#128720;<span> Type a prompt, skim the diff, accept, re-prompt when the tests break.</span></p><p><span>Spec-Driven Development (SDD) is the pushback. And GitHub&#8217;s </span><a href="https://github.com/github/spec-kit"><span>Spec Kit</span></a><span> is the toolkit that makes it usable inside the editor you already have. </span></p><p><span>This post is a walkthrough of installing Spec Kit into an existing ~40k-line SwiftUI + TCA app </span><a href="https://apps.apple.com/us/app/walkmate-route-generator/id6739468121"><span>Walk Mate</span></a><span> and for learning purposes shipping one small feature (Favorites sort + search) end-to-end through the full flow. Every step will be tied to a real file in the repo.</span></p><h3><span>What SDD actually is (and what Spec Kit gives you)</span></h3><p><span>SDD is not </span><em><strong><span>write more comments before you code</span></strong></em><span>. It is a fixed pipeline:</span></p><p><strong><span>1. Constitution:</span></strong><span> the rules of your project</span></p><p><strong><span>2.</span></strong><span> </span><strong><span>Specification:</span></strong><span> user stories, requirements, success criteria</span></p><p><strong><span>3.</span></strong><span> </span><strong><span>Plan:</span></strong><span> the technical approach with your chosen stack (SwiftUI + TCA)</span></p><p><strong><span>4.</span></strong><span> </span><strong><span>Tasks:</span></strong><span> the plan decomposed into buildable phases (spec + plan)</span></p><p><strong><span>5.</span></strong><span> </span><strong><span>Implementation:</span></strong><span> one phase at a time</span></p><p><span>Each stage validated before the next. </span></p><p><span>Spec Kit is GitHub&#8217;s implementation of this pipeline. You&#8217;ll get:</span></p><ul><li><p><span> A CLI </span><em><strong><span>specify</span></strong></em><span> that scaffolds the artifacts into your project</span></p></li><li><p><span>Markdown .md templates for each stage (constitution, spec, plan, tasks)</span></p></li><li><p><span>Editor-native slash commands so Cursor can drive the flow with a shared vocabulary</span></p></li></ul><p><span>The command surface I used in Cursor:</span></p><p><strong><span>Command                       | Purpose</span></strong></p><p><em><strong><span>/speckit-constitution</span></strong></em><span>      | Project principles and hard constraints</span></p><p><em><strong><span>/speckit-specify</span></strong></em><span>               | User stories, requirements, success criteria</span></p><p><em><strong><span>/speckit-clarify</span></strong></em><span>                | Structured Q&amp;A to close ambiguity before planning</span></p><p><em><strong><span>/s</span>peckit-plan</strong></em><code>        </code><span>| Technical plan with the chosen stack</span></p><p><em><strong><span>/speckit-tasks</span></strong></em><span>                   | Decompose the plan into buildable phases</span></p><p><em><strong><span>/speckit-analyze</span></strong></em><span>              | Cross-artifact consistency check</span></p><p><em><strong><span>/speckit-implement</span></strong></em><span>         | Execute one phase at a time</span></p><h3><span>Setup in an existing project</span></h3><p><span>You need three things: </span><a href="https://docs.astral.sh/uv/"><span>uv</span></a><span> (Astral&#8217;s Python package manager), </span><strong><span>Python 3.11+</span></strong><span>, and </span><strong><span>Cursor</span></strong><span>. Install the CLI once:</span></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;52f2060e-a3c0-4278-9154-f3bc12918930&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">&gt; uv tool install specify-cli --from git+https://github.com/github/spec-kit.git</code></pre></div><p><span>Then, inside your Xcode project&#8217;s root:</span></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;f3680282-e882-4cfd-8abb-52b8e4b9c0c3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">&gt; cd MyExistingIOSApp
&gt; specify init . --integration cursor-agent --force</code></pre></div><p><span>Two things worth calling out here. </span><em><strong><span>--force</span></strong></em><span> is necessary because the directory isn&#8217;t empty Spec Kit merges its templates in rather than wiping. And </span><em><strong><span>cursor-agent</span></strong></em><span> is the skills-based integration. It installs 10 skills under </span><em><strong><span>.cursor/skills/</span></strong></em><span>, one per slash command so Cursor&#8217;s palette picks them up automatically.</span></p><p><span>After init, the new tree looks like this:</span></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!6QYs!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c91d7ed-248c-4851-8e8f-1ea6896fbb64_1182x596.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!6QYs!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c91d7ed-248c-4851-8e8f-1ea6896fbb64_1182x596.png 424w, https://substackcdn.com/image/fetch/$s_!6QYs!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c91d7ed-248c-4851-8e8f-1ea6896fbb64_1182x596.png 848w, https://substackcdn.com/image/fetch/$s_!6QYs!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c91d7ed-248c-4851-8e8f-1ea6896fbb64_1182x596.png 1272w, https://substackcdn.com/image/fetch/$s_!6QYs!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c91d7ed-248c-4851-8e8f-1ea6896fbb64_1182x596.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!6QYs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c91d7ed-248c-4851-8e8f-1ea6896fbb64_1182x596.png" width="1182" height="596" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0c91d7ed-248c-4851-8e8f-1ea6896fbb64_1182x596.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:596,&quot;width&quot;:1182,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:87735,&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://emredegirmenci.substack.com/i/206353670?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c91d7ed-248c-4851-8e8f-1ea6896fbb64_1182x596.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_!6QYs!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c91d7ed-248c-4851-8e8f-1ea6896fbb64_1182x596.png 424w, https://substackcdn.com/image/fetch/$s_!6QYs!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c91d7ed-248c-4851-8e8f-1ea6896fbb64_1182x596.png 848w, https://substackcdn.com/image/fetch/$s_!6QYs!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c91d7ed-248c-4851-8e8f-1ea6896fbb64_1182x596.png 1272w, https://substackcdn.com/image/fetch/$s_!6QYs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c91d7ed-248c-4851-8e8f-1ea6896fbb64_1182x596.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 aria-hidden="true" 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><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><span>Two side quests before you move on:</span></p><ul><li><p><span>First, </span><em><strong><span>gitignore:</span></strong></em><span> Spec Kit&#8217;s post-install output specifically warns that agent folders can hold credentials and suggests </span><em><strong><span>.gitignore</span></strong></em><span> -ing </span><em><strong><span>.cursor/</span></strong></em><span>. I already ignored that folder which means my installed skills are per-clone. For a solo indie app that&#8217;s fine. For a team I would commit at least </span><em><strong><span>.cursor/skills/</span></strong></em><span> so everyone has the same slash commands. Make it an explicit decision.</span></p></li><li><p><span>Second, Xcode 16+&#8217;s file-system-synchronized groups. Any new </span><em><strong><span>.swift</span></strong></em><span> file dropped into a synchronized folder is auto-added to the target. When </span><em><strong><span>/speckit-implement</span></strong></em><span> creates </span><strong><span>FavoritesSortOption.swift</span></strong><span>, I don&#8217;t need to touch </span><em><strong><span>project.pbxproj</span></strong></em><span>. If you&#8217;re on a legacy project with manually managed file references, budget extra time for this friction.</span></p></li></ul><h3><span>The constitution: fold in what your project already enforces</span></h3><p><span>This is the most crucial section for me because it&#8217;s where SDD stops being a novelty and starts paying rent </span>&#129297;<span> This usually set up once at the very start of your project.</span></p><p><span>In a greenfield app the constitution is aspirational: rules you </span><strong><span>hope</span></strong><span> to follow. In an existing app it&#8217;s descriptive: rules the codebase </span><strong><span>already</span></strong><span> enforces (tribal knowledge, PR comments or Cursor rules files) but in a form no AI agent can read. Writing the constitution is the act of making that knowledge machine-readable for every future agent session.</span></p><p><span>I ended up with four principles in </span><em><strong><span>.specify/memory/constitution.md:</span></strong></em></p><h4><strong><span>1. SwiftUI + TCA architecture</span></strong></h4><p><em><strong><span>@Reducer</span></strong></em><span>, </span><em><strong><span>@ObservableState</span></strong></em><span>, delegate actions, </span><em><strong><span>@Dependency</span></strong></em><span> clients. This is descriptive: </span><em><strong><span>FavoriteRoutesFeature.swift</span></strong></em><span> already uses </span><em><strong><span>@Dependency(\.persistenceClient)</span></strong></em><span> and a </span><em><strong><span>Delegate</span></strong></em><span> enum to talk to its parent. The constitution just names the pattern.</span></p><h4><strong><span>2. Localization completeness</span></strong></h4><p><span>Every user-facing string must exist in all 15 locales (da, de, es, fr, it, ja, ko, nl, pl, pt, ru, sv, tr, zh-Hans, zh-Hant) inside </span><em><strong><span>Localizable.xcstrings</span></strong></em><span> with </span><em><strong><span>&#8221;state&#8221;: &#8220;translated&#8221;</span></strong></em><span>. This is a verbatim promotion of </span><em><strong><span>.cursor/rules/localization.mdc</span></strong></em><span>  same rule but now the AI sees it at every SDD step not only when it happens to load that rule file.</span></p><h4><strong><span>3. No magic numbers in UI</span></strong></h4><p><span>Layout, spacing, corner radius, animation timing become named constants under </span><em><strong><span>AppConstants.UI</span></strong></em><span>, </span><em><strong><span>Typography</span></strong></em><span>, </span><em><strong><span>AppColors</span></strong></em><span> or a feature-scoped </span><em><strong><span>UIConstants</span></strong></em><strong><span>.&lt;Feature&gt;</span></strong><span> enum.</span></p><h4><strong><span>4. Test-first for reducer logic</span></strong></h4><p><span>Swift Testing + TCA </span><em><strong><span>TestStore</span></strong></em><span> before or alongside any reducer change.</span></p><h3><span>Driving one real feature through the pipeline</span></h3><p><span>The feature: add user-selectable sorting (by date saved, name, distance) and search to the Favorites list in Walk Mate. Small, self-contained, real. Two production files touched at the start </span><strong><span>FavoriteRoutesFeature.swift</span></strong><span> and </span><strong><span>FavoriteRoutesView.swift</span></strong><span>.</span></p><h4><strong><span>1. /speckit-specify (define what to build)</span></strong></h4><p><span>This is where we tell the agent what we&#8217;d like to build. This step excludes any technical information. It&#8217;s purely the business requirements for this feature.</span></p><p><span>Input verbatim: </span><em><span>&#8220;Add user-selectable sorting (date, name, distance) and search to the Favorites list.&#8221;</span></em><span> Spec Kit generated a feature slug (001-favorites-sort-search), scaffolded a directory under specs/, and dropped a filled-out spec.md in it three prioritized user stories (P1 sort, P2 search, plus edge cases), functional requirements FR-001 through FR-010, and measurable success criteria (SC-001: </span><em><span>&#8220;20 favorites, target route found by name in under 5 seconds&#8221;</span></em><span>).</span></p><p><span>The template forced a discipline I keep skipping when I write my own tickets: independently testable user stories, prioritized. If I had just typed </span><em><span>&#8220;add sort and search&#8221;</span></em><span> into a normal ticket, sort and search would have shipped bundled. Here they became User Story 1 (P1) and User Story 2 (P2), each with its own acceptance scenarios, either shippable alone.</span></p><h4><strong><span>2. /speckit-clarify (optional follow up questions)</span></strong></h4><p><span>This is the optional action where the agent can look at our requirements and ask any follow-up questions. </span></p><p><span>Eight questions surfaced. A few of them:</span></p><ul><li><p><span>What&#8217;s the default sort? (Answer: dateSaved descending, matches current behavior, so users who never touch the menu see no change.)</span></p></li><li><p><span>Does search match name only or also formatted stats like &#8220;3.2 km&#8221;? (Name only. Matching localized formatted text would be fragile across locales and units.)</span></p></li><li><p><span>How do we sort favorites whose name is empty? (Sort them last when the user picks &#8220;Name&#8221;, with savedDate as tie-breaker.)</span></p></li><li><p><span>Persist sort selection? Persist search text? (Sort yes, search no.)</span></p></li></ul><h4><strong><span>3. /speckit-plan (implementation plan)</span></strong></h4><p><span>The planning phase is really cozy. This will create a plan, the data models, the service interfaces and even perform research.<br>The current view had a private computed property doing the sorting inline </span>which violates Principle I (business logic in views). The plan moved sort and filter into <em><strong>FavoriteRoutesFeature.State.filteredSortedFavorites</strong></em> a computed property on the state. The view goes back to being a thin observer.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;2ba03ddd-c96a-42fb-ad81-d8d13e55e0e1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">private var sortedFavoriteRoutes: [FavoriteRoute] {
    store.favoriteRoutes.sorted { $0.savedDate &gt; $1.savedDate }
}</code></pre></div><p><span>Notice: </span><strong><span>the plan</span></strong><span> step </span><strong><span>not</span></strong><span> a </span><strong><span>code reviewer</span></strong><span>, caught the constitution violation. That&#8217;s the whole point.</span></p><p><span>The plan ends with a five item constitution-compliance checklist, all green. If any box had been red, the plan would have been the artifact I fixed, </span><strong><span>not the code.</span></strong></p><h4><strong><span>4. /speckit-tasks (break plan into actionable steps)</span></strong></h4><p>It takes the plan and break it up into individual tasks. These are all the individual tasks like the actual code changes to implement this feature.</p><p><span>Tasks came out in three phases: </span></p><p><span>A (reducer + data model + persistence + tests), </span></p><p><span>B (view with sort menu and .searchable), </span></p><p><span>C (localization for all 15 locales). </span></p><p><span>Ordering matters: each phase leaves the project buildable and green.</span></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!UUv3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb3d5d8b-6219-4144-af3a-79f40ca0a437_1142x400.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!UUv3!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb3d5d8b-6219-4144-af3a-79f40ca0a437_1142x400.png 424w, https://substackcdn.com/image/fetch/$s_!UUv3!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb3d5d8b-6219-4144-af3a-79f40ca0a437_1142x400.png 848w, https://substackcdn.com/image/fetch/$s_!UUv3!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb3d5d8b-6219-4144-af3a-79f40ca0a437_1142x400.png 1272w, https://substackcdn.com/image/fetch/$s_!UUv3!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb3d5d8b-6219-4144-af3a-79f40ca0a437_1142x400.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!UUv3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb3d5d8b-6219-4144-af3a-79f40ca0a437_1142x400.png" width="1142" height="400" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cb3d5d8b-6219-4144-af3a-79f40ca0a437_1142x400.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:400,&quot;width&quot;:1142,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:85984,&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://emredegirmenci.substack.com/i/206353670?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb3d5d8b-6219-4144-af3a-79f40ca0a437_1142x400.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_!UUv3!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb3d5d8b-6219-4144-af3a-79f40ca0a437_1142x400.png 424w, https://substackcdn.com/image/fetch/$s_!UUv3!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb3d5d8b-6219-4144-af3a-79f40ca0a437_1142x400.png 848w, https://substackcdn.com/image/fetch/$s_!UUv3!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb3d5d8b-6219-4144-af3a-79f40ca0a437_1142x400.png 1272w, https://substackcdn.com/image/fetch/$s_!UUv3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcb3d5d8b-6219-4144-af3a-79f40ca0a437_1142x400.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 aria-hidden="true" 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><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><span>/speckit-analyze then produced an alignment matrix mapping every FR to a task, a test and a status. That table is now the artifact I&#8217;d hand to a reviewer instead of &#8220;trust me, it&#8217;s covered.&#8221; </span>&#128556;</p><h4><strong>5. /speckit-analyze (optional validate docs)</strong></h4><p>Then we have another optional step called analyze. It will simply look at everything that&#8217;s been produced before it to make sure that the documentation is complete and we&#8217;ve covered everything.</p><h4><strong><span>6. /speckit-implement (execute tasks and test)</span></strong></h4><p><span>Then finally, we have the implement step. This is where we tell the coding agent, hey, we have everything we need. Go ahead and implement all of these tasks. We should be able to test our changes in the system and ask the agent to make any adjustments. </span></p><p><span>Concrete numbers from the run:</span></p><ul><li><p><span>Files touched: </span><em><strong><span>FavoritesSortOption.swift (new), PersistenceClient.swift, FavoriteRoutesFeature.swift, FavoriteRoutesFeatureTests.swift, FavoriteRoutesView.swift, UIConstants.swift, Shared/L10n.swift, Localizable.xcstrings.</span></strong></em></p></li><li><p><span>Localization: 5 new keys &#215; 15 locales = 75 xcstrings entries, zero missed locales. This is Principle II operationalized. The AI knew the constitution required 15 locales, so it produced 15.</span></p></li><li><p><span>Tests: 12 in FavoriteRoutesFeatureTests before, 21 after (+9 new). All 88 tests across 10 suites pass.</span></p></li><li><p><span>Compile-break as a feature. Widening PersistenceClient with two new closures broke every test that constructed one. That&#8217;s not a bug, it&#8217;s the compiler surfacing the exact migration the plan had already enumerated. One edit fixed all callsites. Compiler-enforced migrations are a friend of SDD, they turn </span><em><span>&#8220;did we get every callsite?&#8221;</span></em><span> from a grep into a build error.</span></p></li></ul><p><span>The view diff is the tweetable moment:</span></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;bdd6a102-2511-4187-b661-66b1f26c17f0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">// Before
private var sortedFavoriteRoutes: [FavoriteRoute] {
    store.favoriteRoutes.sorted { $0.savedDate &gt; $1.savedDate }
}
// ...
ForEach(sortedFavoriteRoutes) { favorite in ... }

// After
ForEach(store.filteredSortedFavorites) { favorite in ... }</code></pre></div><p><span>That&#8217;s what &#8220;thin view, fat reducer&#8221; looks like when it&#8217;s enforced by a </span><strong><span>plan</span></strong><span> and not by a reviewer&#8217;s comment.</span></p><h3>What Spec Kit does not solve?</h3><ul><li><p><span>Overhead on tiny changes. Running </span><em><strong><span>/speckit-specify</span></strong></em><span> to add a SwiftUI </span><em><strong><span>#Preview</span></strong></em><span> block or fix a typo is absurd. Reserve SDD for changes that touch more than one file or introduce user-visible behavior. Below that bar, just edit the code :)</span></p></li><li><p><span>The constitution is only as good as your enforcement. Nothing in Spec Kit runs tests, greps for magic numbers, or diffs your xcstrings for missing locales. The constitution is a single source of truth the AI reads; enforcement is still your CI, your pre-commit, your reviewer.</span></p></li><li><p><span>Xcode integration is via file system. Spec Kit never touches project.pbxproj. On Xcode 16+ with synchronized groups this is fine. On legacy projects, add friction to your estimate.</span></p></li><li><p><span>Localization can&#8217;t be verified without inspecting the catalog. </span><em><strong><span>String(localized:)</span></strong></em><span> alone doesn&#8217;t guarantee entries exist in </span><em><strong><span>Localizable.xcstrings</span></strong></em><span>. I ran a one-liner to prove it after each phase:</span></p></li></ul><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;6bed6f20-09b7-46d8-af62-5e1ea17201d4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">python3 -c "import json; d=json.load(open('.../Localizable.xcstrings')); \
[print(k, len(d['strings'][k]['localizations'])) \
 for k in ['Sort','Name','Date saved','Search favorites','No matching favorites']]"</code></pre></div><div><hr></div><p>SDD is a framework for making AI collaboration reviewable: every step produces an artifact you&#8217;d be willing to defend in code review and every artifact is validated against a constitution you wrote deliberately. If you&#8217;ve been using Cursor like me as a smarter autocomplete, this is the next promotion.</p><p><span>Two pieces I leaned on heavily and would recommend as further reading: Mad Devs on SDD in iOS (</span><a href="https://maddevs.io/writeups/spec-driven-development-in-ios/"><span>https://maddevs.io/writeups/spec-driven-development-in-ios/</span></a><span>) for the process wisdom, and Stackademic&#8217;s &#8220;Death of Vibe Coding&#8221; (</span><a href="https://blog.stackademic.com/kiro-ai-for-ios-how-agentic-development-and-spec-driven-swiftui-replace-prompt-coding-091bf760bdbc"><span>https://blog.stackademic.com/kiro-ai-for-ios-how-agentic-development-and-spec-driven-swiftui-replace-prompt-coding-091bf760bdbc</span></a><span>) for the mindset shift.</span></p><p><span>Next I want to write about adding </span><em><strong><span>/speckit-checklist</span></strong></em><span> to CI so the constitution enforces itself in PR review, instead of quietly hoping I&#8217;ll remember. If the constitution is the point, that&#8217;s the natural next step.</span></p><p><strong>Sources:</strong></p><ul><li><p>Other similar options to Spec Kit; <a href="https://specs.md">https://specs.md</a>, <a href="https://github.com/bmad-code-org/bmad-method">https://github.com/bmad-code-org/bmad-method</a></p></li><li><p>Mad Devs on SDD in iOS - (<a href="https://maddevs.io/writeups/spec-driven-development-in-ios/">https://maddevs.io/writeups/spec-driven-development-in-ios/</a>)</p></li><li><p>Death of Vibe Coding - (<a href="https://blog.stackademic.com/kiro-ai-for-ios-how-agentic-development-and-spec-driven-swiftui-replace-prompt-coding-091bf760bdbc">https://blog.stackademic.com/kiro-ai-for-ios-how-agentic-development-and-spec-driven-swiftui-replace-prompt-coding-091bf760bdbc</a>)</p></li></ul><p></p>]]></content:encoded></item><item><title><![CDATA[Splitting Large SwiftUI Views in the Apple's way]]></title><description><![CDATA[Extract Subviews, Not Computed Properties (and Where @ViewBuilder Actually Fits)]]></description><link>https://emredegirmenci.substack.com/p/splitting-large-swiftui-views-in</link><guid isPermaLink="false">https://emredegirmenci.substack.com/p/splitting-large-swiftui-views-in</guid><dc:creator><![CDATA[Emre Degirmenci]]></dc:creator><pubDate>Tue, 07 Jul 2026 11:40:26 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!wOfj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fpbs.substack.com%2Fmedia%2FHMDvSVCbAAAfJY1.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Apple recently added this to their Xcode 27 coding skills guidance, and it started a good conversation on X. Vincent <strong><a href="https://twitter.com/v_pradeilles">shared it</a></strong> and Shannon asked a fair question: <strong>if you split a large view, does </strong><em>@ViewBuilder</em><strong> help with identity?</strong></p><div class="twitter-embed" data-attrs="{&quot;url&quot;:&quot;https://x.com/v_pradeilles/status/2071918929975685351?s=20&quot;,&quot;full_text&quot;:&quot;Don't use computed properties to split a large View &#128581;&#127997;&#8205;&#9792;&#65039;&#128581;&#127995;&#8205;&#9794;&#65039;\n\nThis an official bad practice written by Apple in Xcode 27 coding skills &quot;,&quot;username&quot;:&quot;v_pradeilles&quot;,&quot;name&quot;:&quot;Vincent Pradeilles&quot;,&quot;profile_image_url&quot;:&quot;https://pbs.substack.com/profile_images/1587390960375316481/hFEl2TXy_normal.jpg&quot;,&quot;date&quot;:&quot;2026-06-30T11:29:01.000Z&quot;,&quot;photos&quot;:[{&quot;img_url&quot;:&quot;https://pbs.substack.com/media/HMDvSVCbAAAfJY1.jpg&quot;,&quot;link_url&quot;:&quot;https://t.co/6H7YawLfCI&quot;}],&quot;quoted_tweet&quot;:{},&quot;reply_count&quot;:9,&quot;retweet_count&quot;:22,&quot;like_count&quot;:210,&quot;impression_count&quot;:9935,&quot;expanded_url&quot;:null,&quot;video_url&quot;:null,&quot;video_preview_media_key&quot;:null,&quot;belowTheFold&quot;:false}" data-component-name="Twitter2ToDOM"></div><p>My reply was:</p><blockquote><p>@ViewBuilder gives you clean structural identity for if/switch branches but it doesn&#8217;t create a new invalidation boundary. Same struct, same boundary. Separate View types are still the win for performance imho :)</p></blockquote><p>I applied this in my app <strong><a href="https://apple.co/4mz7vev">Walk Mate</a></strong> while refactoring a few heavy screens. This post is about what I changed, why it matters and where <em><strong>@ViewBuilder</strong></em> still has a place.</p><h3>The Core Idea</h3><p>When state changes, SwiftUI re-runs the <em><strong>body </strong></em>of the smallest enclosing <strong>view type </strong>that depends on that state. Everything inside that body gets evaluated again: conditionals, modifier chains, string interpolation, helper functions, computed properties. Same invalidation boundary. Same cost.</p><p>A separate <em><strong>View </strong></em>struct with narrow inputs gets its <strong>own boundary</strong>. <strong>SwiftUI can skip its body when only its inputs are unchanged.</strong> That&#8217;s the difference &#8220;skip for readability&#8221; and &#8220;split for performance&#8221;.</p><h3>Extract Subviews, Not Computed Properties</h3><h4>What I had before</h4><p>My <em><strong>ContentView </strong></em>was already split into computed properties:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;798d74b2-da68-4bec-b4ff-184bcbb01310&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">struct ContentView: View {
    @Bindable var store: StoreOf&lt;ContentFeature&gt; // TCA thing :)
    @Namespace private var mapScope

    var body: some View {
        configuredContent
    }

    private var configuredContent: some View {
        lifecycleContent
            .alert($store.scope(state: \.alert, action: \.alert))
            .sheet(item: $store.scope(state: \.settingsSheet, action: \.settingsSheet)) { ... }
            // many more modifiers...
    }

    private var lifecycleContent: some View {
        mainContent
            .onAppear { store.send(.onAppear) }
    }

    private var mainContent: some View {
        ZStack(alignment: .bottom) {
            MapContainerView(store: store, mapScope: mapScope)
            // map overlays, loading, bottom controls, medal toast, adventure completed...
        }
    }
}</code></pre></div><p>It reads better than one giant <em><strong>body</strong></em> but every store update still re-evaluates all of those computed properties together.</p><p>Same story in <em><strong>AdventureCompletedView</strong></em>. The completion card lived inline inside <em><strong>body:</strong></em></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;1dd4d384-2395-4bc3-aa70-6aecaa04151e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">var body: some View {
    if store.walkSession.showAdventureCompleted,
       let selectedRoute = store.routeGeneration.selectedRoute {
        ZStack {
            Color.black.opacity(0.5).ignoresSafeArea()

            ScrollView {
                VStack(spacing: 0) {
                    // trophy, title, stats HStack, achievements, buttons...
                    StatCardView(
                        icon: &#8220;medal.fill&#8221;,
                        value: &#8220;\(selectedRoute.medals.filter { $0.isCollected }.count)/\(selectedRoute.medals.count)&#8221;,
                        label: String(localized: &#8220;Medals Collected&#8221;),
                        iconColor: .yellow
                    )
                    // more cards...
                }
            }
        }
    }
}</code></pre></div><p>Any small animation state change (<em><strong>cardScale</strong></em>, <em><strong>cardOpacity</strong></em>, <em><strong>confetti</strong></em>) could pull the whole card tree back through evaluation &#128260;.</p><h4>What I changed</h4><p>I replaced computed properties with private view structs, each with a focused job:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;38ef18b5-3d35-418c-8e0d-2d37133fdc6d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">struct ContentView: View {
    @Bindable var store: StoreOf&lt;ContentFeature&gt;
    @Namespace private var mapScope

    var body: some View {
        ContentConfiguredView(store: store, mapScope: mapScope)
    }
}

private struct ContentConfiguredView: View {
    @Bindable var store: StoreOf&lt;ContentFeature&gt;
    let mapScope: Namespace.ID

    var body: some View {
        ContentLifecycleView(store: store, mapScope: mapScope)
            .alert($store.scope(state: \.alert, action: \.alert))
            .sheet(item: $store.scope(state: \.settingsSheet, action: \.settingsSheet)) { settingsStore in
                SettingsView(store: settingsStore, contentStore: store)
            }
            // sheets only here
    }
}

private struct ContentLifecycleView: View {
    @Bindable var store: StoreOf&lt;ContentFeature&gt;
    let mapScope: Namespace.ID

    var body: some View {
        ContentMainView(store: store, mapScope: mapScope)
    }
}

private struct ContentMainView: View {
    @Bindable var store: StoreOf&lt;ContentFeature&gt;
    let mapScope: Namespace.ID

    var body: some View {
        ZStack(alignment: .bottom) {
            ContentMapLayer(store: store, mapScope: mapScope)

            if store.routeGeneration.isGeneratingRoutes {
                LoadingOverlay()
            }

            ContentForegroundStack(store: store)

            ContentMedalToastOverlay(
                medal: store.walkSession.lastCollectedMedal,
                isShowing: store.walkSession.showMedalCollectionToast,
                onDismiss: { store.send(.walkSession(.dismissMedalCollectionToast)) }
            )

            if store.walkSession.showAdventureCompleted {
                AdventureCompletedView(store: store)
            }
        }
    }
}</code></pre></div><p>Then I split the map and foreground UI too:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;fe4ebfa9-c029-4926-b358-38ad2619a4cb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">private struct ContentMapLayer: View {
    @Bindable var store: StoreOf&lt;ContentFeature&gt;
    let mapScope: Namespace.ID

    var body: some View {
        MapContainerView(store: store, mapScope: mapScope)
            .overlay(alignment: .trailing) {
                if !store.drawRoute.isDrawingRoute
                    &amp;&amp; !store.avoidance.isMarkingAvoidanceMode
                    &amp;&amp; !store.avoidance.isEditingAvoidedLocations {
                    MapControlsView(store: store, mapScope: mapScope)
                }
            }
            .safeAreaInset(edge: .top) {
                if !store.drawRoute.isDrawingRoute
                    &amp;&amp; !store.avoidance.isMarkingAvoidanceMode
                    &amp;&amp; !store.avoidance.isEditingAvoidedLocations {
                    AdventureStatsView(store: store)
                }
            }
    }
}

private struct ContentForegroundStack: View {
    @Bindable var store: StoreOf&lt;ContentFeature&gt;

    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            if store.avoidance.isMarkingAvoidanceMode {
                ContentAvoidanceBanner(
                    showsTapToMarkBanner: store.avoidance.temporarySelectedSegment == nil
                        &amp;&amp; store.avoidance.avoidedSegments.isEmpty
                )
            }
            Spacer()
            if store.drawRoute.isDrawingRoute {
                DrawRouteOverlayView(store: store)
            } else if !store.avoidance.isMarkingAvoidanceMode
                        &amp;&amp; !store.avoidance.isEditingAvoidedLocations {
                BottomControls(
                    routes: store.routeGeneration.routes,
                    isGeneratingRoutes: store.routeGeneration.isGeneratingRoutes,
                    store: store,
                    selectedRoute: store.routeGeneration.selectedRoute
                )
            }
        }
    }
}</code></pre></div><p>The parent <em><strong>ContentView</strong></em> body is now one line. Each layer owns its own invalidation boundary.</p><p>I extracted the card into <em><strong>AdventureCompletionCard</strong></em> and passed only what it needs:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;c78e0314-7c3e-40f5-a2c5-4695f0800108&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">AdventureCompletionCard(
    selectedRoute: selectedRoute,
    distanceWalked: store.walkSession.distanceWalked,
    distanceUnit: store.distanceUnit,
    newlyUnlockedMilestones: store.walkSession.newlyUnlockedMilestones,
    onWalkAgain: { store.send(.walkSession(.walkAgainTapped)) }
)</code></pre></div><p>And the card struct:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;6b1b9339-51e6-44ee-bdf5-35dbabd60f97&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">private struct AdventureCompletionCard: View {
    let selectedRoute: Route
    let distanceWalked: Double
    let distanceUnit: DistanceUnit
    let newlyUnlockedMilestones: [Milestone]
    let onWalkAgain: () -&gt; Void

    var body: some View {
        VStack(spacing: 0) {
            // trophy, title, stats, achievements, buttons
        }
        .compatibleGlassEffect(cornerRadius: 24)
    }
          ...
}</code></pre></div><p>I also split button styling into <em><strong>AdventureCompletedActionButtons, AdventureCompletedActionButton</strong></em> and <em><strong>AdventureCompletedButtonBackground</strong></em>.</p><p>Now animation state (<em><strong>cardScale, showConfetti</strong></em>) lives in <em><strong>AdventureCompletedView</strong></em>, while the heavy card content lives in a separate type with stable inputs.</p><h4>What this gives you</h4><ul><li><p>Better diffing: SwiftUI compares struct inputs, not one giant parent body.</p></li><li><p>Less wasted work on unrelated state changes.</p></li><li><p>Easier previews: you can preview <em><strong>AdventureCompletionCard</strong></em> with sample data.</p></li><li><p>Clearer ownership: each file section has one responsibility. Solid? :D </p></li></ul><h3>@ViewBuilder (and What It Does NOT Do)</h3><p>This is where the X thread gets interesting. <em><strong>@ViewBuilder </strong></em>is useful but it solves a different problem than subview extraction.</p><h4>What @ViewBuilder is good at</h4><p><em><strong>@ViewBuilder</strong></em> helps when you need conditional view structure inside one body:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;13baafff-f8ea-4900-b50d-6f904af642b0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">@ViewBuilder
private var conditionalView: some View {
    if isExpanded {
        VStack {
            Text(&#8221;Expanded View&#8221;)
            Image(systemName: &#8220;star&#8221;)
        }
    } else {
        Text(&#8221;Collapsed View&#8221;)
    }
}</code></pre></div><p>It gives you clean syntax for <em><strong>if/switch</strong></em> branches and structural identity for those branches.</p><p>Apple&#8217;s guidance also says: use it for <strong>small</strong>, <strong>simple</strong> <strong>sections</strong> that <strong>do not</strong> need their <strong>own invalidation boundary.</strong></p><h4>What @ViewBuilder does not do</h4><p>It does <strong>not</strong> create a new invalidation boundary. If you put your whole complex section in a <em><strong>@ViewBuilder</strong></em> function inside the parent struct that function still runs whenever the parent body runs &#128260;.</p><p>So this is still one boundary. Tapping the button re-evaluates <em><strong>complexSection() </strong></em>every time.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;ff4b760b-ed77-4586-8889-91d46f3b52b2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">struct ParentView: View {
    @State private var count = 0

    var body: some View {
        VStack {
            Button(&#8221;Tap: \(count)&#8221;) { count += 1 }
            complexSection()
        }
    }

    @ViewBuilder
    func complexSection() -&gt; some View {
        ForEach(0..&lt;100) { i in
            Text(&#8221;Item \(i)&#8221;)
        }
    }
}</code></pre></div><h4>The objc.io lesson: conditional branches can break identity</h4><p>Chris Eidhof wrote a great post on this: <strong><a href="https://www.objc.io/blog/2021/08/24/conditional-view-modifiers/">Why Conditional View Modifiers are a Bad Idea</a></strong>.</p><p>The key point: when you branch with <em><strong>if/else</strong></em>, SwiftUI often sees different view types in each branch <em><strong>(_ConditionalContent&lt;...&gt;)</strong></em>. That can:</p><ul><li><p>break smooth animations (fade transition instead of interpolation)</p></li><li><p>reset <em><strong>@State / @StateObject</strong></em> when the branch flips</p></li></ul><p>Example from that post:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;3ee20712-e705-410b-b1d6-311b453c9275&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">Rectangle()
    .applyIf(condition: myState, transform: { $0.frame(width: 100) })</code></pre></div><p>vs</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;878b25ae-948a-41d8-bd56-71f8c5c68a12&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">Rectangle()
    .frame(width: myState ? 100 : nil)</code></pre></div><p>The second one keeps the same view identity and animates correctly.</p><p>Same idea applies to <em><strong>@ViewBuilder</strong></em> conditionals inside one parent: they are fine for structure but they are not a free performance win and they can still hurt identity if you use them like a conditional modifier wrapper.</p><h4>Where I still use small extracted structs instead of @ViewBuilder helpers</h4><p>In Walk Mate, I extracted <em><strong>ContentAvoidanceBanner</strong></em> instead of keeping nested <em><strong>if</strong></em> blocks inline:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;2b56165b-bd32-4785-be26-4580f4427754&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">private struct ContentAvoidanceBanner: View {
    let showsTapToMarkBanner: Bool

    var body: some View {
        VStack(spacing: 0) {
            if showsTapToMarkBanner {
                TapToMarkBanner()
                    .padding(.horizontal, AppConstants.UI.largeSpacing)
                    .padding(.trailing, AppConstants.UI.bannerTrailingPadding)
                    .padding(.top, 80)
            }
        }
    }
}</code></pre></div><p>This is a small view but it has a clear input (<em><strong>showsTapToMarkBanner</strong></em>) and its own boundary. The parent only passes a <em><strong>Bool</strong></em>, not the whole banner layout logic.</p><p>That is the pattern I follow now:</p><ul><li><p><em><strong>@ViewBuilder</strong></em> for tiny local branching when extraction would be noisy</p></li><li><p>separate <em><strong>View</strong></em> structs for anything stateful expensive or reused</p></li></ul><h3>Practical Checklist (What I Use in Code Reviews Now)</h3><ol><li><p>If a screen has <em><strong>private var section:</strong></em><code> </code><em><strong>some View</strong></em> and that section depends on changing state, extract a struct.</p></li><li><p>Pass narrow inputs (<em><strong>Route, Double, [Milestone], Bool</strong></em>) instead of the whole store when possible.</p></li><li><p>Do not use computed properties as a fake performance split &#128514;</p></li><li><p>Do not use custom <em><strong>.applyIf</strong></em> style helpers for modifiers.</p></li><li><p>Prefer <em><strong>modifier(value ? a : b)</strong></em> over <em><strong>if</strong></em> branches for the same view.</p></li><li><p>Keep <em><strong>@ViewBuilder</strong></em> for small structural branching not as a replacement for subviews.</p></li></ol><div><hr></div><p>Splitting a large SwiftUI view is not just a style preference anymore. Apple is calling it out directly and in a map-heavy app like <strong><a href="https://apple.co/4mz7vev">Walk Mate</a></strong> it makes a real difference.</p><p><em><strong>@ViewBuilder</strong></em> is still useful. It gives you clean conditional structure. But if your goal is fewer body re-evaluations, separate <em><strong>View</strong></em> types with narrow inputs are the win.</p><p>If you are refactoring an old screen, start with the noisiest part first: map overlays, stats sections, completion cards, sheet containers. That is where I saw the biggest readability and performance payoff.</p><p><strong><span>Sources</span></strong></p><ul><li><p>Apple view structure guidance (Xcode 27 coding skills) </p></li><li><p>swiftui-expert-skill/references/view-structure.md</p></li><li><p><strong><a href="https://www.objc.io/blog/2021/08/24/conditional-view-modifiers/">Why Conditional View Modifiers are a Bad Idea</a></strong> by Chris Eidhof (objc.io)</p></li><li><p><a href="https://www.avanderlee.com/ai-development/using-xcode-27s-agent-skills-in-claude-codex-and-cursor/">https://www.avanderlee.com/ai-development/using-xcode-27s-agent-skills-in-claude-codex-and-cursor/</a></p><p></p></li></ul>]]></content:encoded></item><item><title><![CDATA[Why My SwiftUI Map Bounced in Heading-Follow Mode?]]></title><description><![CDATA[SwiftUI Map Body Redraws]]></description><link>https://emredegirmenci.substack.com/p/why-my-swiftui-map-bounced-in-heading</link><guid isPermaLink="false">https://emredegirmenci.substack.com/p/why-my-swiftui-map-bounced-in-heading</guid><dc:creator><![CDATA[Emre Degirmenci]]></dc:creator><pubDate>Mon, 29 Jun 2026 13:36:34 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!VQ8B!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc40931e-80c4-43d3-afc8-5d84427d4695_500x500.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_!VQ8B!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc40931e-80c4-43d3-afc8-5d84427d4695_500x500.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!VQ8B!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc40931e-80c4-43d3-afc8-5d84427d4695_500x500.png 424w, https://substackcdn.com/image/fetch/$s_!VQ8B!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc40931e-80c4-43d3-afc8-5d84427d4695_500x500.png 848w, https://substackcdn.com/image/fetch/$s_!VQ8B!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc40931e-80c4-43d3-afc8-5d84427d4695_500x500.png 1272w, https://substackcdn.com/image/fetch/$s_!VQ8B!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc40931e-80c4-43d3-afc8-5d84427d4695_500x500.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!VQ8B!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc40931e-80c4-43d3-afc8-5d84427d4695_500x500.png" width="256" height="256" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/dc40931e-80c4-43d3-afc8-5d84427d4695_500x500.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:500,&quot;width&quot;:500,&quot;resizeWidth&quot;:256,&quot;bytes&quot;:121220,&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://emredegirmenci.substack.com/i/203844371?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc40931e-80c4-43d3-afc8-5d84427d4695_500x500.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_!VQ8B!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc40931e-80c4-43d3-afc8-5d84427d4695_500x500.png 424w, https://substackcdn.com/image/fetch/$s_!VQ8B!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc40931e-80c4-43d3-afc8-5d84427d4695_500x500.png 848w, https://substackcdn.com/image/fetch/$s_!VQ8B!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc40931e-80c4-43d3-afc8-5d84427d4695_500x500.png 1272w, https://substackcdn.com/image/fetch/$s_!VQ8B!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc40931e-80c4-43d3-afc8-5d84427d4695_500x500.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 aria-hidden="true" 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><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>As you know when you double-tapped the user location button in an iOS app, it follows your heading. When I do that I hit a bug in <a href="https://apple.co/4mz7vev">Walk Mate</a> which was architected in <strong>The Composable Architecture,</strong> my daily route generator app . The blue user location button jumped, and bounced with view redraws in SwiftUI. <br><br>The weird part is the same map setup worked fine in my other app, <a href="https://apple.co/3NWejUz">EV Charge Stations Map</a> written in <strong>MVVM</strong>. Same <em><strong>Map</strong></em>, same <em><strong>MapUserLocationButton</strong></em>, same double-tap for heading-follow. One app broken, one app fine.<br><br>In this post I&#8217;ll explain <strong>why</strong> it happens and show to fix in the <strong>pure TCA way</strong>.</p><h3>What is heading-follow mode?</h3><p>When you tap the location button once, <strong>MapKit</strong> enters <strong><span>follow location</span></strong> mode. The map keeps your <strong>blue dot centered</strong>.</p><p>When you tap it <strong><span>twice</span></strong>, it enters <strong><span>heading-follow</span></strong> mode. A small blue cone appears in front of the dot. As you rotate your phone left or right, the map rotates with you.</p><p>That&#8217;s when my bug showed up. At certain zoom levels, the map kept <strong><span>jumping and bouncing</span></strong> while I rotated. Sometimes the heading cone flashed for a moment and then <strong><span>disappeared</span></strong> as if heading mode turned off by itself.</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;a38dd7f1-8c67-4da9-9fec-5ed3eb94d875&quot;,&quot;duration&quot;:null}"></div><h3>The setup (WalkMate)</h3><p>My map lived inside TCA. The camera position was stored in <em><strong>ContentFeature.State</strong></em>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;8e510748-99b2-4256-b512-819a0c649bf5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">var cameraPosition: MapCameraPosition = .region(...)</code></pre></div><p><span>And the </span><em><strong>Map</strong></em><span> used a </span><strong><span>hand-rolled </span></strong>&#129755; <strong><span>binding</span></strong><span> that sent an action on every write:</span></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;0edd18b7-b80f-435e-9664-283528ca1304&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">Map(
    position: Binding(
        get: { store.cameraPosition },
        set: { store.send(.updateCameraPosition($0)) }
    ),
    scope: mapScope
) {
    // route polyline, medals, UserAnnotation(), etc.
}</code></pre></div><p>On paper this looks correct for TCA. State in the store, view reads from store, user interaction writes back through actions. That&#8217;s the pattern we use everywhere else.</p><p>But MapKit in heading-follow mode doesn&#8217;t behave like a text field or a slider :)</p><h3>Why the bug happens?</h3><p>In heading-follow mode, MapKit runs a <strong><span>continuous camera animation</span></strong>. Every time your device heading changes even a little, MapKit updates the camera (many times per second).</p><p>With our hand-rolled &#129755; binding, <strong><span>every single camera frame</span></strong> did this:</p><ol><li><p>MapKit writes a new camera position &#8594; <em><strong>set</strong></em> fires</p></li><li><p><em><strong>store.send(.updateCameraPosition($0))</strong></em> runs</p></li><li><p>Reducer sets <em><strong>state.cameraPosition</strong></em></p></li><li><p>SwiftUI re-renders the <em><strong>Map</strong></em></p></li><li><p><em><strong>get</strong></em> returns the value from state</p></li><li><p>MapKit receives it back as an <em><strong><span>external</span></strong></em> camera command</p></li></ol><p><strong><span>The problem:</span></strong> In step 6, we send the camera position back to MapKit, but that value is already old. MapKit is still rotating the map for heading-follow and at the same time it gets a new position from the app. So the map tries to follow two things at once. That&#8217;s why we see the jump, the shake and why heading mode often turns off. The blue cone goes away.</p><p>When you zoom in, this looks even worse. Small camera mistakes are easier to see when you&#8217;re close to the map.</p><p>There was a second problem too. On location updates, the reducer sometimes did:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;26d54662-3919-4187-8191-511a391ac2a6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">state.cameraPosition = .region(...)</code></pre></div><p><span>That runs on GPS ticks. So right after you entered heading-follow, the next location update could </span><strong><span>overwrite</span></strong><span> MapKit&#8217;s follow pointer with a fixed region. Heading cone flashes &#8594; gone.</span></p><h3>Why <a href="https://apple.co/3NWejUz">EV Charge Stations Map</a> didn&#8217;t have this bug?</h3><p><span>I compared with my other app. The difference wasn&#8217;t the button. It was </span><strong><span>who owns the camera during animation</span></strong><span>.</span></p><p><span>EV app:</span></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;4a699a3a-154d-4da1-82e7-efde4c5d6cd4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">Map(position: $mapViewModel.cameraPosition, scope: mapScope)</code></pre></div><p>Direct binding to <em><strong>@Published var cameraPosition</strong></em>. <em><strong>onMapCameraChange</strong></em> only updated <em><strong>currentRegion</strong></em> for clustering <strong><span>not</span></strong> the camera position on <strong>every frame</strong>.<br>Location updates didn&#8217;t constantly reset <em><strong>cameraPosition</strong></em> to a fixed <em><strong>.region(...)</strong></em>.</p><h3>Fix - Pure TCA way (BindingReducer + $store.cameraPosition)</h3><p><strong><a href="https://pointfreeco.github.io/swift-composable-architecture/1.8.0/documentation/composablearchitecture/bindings/">TCA bindings</a></strong><span> exist partly to remove boilerplate like this:</span></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;6c6d2e63-2ce1-48c4-bb1b-2b1baf225966&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">case updateCameraPosition(MapCameraPosition)

case let .updateCameraPosition(position):
    state.cameraPosition = position
    return .none</code></pre></div><p>I replaced the hand-rolled &#129755; binding with first-class TCA bindings. We can eliminate boilerplate using <em><strong>BindableAction</strong></em> and <em><strong>BindingReducer</strong></em>.</p><p><strong><span>Reducer:</span></strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;4ff4de60-4714-474c-b7f1-6d6067e792d9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">enum Action: BindableAction, Equatable {
    case binding(BindingAction&lt;State&gt;)
    // ...
}

var body: some ReducerOf&lt;Self&gt; {
    BindingReducer()
    contentWithMapSheets
}</code></pre></div><p><strong>View:</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;391cd0d3-b589-41a3-8497-e78591f14742&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">@Bindable var store: StoreOf&lt;ContentFeature&gt;

Map(position: $store.cameraPosition, scope: mapScope) {
    // ...
}</code></pre></div><p>Now Map-driven updates go through <em><strong>$store.cameraPosition</strong></em> &#8594; <em><strong>BindingReducer()</strong></em> &#8594; <em><strong>state.cameraPosition</strong></em>. No custom <em><strong>updateCameraPosition</strong></em> action. No <em><strong>send</strong></em> on every frame through a closure we wrote by hand.</p><p>We also guarded location-driven camera resets:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;d33ae679-e5b0-47c9-a57f-982788d70f7e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">let shouldUpdateCamera = !state.avoidance.isEditingAvoidedLocations
    &amp;&amp; !state.cameraPosition.followsUserLocation
    &amp;&amp; (/* other conditions */)</code></pre></div><p>So GPS updates don&#8217;t pull the map out of heading mode.</p><p><strong>What stays in the reducer on purpose:</strong> programmatic moves like framing a route, opening a sheet or reapplying the camera after a sheet animation (<em><strong>forceCameraPosition</strong></em>). Those are business logic, not UI bindings. <em><strong>BindingReducer()</strong></em> doesn&#8217;t replace them.</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;e9aabaeb-3912-4771-87a4-7f2200dfdef8&quot;,&quot;duration&quot;:null}"></div><div><hr></div><p><strong>My takeaway:</strong> the bug was <strong><span>treating MapKit&#8217;s continuous camera stream like a simple two-way form field</span></strong>. Sliders and text fields want every write in the store. Heading-follow wants MapKit to drive until you programmatically take over.</p><p><strong><span>Sources:</span></strong></p><ul><li><p><strong><a href="https://pointfreeco.github.io/swift-composable-architecture/1.8.0/documentation/composablearchitecture/bindings/">TCA Bindings documentation</a></strong></p></li><li><p><strong><a href="https://developer.apple.com/documentation/mapkit/mapcameraposition">MapCameraPosition - followsUserLocation / followsUserHeading</a></strong></p></li></ul><p></p>]]></content:encoded></item><item><title><![CDATA[Adopting SwiftData for a Core Data app]]></title><description><![CDATA[Last week I decided to finally move my side project, EV Charge Stations Map, from Core Data to SwiftData. Every new Apple sample, every WWDC session, every blog post I bookmarked and never read&#8230; they all pointed to SwiftData. And I got tired of maintaining]]></description><link>https://emredegirmenci.substack.com/p/adopting-swiftdata-for-a-core-data</link><guid isPermaLink="false">https://emredegirmenci.substack.com/p/adopting-swiftdata-for-a-core-data</guid><dc:creator><![CDATA[Emre Degirmenci]]></dc:creator><pubDate>Sun, 21 Jun 2026 06:54:10 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!o1-v!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa732f8ef-f74d-48b2-86d0-e3c909ffcb21_1448x488.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Last week I decided to finally move my side project, <strong><a href="https://apple.co/3NWejUz"><span>EV Charge Stations Map</span></a></strong>, from <strong>Core Data</strong> to <strong>SwiftData</strong>. Every new Apple sample, every WWDC session, every blog post I bookmarked and never read&#8230; they all pointed to SwiftData. And I got tired of maintaining <em><strong>.xcdatamodeld</strong></em> plus generated <em><strong>+CoreDataClass</strong></em> files plus <em><strong>NSFetchRequest</strong></em> boilerplate for a pretty small data layer.</p><p>My app stores three things locally: favorited charging stations, their connector details, and saved route planner inputs. No CloudKit sync. No background contexts. Just one <em><strong>viewContext</strong></em> passed around SwiftUI views. I thought &#8220;this should be straightforward.&#8221;</p><p>It was. Mostly. But I also hit a launch crash, one schema mistake that almost made me panic about user data. So here is what actually happened.</p><h3>What I Had Before</h3><p>Classic Core Data setup. <em><strong>PersistenceController</strong></em> with <em><strong>NSPersistentContainer</strong></em>, entities in <em><strong>.xcdatamodeld</strong></em>, and a bunch of <em><strong>+CoreDataProperties.swift</strong></em> files.</p><p>The stack looked like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;e8b92452-4b37-4387-a445-062d0f5ae1bc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">import CoreData

final class PersistenceController: ObservableObject {
    let container = NSPersistentContainer(name: "EV Charge Stations Map")

    init() {
        container.loadPersistentStores { desc, error in
            if let error = error {
                Log.log("Core Data failed to load: \(error.localizedDescription)", level: .error)
            }
        }
    }
}</code></pre></div><p>In the app entry point I injected the context everywhere:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;d4fa11da-07f8-49ca-96e0-6352aba37455&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">.environment(\.managedObjectContext, dataController.container.viewContext)</code></pre></div><p>Views used<span data-color="rgba(228, 228, 228, 0.92)" style="color: rgba(228, 228, 228, 0.92);"> </span><em><strong>@FetchRequest</strong></em><span data-color="rgba(228, 228, 228, 0.92)" style="color: rgba(228, 228, 228, 0.92);"> </span>and<span data-color="rgba(228, 228, 228, 0.92)" style="color: rgba(228, 228, 228, 0.92);"> </span><em><strong>moc:</strong></em></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;8bd477b4-3980-4c33-8587-dae3d84277c6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">@Environment(\.managedObjectContext) var moc
@FetchRequest(sortDescriptors: []) var favorites: FetchedResults&lt;ChargingPoint&gt;</code></pre></div><p>And when someone favorited a charger from the map, the code was very Core Data &#128578;</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;e8983694-9df2-45c8-8b02-75d8f308b3c6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">let newFavorite = ChargingPoint(context: moc)
newFavorite.name = chargingPointPOILocation.title
newFavorite.latitude = chargingPointPOILocation.coordinate?.latitude ?? 0.0
// ...

let connection = ChargingPointConnection(context: moc)
connection.powerkW = chargingPointConnectionType.powerkW
newFavorite.addToChargingPointConnections(connection)

try moc.save()</code></pre></div><h3>How I Thought About the Migration</h3><p>I did not want in one PR. I treated it like rewiring a house room by room while people still live in it. Same SQLite file on disk. Same entity names. Same property names. Just swap the API layer on top.</p><h3>Step 1: Replace Entities With <code>@Model</code> Classes</h3><p>I started with the simplest entity, <em><strong>RoutePlannerSelectionPersistence</strong></em>. No relationships. Just saved route fields.</p><p>Core Data had a <em><strong>.xcdatamodel</strong></em> entry plus two generated Swift files. SwiftData wants one file:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;253782fd-baed-40d7-9a1b-6ec51ca7abc5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">import SwiftData

@Model
final class RoutePlannerSelectionPersistence {
    var initialSOC: Double
    var selectedTargetLocationLat: Double
    var selectedTargetLocationLon: Double
    var selectedTargetLocationName: String?
    var selectedTypecode: String?
    var selectedVehicleDetails: String?
    var yourLocationLat: Double
    var yourLocationLon: Double
    var yourLocationName: String?

    init(initialSOC: Double = 0.0, /* ... */) {
        self.initialSOC = initialSOC
        // ...
    }
}</code></pre></div><p>Then<span data-color="rgba(228, 228, 228, 0.92)" style="color: rgba(228, 228, 228, 0.92);"> </span><em><strong>ChargingPointConnection</strong></em><span data-color="rgba(228, 228, 228, 0.92)" style="color: rgba(228, 228, 228, 0.92);"> </span>(child), then<span data-color="rgba(228, 228, 228, 0.92)" style="color: rgba(228, 228, 228, 0.92);"> </span><em><strong>ChargingPoint</strong></em><span data-color="rgba(228, 228, 228, 0.92)" style="color: rgba(228, 228, 228, 0.92);"> </span>(parent with the relationship):</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;0e3d6490-a2e2-469b-bf57-dbed302e2ebc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">@Model
final class ChargingPoint {
    var name: String?
    var latitude: Double
    var longitude: Double
    // ...

    @Relationship(deleteRule: .nullify, inverse: \ChargingPointConnection.chargingPoint)
    var chargingPointConnections: [ChargingPointConnection] = []

    func addConnection(_ connection: ChargingPointConnection) {
        connection.chargingPoint = self
        chargingPointConnections.append(connection)
    }
}</code></pre></div><p>One thing I learned: put <em><strong>@Relationship</strong></em> on <strong><span>one side only</span></strong> (the parent). On the child just declare <em><strong>var chargingPoint: ChargingPoint?</strong></em> without the macro. Apple&#8217;s docs say this, I ignored it at first, then went back and fixed it &#128123;</p><p>I deleted all <em><strong>+CoreDataClass</strong></em> and <em><strong>+CoreDataProperties</strong></em> files and eventually removed <em><strong>.xcdatamodeld</strong></em> from the project entirely.</p><h3>Step 2: Swap the Stack</h3><p>Old:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;8e433a81-00d8-4538-b712-64ab74a2d1e9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">NSPersistentContainer(name: "EV Charge Stations Map")</code></pre></div><p>New:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;dd0512f5-a1ac-422a-a441-e0ce143be8f4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">static let schema = Schema([
    ChargingPoint.self,
    ChargingPointConnection.self,
    RoutePlannerSelectionPersistence.self,
])

static var storeURL: URL {
    let directory = FileManager.default.urls(
        for: .applicationSupportDirectory,
        in: .userDomainMask
    ).first!
    return directory.appendingPathComponent("EV Charge Stations Map.sqlite")
}

let configuration = ModelConfiguration(
    "EV Charge Stations Map",
    schema: schema,
    url: storeURL,
    cloudKitDatabase: .none
)

modelContainer = try ModelContainer(for: schema, configurations: configuration)</code></pre></div><p>That <em><strong>storeURL</strong></em> line is the whole game if you care about existing users. Point SwiftData at the <strong><span>same</span></strong> <em><strong>.sqlite</strong></em> file Core Data already used. Same folder (<em><strong>Application Support</strong></em>), same filename. If you get this wrong, the app launches fine but looks like everyone lost their data. They did not. You just opened an empty new drawer.</p><h3>Step 3: Wire the App</h3><p>Instead of injecting <em><strong>managedObjectContext</strong></em>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;2d62873f-2e71-413a-8c96-bd6e5e6d3af7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">.modelContainer(dataController.modelContainer)</code></pre></div><p>On the<span data-color="rgba(228, 228, 228, 0.92)" style="color: rgba(228, 228, 228, 0.92);"> </span><em><strong>WindowGroup</strong></em><span data-color="rgba(228, 228, 228, 0.92)" style="color: rgba(228, 228, 228, 0.92);"> </span>in<span data-color="rgba(228, 228, 228, 0.92)" style="color: rgba(228, 228, 228, 0.92);"> </span><em><strong>EVChargeStationsMapApp</strong></em>. That is it for the plumbing. Child views pick up <em><strong>@Environment(\.modelContext)</strong></em><span data-color="rgba(228, 228, 228, 0.92)" style="color: rgba(228, 228, 228, 0.92);"> </span>automatically.</p><h3>Step 4: Update Views One by One</h3><p>This part is mostly search and replace with small brain engaged.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!o1-v!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa732f8ef-f74d-48b2-86d0-e3c909ffcb21_1448x488.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!o1-v!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa732f8ef-f74d-48b2-86d0-e3c909ffcb21_1448x488.png 424w, https://substackcdn.com/image/fetch/$s_!o1-v!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa732f8ef-f74d-48b2-86d0-e3c909ffcb21_1448x488.png 848w, https://substackcdn.com/image/fetch/$s_!o1-v!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa732f8ef-f74d-48b2-86d0-e3c909ffcb21_1448x488.png 1272w, https://substackcdn.com/image/fetch/$s_!o1-v!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa732f8ef-f74d-48b2-86d0-e3c909ffcb21_1448x488.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!o1-v!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa732f8ef-f74d-48b2-86d0-e3c909ffcb21_1448x488.png" width="1448" height="488" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a732f8ef-f74d-48b2-86d0-e3c909ffcb21_1448x488.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:488,&quot;width&quot;:1448,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:153100,&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://emredegirmenci.substack.com/i/202765272?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa732f8ef-f74d-48b2-86d0-e3c909ffcb21_1448x488.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_!o1-v!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa732f8ef-f74d-48b2-86d0-e3c909ffcb21_1448x488.png 424w, https://substackcdn.com/image/fetch/$s_!o1-v!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa732f8ef-f74d-48b2-86d0-e3c909ffcb21_1448x488.png 848w, https://substackcdn.com/image/fetch/$s_!o1-v!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa732f8ef-f74d-48b2-86d0-e3c909ffcb21_1448x488.png 1272w, https://substackcdn.com/image/fetch/$s_!o1-v!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa732f8ef-f74d-48b2-86d0-e3c909ffcb21_1448x488.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 aria-hidden="true" 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><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><em><strong>SavedRouteListView</strong></em><span data-color="rgba(228, 228, 228, 0.92)" style="color: rgba(228, 228, 228, 0.92);"> </span>after migration:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;2ac48586-d365-4a44-a0a4-9c8215398674&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">@Environment(\.modelContext) private var modelContext
@Query(sort: \RoutePlannerSelectionPersistence.yourLocationName)
private var savedRoutes: [RoutePlannerSelectionPersistence]</code></pre></div><p><em><strong>ItemInfoView</strong></em> for favorites:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;9e05222d-ee52-4378-bf9f-509f8c7163b3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">@Environment(\.modelContext) private var modelContext
@Query(sort: \ChargingPoint.id) private var favorites: [ChargingPoint]

func toggleFavoriteForPOIData() {
    if let existingFavorite = favorites.first(where: { $0.matches(chargingPointPOILocation) }) {
        modelContext.delete(existingFavorite)
    } else {
        let newFavorite = ChargingPoint.favorite(from: chargingPointPOILocation)
        modelContext.insert(newFavorite)
    }
    try modelContext.save()
}</code></pre></div><p>I pulled the &#8220;create favorite from map data&#8221; logic into a helper on <em><strong>ChargingPoint</strong></em> so I was not copy pasting 20 lines in every view. Small thing, saved my sanity.</p><p>View models that cannot use <em><strong>@Query</strong></em> (because <em><strong>@Query</strong></em> only works inside SwiftUI views) still fetch manually:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;a29d5d04-2ecd-41c4-918f-24157fa8e83c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">favorites = try modelContext.fetch(ChargingPoint.favoritesFetchDescriptor())</code></pre></div><h3>The Crash That Almost Ruined My Evening</h3><p>First build after migration. App launches. Instant crash. &#128556;</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;528f21cd-ada7-4c20-83f1-f00d3da8f5ec&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">Fatal error: SwiftData container failed to load: SwiftDataError(...loadIssueModelContainer...)</code></pre></div><p>The cause was embarrassing. During migration I added a new property <em><strong>id: UUID</strong></em> on <em><strong>RoutePlannerSelectionPersistence</strong></em>. Felt like a good idea. Clean identity for lists, <em><strong>Identifiable</strong></em>, future proofing, all that.</p><p>But that column <strong><span>never existed</span></strong> in the old Core Data model. SwiftData tried to open the existing SQLite file, saw a schema it could not reconcile, and gave up.</p><p><strong>Fix:</strong> remove <em><strong>id</strong></em>. Match the legacy schema exactly. SwiftData already gives you <em><strong>persistentModelID</strong></em> for identity. You do not need to invent a new column on day one.</p><p>After that fix the app launched and <strong><span>all my old favorites and saved routes were still there</span></strong>.</p><h3>What I Would Tell Another iOS Dev</h3><ol><li><p><strong><span>Check your scope first.</span></strong> If you have CloudKit sync, complex migrations, heavy background contexts&#8230; budget more time. My app was small. That helped a lot. However, I&#8217;m planning to think about CloudKit sync as well later.</p></li><li><p><strong><span>Match the old schema on the first pass.</span></strong> Do not add properties &#8220;while you are here.&#8221;</p></li><li><p><strong><span>Keep the same </span></strong><em><strong>.sqlite</strong></em><strong><span> path.</span></strong> Seriously. &#128521;</p></li><li><p><strong><span>Migrate file by file.</span></strong> Models &#8594; stack &#8594; app entry &#8594; views. You can keep the project compiling between steps if you are disciplined.</p></li><li><p><strong><span>Delete </span></strong><em><strong>.xcdatamodeld</strong></em><strong><span> last.</span></strong> Not first.</p></li><li><p><strong><span>Test on a device with real old data.</span></strong> Simulator with a fresh install will not teach you anything about schema mismatches.</p></li><li><p><em><strong>@Query</strong></em><strong><span> is not for view models.</span></strong> Only views. I learned this the boring way.</p></li></ol><div><hr></div><p>Core Data was not hurting me. But <em><strong>SwiftUI</strong></em> + <em><strong>@Query</strong></em> + <em><strong>@Model</strong></em> feels more elegant now, and I do not miss old age <em><strong>NSFetchRequest</strong></em> factory extensions one bit.</p><p>If you are sitting on a similar small Core Data layer and thinking about SwiftData, you can do it incrementally. Same locker, new key. Just do not change the lock on the first try &#128272;</p><p>Sources:</p><ul><li><p><a href="https://developer.apple.com/documentation/CoreData/adopting-swiftdata-for-a-core-data-app">Adopting SwiftData for a Core Data app</a></p></li></ul><div id="youtube2-oIsjgo4Qb4A" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;oIsjgo4Qb4A&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/oIsjgo4Qb4A?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div>]]></content:encoded></item><item><title><![CDATA[Swift - NSPointerArray/WeakCollections]]></title><description><![CDATA[Have you ever encountered to store weak references in an array in other words using arrays with weak references?]]></description><link>https://emredegirmenci.substack.com/p/swift-nspointerarrayweakcollections</link><guid isPermaLink="false">https://emredegirmenci.substack.com/p/swift-nspointerarrayweakcollections</guid><dc:creator><![CDATA[Emre Degirmenci]]></dc:creator><pubDate>Sat, 13 Jun 2026 14:04:12 GMT</pubDate><enclosure url="https://images.unsplash.com/photo-1639548538099-6f7f9aec3b92?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMHx8Y2xhc3N8ZW58MHx8fHwxNzgxMjc2MTY0fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" 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://images.unsplash.com/photo-1639548538099-6f7f9aec3b92?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMHx8Y2xhc3N8ZW58MHx8fHwxNzgxMjc2MTY0fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://images.unsplash.com/photo-1639548538099-6f7f9aec3b92?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMHx8Y2xhc3N8ZW58MHx8fHwxNzgxMjc2MTY0fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1639548538099-6f7f9aec3b92?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMHx8Y2xhc3N8ZW58MHx8fHwxNzgxMjc2MTY0fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1639548538099-6f7f9aec3b92?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMHx8Y2xhc3N8ZW58MHx8fHwxNzgxMjc2MTY0fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1639548538099-6f7f9aec3b92?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMHx8Y2xhc3N8ZW58MHx8fHwxNzgxMjc2MTY0fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw"><img src="https://images.unsplash.com/photo-1639548538099-6f7f9aec3b92?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMHx8Y2xhc3N8ZW58MHx8fHwxNzgxMjc2MTY0fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" width="382" height="571.9659706109823" data-attrs="{&quot;src&quot;:&quot;https://images.unsplash.com/photo-1639548538099-6f7f9aec3b92?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMHx8Y2xhc3N8ZW58MHx8fHwxNzgxMjc2MTY0fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:3872,&quot;width&quot;:2586,&quot;resizeWidth&quot;:382,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;a dining room table&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpg&quot;,&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="a dining room table" title="a dining room table" srcset="https://images.unsplash.com/photo-1639548538099-6f7f9aec3b92?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMHx8Y2xhc3N8ZW58MHx8fHwxNzgxMjc2MTY0fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1639548538099-6f7f9aec3b92?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMHx8Y2xhc3N8ZW58MHx8fHwxNzgxMjc2MTY0fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1639548538099-6f7f9aec3b92?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMHx8Y2xhc3N8ZW58MHx8fHwxNzgxMjc2MTY0fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1639548538099-6f7f9aec3b92?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHwxMHx8Y2xhc3N8ZW58MHx8fHwxNzgxMjc2MTY0fDA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 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 aria-hidden="true" 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><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">Photo by <a href="https://unsplash.com/@bitu2104">Tuyen Vo</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure></div><p>Have you ever encountered to store weak references in an array in other words using arrays with weak references? I know that&#8217;s not a case for every array usage in Swift but that&#8217;s not an excuse not to know how to achieve it.</p><p>In Swift, standard arrays store <strong>strong references</strong> to the objects they contain. This means as long as an object is in the array, its reference count stays above zero, preventing it from being <strong>deallocated</strong> from memory.</p><h3>The Need for Weak References </h3><p>Sometimes, you want to keep track of a collection of objects without owning them. This is where you need <strong>weak references</strong>. If the only remaining reference to an object is in your collection, you want that object to be <strong>deallocated</strong> automatically to <strong>avoid</strong> <strong>memory leaks</strong>.</p><h3>Why weak references?</h3><p>For example, I have a <em><strong>HomeViewController </strong></em>and I wanna pop-up some custom views from there. I&#8217;m creating an array which points to the PopupView array strongly and injecting it in the initializer of HomeVC. Also, I have a ViewPresenter class to present my custom views and passing them to HomeVC. If we try to break the strong reference cycle in <em><strong>destroyViews()</strong></em> function it won&#8217;t happen since arrays are value types.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;feaf7745-ecb5-4abd-be2d-c7890af3ff1b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">import Foundation

class PopupView {}

class HomeViewController {
    private var views: [PopupView]? //Strong reference
    
    init(views: [PopupView]) {
        self.views = views
    }
    
    func poppAction() {
        print("Views popped up!")
    }
}

class ViewPresenter {
    private var popupA: PopupView? = PopupView()
    private var popupB: PopupView? = PopupView()
    private var homeVC: HomeViewController
    
    init() {
        self.homeVC = HomeViewController(views: [popupA!, popupB!])//injection of strong reference
    }
    
    func destroyViews () {
        popupA = nil
        popupB = nil //destroyViews doesn't destroy the two views 
        // because the array inside HomeViewController is maintaining a strong reference of the views.
    }
}</code></pre></div><p>We can avoid this problem replacing the array with <em><strong>NSPointerArray</strong></em>.</p><h3>What is NSPointerArray?</h3><p><em><strong>NSPointerArray</strong></em> is a Foundation class that acts like a specialized, mutable collection. Unlike a standard array, it can store <strong>weak references</strong> to objects.</p><p>It is specifically designed to store pointers. By configuring it with <em><strong>NSPointerArray.weakObjects</strong></em>, it doesn't increment the reference count of the objects you add to it.</p><p><a href="https://www.marcosantadev.com/swift-arrays-holding-elements-weak-references/">Using the following extension we can add convenience methods, allowing us to cast the </a><em><strong><a href="https://www.marcosantadev.com/swift-arrays-holding-elements-weak-references/">AnyObject</a></strong></em><a href="https://www.marcosantadev.com/swift-arrays-holding-elements-weak-references/"> pointers back to our specific class types easily. Using the pointer may be annoying, you can use this extension which made to simplify the </a><em><strong><a href="https://www.marcosantadev.com/swift-arrays-holding-elements-weak-references/">NSPointerArray</a></strong></em><a href="https://www.marcosantadev.com/swift-arrays-holding-elements-weak-references/">:</a></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;9b8467a3-9e6d-43d5-ae3f-e3c9ac1ac220&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">extension NSPointerArray {
    func addobject(_ object: AnyObject?) {
        guard let strongObject = object else { return }
        
        let pointer = Unmanaged.passUnretained(strongObject).toOpaque()
        addPointer(pointer)//: UnsafeMutableRawPointer?
    }
    
    func object(at index: Int) -&gt; AnyObject? {
        guard index &lt; count,
              let pointer = self.pointer(at: index) else { return nil }
        return Unmanaged&lt;AnyObject&gt;.fromOpaque(pointer).takeUnretainedValue()
    }
}</code></pre></div><p>We can get rid of <em><strong>Unmanaged.passUnreateined </strong></em>usage in the following code by using the above NSPointerArray extension!</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;1bd6dc04-7205-4517-bbcd-06d014206855&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">class MyView { }

var array = NSPointerArray.weakObjects()

let obj = MyView()
let pointer = Unmanaged.passUnretained(obj).toOpaque()
array.addPointer(pointer)</code></pre></div><p>We can replace the previous example with:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;36be3bbb-69a7-42e2-bc79-7994b7159cd6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">class MyView { }

var array = NSPointerArray.weakObjects()

let obj = MyView()
array.addObject(obj)</code></pre></div><p>Let&#8217;s fix the problem in the <strong>Why weak references?</strong> code example:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;0ac9fc2b-d975-40fd-ae45-cd2421c55c54&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import Foundation

class PopupView {}

class HomeViewController {
    private let views: NSPointerArray
    
    init(views: NSPointerArray) {
        self.views = views
    }
    
    func poppAction() {
        print("Views popped up!")
    }
}

class ViewPresenter {
    private var popupA: PopupView? = PopupView()
    private var popupB: PopupView? = PopupView()
    private var homeVC: HomeViewController
    
    init() {
        let pointerArray = NSPointerArray.weakObjects()
        pointerArray.addObject(popupA)
        pointerArray.addObject(popupB)
        self.homeVC = HomeViewController(views: pointerArray)
    }
    
    func destroyViews () {
        popupA = nil // it now destroys the views from memory since we made them weak
        popupB = nil
    }
}

ViewPresenter().destroyViews()</code></pre></div><p>It now destroys the views from memory since we made them weak in the initializer of the ViewPresenter class. The <em><strong>NSPointerArray</strong></em> stores pointers of <em><strong>AnyObject</strong></em> only, it means that we can only store just classes (not structs or enums) but we can store protocols if they are class-bound;</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;103dda74-94d9-4d1f-b2cf-a1efc51af7f1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">protocol SomeProtocol: AnyObject {}</code></pre></div><p>The biggest concern about NSPointerArray is <strong>type safety</strong>. The compiler is not able to infer the type of the objects inside <em><strong>NSPointerArray</strong></em>, since it uses pointers of objects <em><strong>AnyObject</strong></em>. For this reason, when we get an object from the array, we must cast it to a specific type:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;b2fe52aa-1dbd-4ebf-b419-97d94e2d758f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">if let objectAtIndexZero = array.object(at: 0) as? Class { // Cast to Class
    print("The object at index zero is MyClass")
}</code></pre></div><p>To solve this type-cast problem we need a type-safe alternative generic class.<br>Let&#8217;s make the previous example type-safe by generic WeakConverter wrapper class;</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;cf0ea9f5-1a2d-46e2-b4a7-854fbc37b4ad&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">import Foundation

typealias WeakConverterView = WeakConverter&lt;PopupView&gt;

final class WeakConverter&lt;A: AnyObject&gt; {

    // private(set) exposes convert as read-only, 
    // so callers cannot reassign the weak reference from outside the class.
    private(set) weak var convert: A? 
    
    init(_ value: A?) {
        convert = value
    }
}

class PopupView {}

class HomeViewController {
    private let views: [WeakConverterView]
    
    init(views: [WeakConverterView]) {
        self.views = views
    }
    
    func popupAction() {
        print("Views popped up!")
        
    }
}

class ViewPresenter {
    private var popupA: PopupView? = PopupView()
    private var popupB: PopupView? = PopupView()
    private var homeVC: HomeViewController
    
    init() {
        var array = [WeakConverterView]()
        array.append(WeakConverterView(popupA))
        array.append(WeakConverterView(popupB))
        self.homeVC = HomeViewController(views: array)
    }
    
    func destroyViews () {
        popupA = nil
        popupB = nil
    }
}

ViewPresenter().destroyViews()</code></pre></div><p><em><strong>A: AnyObjec</strong></em><code>t</code> restricts the generic to class instances, which is required because only reference types support <em><strong>weak</strong></em>. The wrapper gives the compiler a concrete type. For example <em><strong>[WeakConverter&lt;PopupView&gt;]</strong></em>, so we no longer need casting when we use the array.</p><p>When <em><strong>destroyViews()</strong></em> runs,<em> <strong>popupA</strong> </em>and<em> <strong>popupB</strong> </em>release their strong references. The <em><strong>PopupView</strong> </em>instances are deallocated, and each <em><strong>WeakConverterView.convert</strong> </em>becomes <em><strong>nil</strong> </em>without<em> <strong>HomeViewController</strong> </em>needing to remove entries manually.</p><div><hr></div><p>If you want to see all of the code above in an editor you can watch my youtube video here;</p><div id="youtube2-m_Zq9e_kNPI" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;m_Zq9e_kNPI&quot;,&quot;startTime&quot;:&quot;105s&quot;,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/m_Zq9e_kNPI?start=105s&amp;rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><p>Sources:<br>- <a href="https://www.marcosantadev.com/swift-arrays-holding-elements-weak-references/">Swift Arrays Holding Elements With Weak References</a></p><p>- <a href="https://developer.apple.com/documentation/foundation/nspointerarray">NSPointerArray</a></p>]]></content:encoded></item><item><title><![CDATA[CloudKit Mystery in iOS]]></title><description><![CDATA[Why My App Showed Different Heatmap Data on Xcode and on the App Store?]]></description><link>https://emredegirmenci.substack.com/p/cloudkit-mystery-in-ios</link><guid isPermaLink="false">https://emredegirmenci.substack.com/p/cloudkit-mystery-in-ios</guid><dc:creator><![CDATA[Emre Degirmenci]]></dc:creator><pubDate>Sat, 23 May 2026 15:49:22 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!OSlf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb610be88-b3d2-46ee-a414-efa51f9ff8f8_1408x2770.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I want to share a confusing CloudKit story that took me a full day of debugging to fully understand. Maybe it helps you if you also use CloudKit in your iOS app.</p><h3>What I Was Trying To Do</h3><p>My app, <a href="https://apple.co/4mz7vev">Walk Mate</a>, has a <strong>Heatmap</strong> screen with two filters: <em>&#8220;You&#8221;</em> and<em> &#8220;Others&#8221;</em>. The <em>&#8220;You&#8221;</em> filter shows places where the current user walked. The <em>&#8220;Others&#8221;</em> filter shows anonymous walks from all other users around the world. The data goes through CloudKit Public Database so every user can see everyone else&#8217;s walk place in a circular region. (i.e. over Taipei City and Kaohsiung City)</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!OSlf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb610be88-b3d2-46ee-a414-efa51f9ff8f8_1408x2770.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!OSlf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb610be88-b3d2-46ee-a414-efa51f9ff8f8_1408x2770.png 424w, https://substackcdn.com/image/fetch/$s_!OSlf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb610be88-b3d2-46ee-a414-efa51f9ff8f8_1408x2770.png 848w, https://substackcdn.com/image/fetch/$s_!OSlf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb610be88-b3d2-46ee-a414-efa51f9ff8f8_1408x2770.png 1272w, https://substackcdn.com/image/fetch/$s_!OSlf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb610be88-b3d2-46ee-a414-efa51f9ff8f8_1408x2770.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!OSlf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb610be88-b3d2-46ee-a414-efa51f9ff8f8_1408x2770.png" width="266" height="523.3096590909091" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b610be88-b3d2-46ee-a414-efa51f9ff8f8_1408x2770.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:2770,&quot;width&quot;:1408,&quot;resizeWidth&quot;:266,&quot;bytes&quot;:3393991,&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://emredegirmenci.substack.com/i/198973741?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb610be88-b3d2-46ee-a414-efa51f9ff8f8_1408x2770.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_!OSlf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb610be88-b3d2-46ee-a414-efa51f9ff8f8_1408x2770.png 424w, https://substackcdn.com/image/fetch/$s_!OSlf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb610be88-b3d2-46ee-a414-efa51f9ff8f8_1408x2770.png 848w, https://substackcdn.com/image/fetch/$s_!OSlf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb610be88-b3d2-46ee-a414-efa51f9ff8f8_1408x2770.png 1272w, https://substackcdn.com/image/fetch/$s_!OSlf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb610be88-b3d2-46ee-a414-efa51f9ff8f8_1408x2770.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 aria-hidden="true" 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><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>I shipped version 3.4.2 on App Store last week. Real users from Turkey, USA, Taiwan, Sweden, Japan and Canada started using the app. I could see their accounts in App Store Connect Analytics. But when I opened my Heatmap <em>&#8220;Others&#8221;</em> tab in the simulator from Xcode, I saw nothing from those countries. Only my own old test walks from Izmir, Turkey.</p><p>This was the start of the confusion. &#129327;</p><h3>First Mistake: I Trusted The Dashboard Without Checking The Environment</h3><p>I opened CloudKit Dashboard. I picked Production. I picked Public Database. I picked the record type HeatmapContribution. I saw a lot of records. Some had latitude around 25 (Taiwan), some around 35 (Japan), and so on. So I thought <em>&#8220;the data is there, the bug must be in my app.&#8221;</em></p><p>I asked an AI agent to help me debug. We added log lines to the fetch function. We ran the app from Xcode. The logs said the fetch returned 134 records. All of them were near Izmir. None from Taiwan or Japan.</p><p>This made no sense to me at first. The dashboard clearly showed Taiwan records. The app clearly did not return them. We tried many things. We checked sort orders. We checked permissions. We checked if records were lost during parsing. Nothing explained it.</p><h3>The Real Cause: Two CloudKit Worlds</h3><p>Then we tried one direct test. We picked one record name (9A85DFF-089E-4435-90E5-XXXXX) from the dashboard for a Taiwan record. We told the app to fetch that exact record by its name. The app got back <em>&#8220;Record not found&#8221;</em>.</p><p>That was the moment everything clicked.</p><p>CloudKit has two separate databases for every container. One is called <strong>Development</strong>. One is called <strong>Production</strong>. They look the same in code. They have the same name, the same record types, and the same fields. But they hold different data and they live on different servers.</p><p>When you build and run your app from Xcode with your normal Apple developer signing, your app talks to <strong>Development</strong>. When Apple builds your app for <strong>App Store</strong> or <strong>TestFlight</strong> with distribution signing, your app talks to <strong>Production</strong>. This switch happens because of the signing profile, not because of your build configuration. So even if I changed <strong>Xcode scheme</strong> to <strong>Release</strong>, my Xcode build still talked to Development as long as I used my normal developer certificate.</p><p>So my picture was like this:</p><ul><li><p>My old test walks from past development runs went into Development.</p></li><li><p>Real App Store users on 3.4.2 wrote their walks into Production.</p></li><li><p>The CloudKit Dashboard tab said &#8220;Production&#8221; at the top and showed me Production data.</p></li><li><p>My Xcode debug build read only Development data.</p></li></ul><p>Two different worlds. Looking at one in the dashboard, and reading the other in the app. They never met.</p><h3>A Second Bug Hiding Behind The Confusion</h3><p>While we were fixing the environment confusion, we found another real problem. The <em>&#8220;Others&#8221;</em> fetch in 3.4.2 was failing silently for every real user. Why? Because the Production schema needed a <strong>Queryable</strong> index on the system field recordName, and a <strong>Queryable</strong> + <strong>Sortable</strong> index on the timestamp field. These indexes were not deployed in Production. Without them, every <em>&#8220;Others&#8221;</em> query returned the error <em>&#8220;Field &#8216;recordName&#8217; is not marked queryable&#8221;.</em></p><p>The good news is that CloudKit indexes apply right away once you deploy them. So I went into the dashboard, added the indexes in <strong>Development</strong>, then clicked <em>&#8220;Deploy Schema Changes to Production&#8221;.</em> After that, my live App Store users could finally see global heatmap data without me shipping a new version.</p><p>I tested this by deleting Walk Mate from my iPhone, reinstalling 3.4.2 from the App Store, and opening the Heatmap. I saw my own Izmir walks under <strong>&#8220;You&#8221;</strong> and global walks by others under <strong>&#8220;Others&#8221;</strong>. It worked. &#128640;</p><h3>The Confusion Came Back</h3><p>Then I built and ran the same code from Xcode in Debug mode. The others walks were gone again. Only my local one under both <em>&#8220;You&#8221;</em> and <em>&#8220;Others&#8221;</em>. I thought I broke something. &#128558;&#8205;&#128168;</p><p>I did not break anything. The Xcode debug build was reading <strong>Development</strong> again. <strong>Development</strong> still has no Taiwan or others records because no real user ever writes to Development. Only my past tests wrote to <strong>Development</strong>. So the Heatmap <em>&#8220;Others&#8221;</em> looks empty in Xcode debug runs and that is the correct expected behavior.</p><h3>How To Verify Production Data From Xcode If You Want</h3><p>There are two ways:</p><ol><li><p>Add this temporary key in your entitlements file:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;ruby&quot;,&quot;nodeId&quot;:&quot;a3cb45e6-f0af-4f7e-a21a-da68e8cef0f7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-ruby">&lt;key&gt;com.apple.developer.icloud-container-environment&lt;/key&gt;
&lt;string&gt;Production&lt;/string&gt;</code></pre></div></li></ol><p>This forces Xcode builds to talk to <strong>Production</strong>. Use it only for testing and remove it after. Apple&#8217;s archive flow does not need this key. It picks <strong>Production</strong> by <strong>itself</strong> when it signs with a distribution profile.</p><ol start="2"><li><p>Distribute the build through <strong>TestFlight Internal Testing</strong>. TestFlight builds use <strong>Production</strong> by default. So if you install your TestFlight build on a simulator or a device, you will see real user data.</p></li></ol><h3>What I Learned</h3><p>A few simple things I now keep in mind:</p><ul><li><p><strong>CloudKit Development</strong> and <strong>CloudKit Production</strong> are not the same database. They look the same in code. They are not the same at runtime.</p></li><li><p>The <strong>CloudKit Dashboard</strong> tab you are looking at controls only the dashboard view. Your app picks its own database based on signing.</p></li><li><p>Schema changes and index changes only apply to one environment. You must click <em>&#8220;Deploy Schema Changes&#8221;</em> to push them from <strong>Development</strong> to <strong>Production</strong>.</p></li><li><p><strong>Indexes</strong> are <strong>not</strong> <strong>auto created</strong> in <strong>Production</strong> by save. Records can be saved without indexes but cannot be queried. So <strong>users</strong> will <strong>quietly upload data</strong> that <strong>nobody can ever read</strong> until you <strong>deploy the indexes.</strong></p></li><li><p>A simple query log that prints the count of records returned would have saved me hours. I added that log on day one for next time.</p></li><li><p>A direct fetch by record name is the fastest way to confirm <em>&#8220;is this record actually in the database my app talks to&#8221;.</em> If you get back <em>&#8220;Record not found&#8221;</em> for a record you can clearly see in the dashboard, you are not in the same database.</p></li></ul><p>The bug felt huge while I was inside it. The fix in code was tiny. The fix in dashboard was just a few clicks. The real lesson was about understanding which <strong>CloudKit</strong> world I was talking to at any moment. &#129300;</p>]]></content:encoded></item><item><title><![CDATA[What's the difference between frame and bounds?]]></title><description><![CDATA[Frame and Bounds in UIKit]]></description><link>https://emredegirmenci.substack.com/p/whats-the-difference-between-frame</link><guid isPermaLink="false">https://emredegirmenci.substack.com/p/whats-the-difference-between-frame</guid><dc:creator><![CDATA[Emre Degirmenci]]></dc:creator><pubDate>Sun, 17 May 2026 07:01:34 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!rakk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa646abf1-9471-4ef0-a6c8-25e2df6834b9_660x758.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Another most asked question in iOS interviews is <strong>&#8220;</strong><em><strong>Tell me the difference between frame and bounds&#8221;</strong></em> with a follow-up <strong>&#8220;</strong><em><strong>What happens when you rotate a view</strong></em><strong>?&#8221;.</strong> Imagine, you are applying a transformation to a view, what happens in terms of frame and bounds. Are they gonna be change or stay in same values? </p><p>Short answer is;</p><p><strong>Frame =</strong> a view&#8217;s <strong>location</strong> and size using the <strong>parent view&#8217;s coordinate system</strong>. Placing the view on the parent.</p><p><strong>Bounds =</strong> a view&#8217;s <strong>location</strong> and size using its <strong>own coordinate system</strong>. Placing the view&#8217;s content or subview within itself.</p><p>If you think about the view frame in its parent coordinate system when you are rotating, you are changing parent view&#8217;s coordinate system. <strong>Bounds</strong> value will be <strong>same</strong> and <strong>frame</strong> value <strong>changes</strong>. Figure 1.0 shows how it looks like before transformation (rotating) and look carefully to the values.<strong> </strong></p><p>The values in <em><strong>CGRect(x: 5, y: 5, width: 30, height: 40)</strong></em> define the rectangle's position and size:</p><ul><li><p><strong>x (5):</strong> The horizontal distance from the left edge of the parent view to the rectangle&#8217;s origin.</p></li><li><p><strong>y (5):</strong> The vertical distance from the top edge of the parent view to the rectangle&#8217;s origin.</p></li><li><p><strong>width (30)</strong>: The horizontal span of the rectangle.</p></li><li><p><strong>height (40)</strong>: The vertical span of the rectangle.</p></li></ul><p>These values specifically define the <strong>frame</strong> of the view, representing its location and size within its superview (parent) coordinate system.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!rakk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa646abf1-9471-4ef0-a6c8-25e2df6834b9_660x758.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!rakk!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa646abf1-9471-4ef0-a6c8-25e2df6834b9_660x758.png 424w, https://substackcdn.com/image/fetch/$s_!rakk!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa646abf1-9471-4ef0-a6c8-25e2df6834b9_660x758.png 848w, https://substackcdn.com/image/fetch/$s_!rakk!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa646abf1-9471-4ef0-a6c8-25e2df6834b9_660x758.png 1272w, https://substackcdn.com/image/fetch/$s_!rakk!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa646abf1-9471-4ef0-a6c8-25e2df6834b9_660x758.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!rakk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa646abf1-9471-4ef0-a6c8-25e2df6834b9_660x758.png" width="304" height="349.1393939393939" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a646abf1-9471-4ef0-a6c8-25e2df6834b9_660x758.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:758,&quot;width&quot;:660,&quot;resizeWidth&quot;:304,&quot;bytes&quot;:39501,&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://emredegirmenci.substack.com/i/197974405?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa646abf1-9471-4ef0-a6c8-25e2df6834b9_660x758.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_!rakk!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa646abf1-9471-4ef0-a6c8-25e2df6834b9_660x758.png 424w, https://substackcdn.com/image/fetch/$s_!rakk!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa646abf1-9471-4ef0-a6c8-25e2df6834b9_660x758.png 848w, https://substackcdn.com/image/fetch/$s_!rakk!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa646abf1-9471-4ef0-a6c8-25e2df6834b9_660x758.png 1272w, https://substackcdn.com/image/fetch/$s_!rakk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa646abf1-9471-4ef0-a6c8-25e2df6834b9_660x758.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 aria-hidden="true" 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><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">Figure 1.0: UIView coordinate systems (with example values)</figcaption></figure></div><h4><em><strong>View:</strong></em></h4><p>frame = { 5, 5, 30, 40 }<br>bounds = { 0, 0, 30, 40 }<br>center = { 20, 25 }</p><p>When the view rotated:</p><ul><li><p><strong>Frame (changed):</strong> Because the view is now at an angle, it occupies more &#8220;horizontal and vertical space&#8221; relative to the <strong>parent&#8217;s X</strong> and <strong>Y</strong> axes.</p></li><li><p><strong>Bounds (unchanged):</strong> The view&#8217;s internal size remains (30 x 40) because the object itself hasn&#8217;t grown.</p></li></ul><p>The dashed line in Figure 1.1 shows rotation and parent view&#8217;s location. Its <strong>width (52)</strong> and <strong>height (54)</strong> are calculated based on the rotated corners of the original (30 x 40) rectangle. Its <strong>corners</strong> push further out along the <strong>X</strong> and <strong>Y</strong> axes. To keep the view contained the parent system must calculate a new bounding box (the dashed line in Figure1.1) that reaches these new outermost points. The distance between its <strong>leftmost corner</strong> and <strong>rightmost corner</strong> is now <strong>52</strong>, and the distance between its <strong>topmost</strong> and <strong>bottommost</strong> corner is <strong>54</strong>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!B5MX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12b37c1f-a269-4faf-b7c1-df45a0806283_744x816.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!B5MX!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12b37c1f-a269-4faf-b7c1-df45a0806283_744x816.png 424w, https://substackcdn.com/image/fetch/$s_!B5MX!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12b37c1f-a269-4faf-b7c1-df45a0806283_744x816.png 848w, https://substackcdn.com/image/fetch/$s_!B5MX!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12b37c1f-a269-4faf-b7c1-df45a0806283_744x816.png 1272w, https://substackcdn.com/image/fetch/$s_!B5MX!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12b37c1f-a269-4faf-b7c1-df45a0806283_744x816.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!B5MX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12b37c1f-a269-4faf-b7c1-df45a0806283_744x816.png" width="310" height="340" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/12b37c1f-a269-4faf-b7c1-df45a0806283_744x816.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:816,&quot;width&quot;:744,&quot;resizeWidth&quot;:310,&quot;bytes&quot;:76367,&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://emredegirmenci.substack.com/i/197974405?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12b37c1f-a269-4faf-b7c1-df45a0806283_744x816.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_!B5MX!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12b37c1f-a269-4faf-b7c1-df45a0806283_744x816.png 424w, https://substackcdn.com/image/fetch/$s_!B5MX!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12b37c1f-a269-4faf-b7c1-df45a0806283_744x816.png 848w, https://substackcdn.com/image/fetch/$s_!B5MX!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12b37c1f-a269-4faf-b7c1-df45a0806283_744x816.png 1272w, https://substackcdn.com/image/fetch/$s_!B5MX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12b37c1f-a269-4faf-b7c1-df45a0806283_744x816.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 aria-hidden="true" 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><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">Figure 1.1: The effect rotating a view has on its frame property</figcaption></figure></div><h4><em><strong>View:</strong></em></h4><p>frame = { -6, -2, 52, 54 } // changed<br>bounds = { 0, 0, 30, 40 } // unchanged<br>center = { 20, 25 } // unchanged</p>]]></content:encoded></item><item><title><![CDATA[Synchronizing the access to the common resource]]></title><description><![CDATA[DispatchBarrier vs Actor]]></description><link>https://emredegirmenci.substack.com/p/synchronizing-the-access-to-the-common</link><guid isPermaLink="false">https://emredegirmenci.substack.com/p/synchronizing-the-access-to-the-common</guid><dc:creator><![CDATA[Emre Degirmenci]]></dc:creator><pubDate>Thu, 14 May 2026 20:44:34 GMT</pubDate><enclosure url="https://images.unsplash.com/photo-1588362951121-3ee319b018b2?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw0fHxiYXJyaWVyfGVufDB8fHx8MTc3ODc5MTkyNHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" 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://images.unsplash.com/photo-1588362951121-3ee319b018b2?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw0fHxiYXJyaWVyfGVufDB8fHx8MTc3ODc5MTkyNHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://images.unsplash.com/photo-1588362951121-3ee319b018b2?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw0fHxiYXJyaWVyfGVufDB8fHx8MTc3ODc5MTkyNHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1588362951121-3ee319b018b2?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw0fHxiYXJyaWVyfGVufDB8fHx8MTc3ODc5MTkyNHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1588362951121-3ee319b018b2?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw0fHxiYXJyaWVyfGVufDB8fHx8MTc3ODc5MTkyNHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1588362951121-3ee319b018b2?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw0fHxiYXJyaWVyfGVufDB8fHx8MTc3ODc5MTkyNHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1456w" sizes="100vw"><img src="https://images.unsplash.com/photo-1588362951121-3ee319b018b2?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw0fHxiYXJyaWVyfGVufDB8fHx8MTc3ODc5MTkyNHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080" width="500" height="333.3333333333333" data-attrs="{&quot;src&quot;:&quot;https://images.unsplash.com/photo-1588362951121-3ee319b018b2?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw0fHxiYXJyaWVyfGVufDB8fHx8MTc3ODc5MTkyNHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:4000,&quot;width&quot;:6000,&quot;resizeWidth&quot;:500,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;white and red sedan on road during daytime&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpg&quot;,&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="white and red sedan on road during daytime" title="white and red sedan on road during daytime" srcset="https://images.unsplash.com/photo-1588362951121-3ee319b018b2?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw0fHxiYXJyaWVyfGVufDB8fHx8MTc3ODc5MTkyNHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 424w, https://images.unsplash.com/photo-1588362951121-3ee319b018b2?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw0fHxiYXJyaWVyfGVufDB8fHx8MTc3ODc5MTkyNHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 848w, https://images.unsplash.com/photo-1588362951121-3ee319b018b2?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw0fHxiYXJyaWVyfGVufDB8fHx8MTc3ODc5MTkyNHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 1272w, https://images.unsplash.com/photo-1588362951121-3ee319b018b2?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wzMDAzMzh8MHwxfHNlYXJjaHw0fHxiYXJyaWVyfGVufDB8fHx8MTc3ODc5MTkyNHww&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=1080 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 aria-hidden="true" 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><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">Photo by <a href="https://unsplash.com/@maurosbicego">Mauro Sbicego</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure></div><p>I&#8217;m back with another concurrency question asked in one of the interviews like:</p><p>&#8220;When you have a Singleton (shared resource) in a multi-threaded environment, multiple queues can access to Singleton, the same shared state. You need to use a mechanism for accessing the common resource synchronously. What else do you remember for synchronizing the access to the common resource other than <strong>NSLock</strong> and <strong>DispatchSemaphore</strong>?&#8221;<br><br>I answered with <strong>DispatchBarrier</strong> by trying my best to explain. Let&#8217;s deep dive into the <strong>DispatchBarrier </strong>and then the modern way!</p><h3>DispatchBarrier</h3><p>It tells the compiler; &#8220;Wait. Let&#8217;s finish all the work that has already started. Then, just ME will work. Once I&#8217;m finished, the other tasks can continue.&#8221; In order to prevent data race, run tasks in an ordered way. Don&#8217;t run read and write operations at the same time! Once at a time!<br><br>Let&#8217;s explain it with a real life scenario;<br><br>Concurrent queue = multi-lane highway &#128739;&#65039;</p><ul><li><p>Read operations = normal cars &#128663;</p></li><li><p>Write operations = road maintenance car &#128679; </p></li></ul><p>When the Dispatch Barrier arrives;</p><ol><li><p>All cars pass first,</p></li><li><p>Road is closed completely,</p></li><li><p>Road maintenance car works alone,</p></li><li><p>Road opens again once the maintenance completed.</p></li></ol><p>Let&#8217;s jump into the code example:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;ef3d4f3f-fc8b-4c73-a3d7-1e90d3d7596d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">final class Highway {

    private let roadQueue = DispatchQueue( // 1
        label: "highway.queue",
        attributes: .concurrent
    )

    // Current road status
    private var roadStatus = "Road is Open"

    // Normal cars
    func carPass(carName: String) {

        roadQueue.async { // 2
            print("&#128663; \(carName) is passing...")
            sleep(2)
            print("&#9989; \(carName) passed")
        }
    }

    // Road maintenance car is coming
    func roadMaintenance() {

        roadQueue.async(flags: .barrier) { // 3
            print("&#128721; Road is closing...")
            print("&#128736;&#65039; Maintenance car is working...")
            
            sleep(3)

            self.roadStatus = "Maintenance Completed!"

            print("&#9989; Maintenance is done")
            print("&#128994; Road opens again")

            // new cars can pass concurrently again
        }
    }
}</code></pre></div><p>Let&#8217;s dive into comment number lines:</p><ol><li><p>Create a concurrent queue</p></li><li><p>async + concurrent queue</p></li><li><p>Use barrier. At this point queue behaves as serial temporarily.</p></li></ol><p>Usage:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;51c36ae1-e2ef-4248-b411-269907be7ce0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">let highway = Highway()

highway.carPass(carName: "Car 1")
highway.carPass(carName: "Car 2")
highway.carPass(carName: "Car 3")

highway.roadMaintenance()

highway.carPass(carName: "Car 4")
highway.carPass(carName: "Car 5")</code></pre></div><p>The workflow would roughly be like:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;1e6aeeff-7ea1-47e2-a1f2-9771d30421ad&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">&#128663; Car 1 is passing...
&#128663; Car 2 is passing...
&#128663; Car 3 is passing...

&#9989; Car 1 passed
&#9989; Car 2 passed
&#9989; Car 3 passed

&#128721; Road is closing...
&#128736;&#65039; Maintenance car is working...

&#9989; Maintenance is done
&#128994; Road opens again

&#128663; Car 4 is passing...
&#128663; Car 5 is passing...</code></pre></div><p>The critical points are here:</p><ul><li><p>The first 3 car can go at the same time (<em><strong>concurrent</strong></em>)</p></li><li><p>Everybody waits when the maintenance car arrive (<em><strong>barrier</strong></em>)</p></li><li><p>Road opens again when the maintenance completed.</p></li></ul><h3>Modern Way Actor</h3><p>In a modern Swift Concurrency way, actors allow us to <strong>protect shared mutable state</strong> from <strong>data races</strong>. It means that an <strong>actor</strong> can help us <strong>fix a data race</strong> in a same way we had before by serializing access to mutable state. This is what we did manually with the <strong>DispatchBarrier </strong>earlier except actors are a lot smarter than the naive <strong>.barrier</strong> approach we took earlier.</p><p>Let&#8217;s update the code example with the modern and safer way:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;965b7ebe-61f5-4784-ba99-659ed38b8af8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">// 1
actor Highway {

    // Current road status
    private var roadStatus = "Road is Open" // 2

    // Normal cars
    func carPass(carName: String) async {

        print("&#128663; \(carName) is passing...")
        try? await Task.sleep(for: .seconds(2)) // 3
        print("&#9989; \(carName) passed")
    }

    // Road maintenance car is coming
    func roadMaintenance() async {

        print("&#128721; Road is closing...")
        print("&#128736;&#65039; Maintenance car is working...")

        try? await Task.sleep(for: .seconds(3))

        roadStatus = "Maintenance Completed!"

        print("&#9989; Maintenance is done")
        print("&#128994; Road opens again")
    }

    func currentStatus() -&gt; String {
        roadStatus
    }
}</code></pre></div><p>Let&#8217;s dive into comment number lines:</p><ol><li><p>I will queue access to the mutable state within this object.</p><ul><li><p>No need .barrier definition</p></li><li><p>No queue creation</p></li><li><p>No synchronization code</p></li></ul></li><li><p>actor protects state of <em><strong>private var roadStatus</strong></em></p><ul><li><p>Two tasks cannot change state simultaneously</p></li><li><p>One cannot interrupt while the other is changing state</p></li><li><p>No data race</p></li></ul></li></ol><h4>Similarity with DispatchBarrier</h4><p>Mentally that&#8217;s what actor makes under the hood: <em><strong>concurrent queue + barrier. </strong></em>But the main difference is: in the <strong>.barrier </strong>approach you write synchronization manually, in the <strong>actor, </strong>Swift runtime manages synchronization automatically.</p><div><hr></div><h3>Conclusion</h3><p>In the <strong>Dispatch Barrier</strong> version of our Highway example, every car that wanted to access the road had to wait for the barrier operation to finish before continuing. During this waiting period, the underlying thread was <strong>blocked</strong>. In other words, the thread could not do any other useful work until the road maintenance operation completed.</p><p>This is one of the biggest differences between traditional GCD synchronization techniques and Swift Concurrency.</p><p>With <strong>Dispatch Barrier</strong>, we manually tell the queue: <strong>&#8220;Stop all traffic temporarily and let this critical operation run alone.&#8221; </strong>While this approach is safe and effective, waiting tasks often end up blocking threads.</p><p><strong>Actors</strong> take a different approach. By converting our Highway type from a <strong>class</strong> into an <strong>actor</strong>, Swift automatically <strong>serializes</strong> access to <strong>mutable state</strong> for us. Instead of blocking threads while waiting for access, Swift Concurrency <strong>suspends</strong> tasks.</p><p>So mentally, you can think of the difference like this:</p><p><strong>Dispatch Barrier</strong></p><blockquote><p>&#8220;Block the road until maintenance is done.&#8221;</p></blockquote><p><strong>Actor</strong></p><blockquote><p>&#8220;Cars waiting for maintenance don&#8217;t block the road itself. They pause and resume later.&#8221;</p></blockquote><p>This is one of the core ideas behind Swift Concurrency: <strong>Suspend tasks, not threads.</strong></p><p></p><p><strong>Sources: </strong></p><ul><li><p><a href="https://developer.apple.com/documentation/swift/managing-a-shared-resource-using-a-singleton">Apple Developer - Managing a Shared Resource Using a Singleton</a></p></li></ul><ul><li><p><a href="https://stackoverflow.com/questions/49160125/thread-safe-singleton-in-swift">Thread safe singleton in swift - Stack Overflow</a></p></li><li><p><a href="https://stackoverflow.com/a/76942501/4442254">Stack Overflow Legend Rob</a> &#129321;</p></li><li><p><a href="https://www.donnywals.com/books/">Practical Swift Concurrency - Donny Wals</a></p></li></ul>]]></content:encoded></item><item><title><![CDATA[Modern Semaphore]]></title><description><![CDATA[How to end an iOS Live Activity on app termination?]]></description><link>https://emredegirmenci.substack.com/p/modern-semaphore</link><guid isPermaLink="false">https://emredegirmenci.substack.com/p/modern-semaphore</guid><dc:creator><![CDATA[Emre Degirmenci]]></dc:creator><pubDate>Mon, 11 May 2026 09:52:37 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!FczD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F585efeab-06f3-49a9-8ad9-eed8bd7d3a7c_931x1094.jpeg" 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_!FczD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F585efeab-06f3-49a9-8ad9-eed8bd7d3a7c_931x1094.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!FczD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F585efeab-06f3-49a9-8ad9-eed8bd7d3a7c_931x1094.jpeg 424w, https://substackcdn.com/image/fetch/$s_!FczD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F585efeab-06f3-49a9-8ad9-eed8bd7d3a7c_931x1094.jpeg 848w, https://substackcdn.com/image/fetch/$s_!FczD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F585efeab-06f3-49a9-8ad9-eed8bd7d3a7c_931x1094.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!FczD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F585efeab-06f3-49a9-8ad9-eed8bd7d3a7c_931x1094.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!FczD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F585efeab-06f3-49a9-8ad9-eed8bd7d3a7c_931x1094.jpeg" width="384" height="451.2309344790548" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/585efeab-06f3-49a9-8ad9-eed8bd7d3a7c_931x1094.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:1094,&quot;width&quot;:931,&quot;resizeWidth&quot;:384,&quot;bytes&quot;:181362,&quot;alt&quot;:&quot;black traffic light on green light&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-normal" alt="black traffic light on green light" title="black traffic light on green light" srcset="https://substackcdn.com/image/fetch/$s_!FczD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F585efeab-06f3-49a9-8ad9-eed8bd7d3a7c_931x1094.jpeg 424w, https://substackcdn.com/image/fetch/$s_!FczD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F585efeab-06f3-49a9-8ad9-eed8bd7d3a7c_931x1094.jpeg 848w, https://substackcdn.com/image/fetch/$s_!FczD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F585efeab-06f3-49a9-8ad9-eed8bd7d3a7c_931x1094.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!FczD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F585efeab-06f3-49a9-8ad9-eed8bd7d3a7c_931x1094.jpeg 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 aria-hidden="true" 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><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">Photo by <a href="https://unsplash.com/@rodrigocuri">Rodrigo Curi</a> on <a href="https://unsplash.com">Unsplash</a></figcaption></figure></div><p>Most recently, when I was trying to end Live Activities completely and immediately and remove from both Dynamic Island and lock screen when the app terminated, I had a nondeterministic behavior. That Live Activity was sometimes removed sometimes not. After conducting some research, I discovered the solution using both modern Swift Concurrency and old Semaphore like below.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;7ecbd1b1-d55a-41d6-9993-6f47508ea9cb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">final class AppDelegate: NSObject, UIApplicationDelegate {
    
    func applicationWillTerminate(_ application: UIApplication) {
        let semaphore = DispatchSemaphore(value: 0)
        
        // Intentional: on termination we have a tiny window to end Live Activities.
        // A detached task avoids main-actor inheritance while we do a short bounded wait.
        Task.detached(priority: .high) {
            for activity in Activity&lt;WalkActivityAttributes&gt;.activities {
                let finalContent = ActivityContent(state: activity.content.state, staleDate: nil)
                await activity.end(finalContent, dismissalPolicy: .immediate)
            }
            // the semaphore&#8217;s .signal method is called to increment the number of available resources
            // so that whoever is waiting to access our resource can eventually gain access
            semaphore.signal()
        }
        // Every time we call .wait on the semaphore, the number of available resources either
        // decreases, or we wait for a resource to become available.
        _ = semaphore.wait(timeout: .now() + 2)
    }
}</code></pre></div><p>Let&#8217;s visualize that code in the <a href="https://www.geeksforgeeks.org/operating-systems/dining-philosopher-problem-using-semaphores/">Dining Philosopher Problem</a> manner but from the restaurant staff perspective. </p><p>The main purpose here is to clear the Live Activity as quickly as possible before the app completely terminates.</p><p>But the problem is;</p><ul><li><p><em><strong>activity.end(...)</strong></em> works async,</p></li><li><p>the process may be  interrupted,</p></li><li><p>Result: Live Activity stays remaining in both screens.</p></li></ul><p>That semaphore solution does; wait for a while before the app termination and Live Activity cleaning ends.<br><br>Let&#8217;s visualize in the restaurant analogy &#128071;</p><h3>Restaurant Analogy</h3><p>You are the restaurant owner. Restaurant will close soon. But there are still customers eating:</p><ul><li><p>Live Activities appear in lock screen</p></li><li><p>Live Activities in the Dynamic Island</p></li></ul><p>You say: <em>&#8220;Get out all customers before restaurant close &#129324;&#8221;</em></p><h3>1. Creating Semaphores</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;0dc2f9b6-2711-436b-95a6-9c316da0edc8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">let semaphore = DispatchSemaphore(value: 0)</code></pre></div><p>It means, put a waiter to the door. But the waiter at first in &#8220;None of the customers have not completed their dinner.&#8220; status. So, the restaurant owner (app) has to wait.</p><p><em><strong>value: 0</strong></em> means there is no allowance to close restaurant.</p><h3>2. Cleaning starts in background</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;2bf2511d-ed11-4dcd-a193-cfecdbd8c70d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">Task.detached(priority: .high) {}</code></pre></div><p>Send waiters to clean tables quickly.</p><p>Logic behind <em><strong>detached</strong></em> is &#8220;have a separate team work without involving the main restaurant manager&#8221;</p><h3>3. Remove all customers</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;673cba12-5093-4dc5-b6ed-b42e85988e0e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">for activity in Activity&lt;WalkActivityAttributes&gt;.activities {
    await activity.end(...)
}</code></pre></div><p>This means, the waiter goes to all tables and says <em>&#8220;Restaurant is closing, you must leave.&#8220;</em><br><br><em><strong>await</strong></em> is important because, the waiter is really expecting customers to leave. But, this doesn&#8217;t happen immediately.</p><h3>4. Notify the waiter when the job is finished</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;50ca067a-0ef4-45e8-9017-2ec3dacbf8bb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">semaphore.signal()</code></pre></div><p>It means, the waiter comes and tells <em>&#8220;Finally, all customers are gone &#128558;&#8205;&#128168;&#8220;</em></p><p><strong>DispatchSemaphore</strong> value becomes <strong>0 &#8594; 1</strong>. So, we can close the restaurant.</p><h3>5. App waits for a bit</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;6e5965de-6acb-43b5-9eca-6b33794e1ed8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">_ = semaphore.wait(timeout: .now() + 2)</code></pre></div><p>The restaurant owner waits on the doorstep and yells to staff <em>&#8220;I&#8217;ll wait for 2 sec until cleaning completes&#8220;.</em></p><p>If:</p><ul><li><p>Staff finish their job &#8594; restaurant closes properly.</p></li><li><p>If they can&#8217;t &#8594; time&#8217;s up &#8594; restaurant closes.</p></li></ul><h3>Why this code is important?</h3><p>Because <strong>iOS doesn&#8217;t wait</strong> <strong>async jobs</strong> while app is <strong>terminating</strong>.</p><p>So, normally if you do that app can directly close:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;554a8961-ba77-4eb5-ad4f-e3944ab34148&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">await activity.end()</code></pre></div><p>Semaphore solution says, <em>&#8220;One minute, let&#8217;s finish this job before close&#8221;.</em></p><h3>What does Semaphore represent here?</h3><p>In the restaurant example:</p><p><em><strong>wait(): </strong></em>The waiter waits on the doorstep.</p><p><em><strong>signal(): </strong></em>The staff says &#8220;Job is done&#8221;.</p><p><em><strong>value:</strong></em> <em><strong>0: </strong></em>No close.</p><p><em><strong>value: 1: </strong></em>Can be close now.</p><h3>Briefly</h3><ol><li><p>App will terminate</p></li><li><p>Start closing Live Activities</p></li><li><p>Wait a short while so the app doesn&#8217;t close immediately</p></li><li><p>Continue when the work is finished</p></li><li><p>Close again after 2 sec max</p></li></ol><p>Semaphore here works like a:</p><p>&#8220;Traffic police who manages closing road for a short time&#8221; &#128578;</p><h3>The Modern Way</h3><p><em><strong>DispatchSemaphore</strong></em> is an old school synchronization tool which comes from Grand Central Dispatch world. In the modern swift concurrency approach <em><strong>async/await</strong></em> and <em><strong>Task</strong></em> usage will be chosen.</p><p>In theory we can use something like below for terminating Live Activities:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;b85c7fe0-d211-43eb-8cb0-67480462ce28&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">func endAllActivities() async {
    for activity in Activity&lt;WalkActivityAttributes&gt;.activities {
        let finalContent = ActivityContent(
            state: activity.content.state,
            staleDate: nil
        )

        await activity.end(
            finalContent,
            dismissalPolicy: .immediate
        )
    }
}</code></pre></div><p>and we can call it:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;de20ca32-2666-4d2d-9aa7-d003d5d45f56&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">Task {
    await endAllActivities()
}</code></pre></div><p>However, there is an important problem here is the <em><strong>applicationWillTerminate </strong></em>is<em> </em><strong>not</strong><em> </em>an <strong>async</strong> lifecycle callback. iOS doesn&#8217;t guarantee the completion of async tasks when  terminating and application. Therefore, while using only <em><strong>Task {} </strong></em>is more modern in theory, it may not be reliable in practice.</p><p>Hence, the most healthier approach is not doing Live Activity cleanup while app termination but doing it in either while switching to background or doing it before the scene has been inactive.</p><p>For instance:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;1448eeb3-ad78-4e1e-b3ff-e83a8440aef4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">func sceneDidEnterBackground(_ scene: UIScene) {
    Task {
        await endAllActivities()
    }
}</code></pre></div><p>This approach:</p><ul><li><p>It is natively compatible with Swift Concurrency</p></li><li><p>It does not cause thread blocking</p></li><li><p>It removes Semaphore requirement</p></li><li><p>It increases completion possibility of async tasks</p></li></ul><div><hr></div><h3>Conclusion</h3><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!oTSD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F366a82f6-e5ce-4f41-b0cc-afd42160eea4_1324x266.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!oTSD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F366a82f6-e5ce-4f41-b0cc-afd42160eea4_1324x266.png 424w, https://substackcdn.com/image/fetch/$s_!oTSD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F366a82f6-e5ce-4f41-b0cc-afd42160eea4_1324x266.png 848w, https://substackcdn.com/image/fetch/$s_!oTSD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F366a82f6-e5ce-4f41-b0cc-afd42160eea4_1324x266.png 1272w, https://substackcdn.com/image/fetch/$s_!oTSD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F366a82f6-e5ce-4f41-b0cc-afd42160eea4_1324x266.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!oTSD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F366a82f6-e5ce-4f41-b0cc-afd42160eea4_1324x266.png" width="1324" height="266" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/366a82f6-e5ce-4f41-b0cc-afd42160eea4_1324x266.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:266,&quot;width&quot;:1324,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:105974,&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://emredegirmenci.substack.com/i/196920491?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F366a82f6-e5ce-4f41-b0cc-afd42160eea4_1324x266.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_!oTSD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F366a82f6-e5ce-4f41-b0cc-afd42160eea4_1324x266.png 424w, https://substackcdn.com/image/fetch/$s_!oTSD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F366a82f6-e5ce-4f41-b0cc-afd42160eea4_1324x266.png 848w, https://substackcdn.com/image/fetch/$s_!oTSD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F366a82f6-e5ce-4f41-b0cc-afd42160eea4_1324x266.png 1272w, https://substackcdn.com/image/fetch/$s_!oTSD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F366a82f6-e5ce-4f41-b0cc-afd42160eea4_1324x266.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p><em><strong>Task + async/await</strong></em> is the modern approach but there is no 100% guarantee during the app termination. Hence, the best approach is doing cleanup at the early stages of the lifecycle.</p><h3></h3><p></p><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[What do you do when you want to get notified when a bunch of async tasks have finished?]]></title><description><![CDATA[DispatchGroup vs TaskGroup]]></description><link>https://emredegirmenci.substack.com/p/what-do-you-do-when-you-want-to-get</link><guid isPermaLink="false">https://emredegirmenci.substack.com/p/what-do-you-do-when-you-want-to-get</guid><dc:creator><![CDATA[Emre Degirmenci]]></dc:creator><pubDate>Thu, 07 May 2026 16:21:50 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!PIrl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ba8870e-750d-4ed2-9442-ec5a00dd8087_1216x710.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_!PIrl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ba8870e-750d-4ed2-9442-ec5a00dd8087_1216x710.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PIrl!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ba8870e-750d-4ed2-9442-ec5a00dd8087_1216x710.png 424w, https://substackcdn.com/image/fetch/$s_!PIrl!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ba8870e-750d-4ed2-9442-ec5a00dd8087_1216x710.png 848w, https://substackcdn.com/image/fetch/$s_!PIrl!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ba8870e-750d-4ed2-9442-ec5a00dd8087_1216x710.png 1272w, https://substackcdn.com/image/fetch/$s_!PIrl!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ba8870e-750d-4ed2-9442-ec5a00dd8087_1216x710.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PIrl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ba8870e-750d-4ed2-9442-ec5a00dd8087_1216x710.png" width="480" height="280.2631578947368" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0ba8870e-750d-4ed2-9442-ec5a00dd8087_1216x710.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:710,&quot;width&quot;:1216,&quot;resizeWidth&quot;:480,&quot;bytes&quot;:426151,&quot;alt&quot;:&quot;Apple Developer - WWDC21 - Swift concurrency: Behind the scenes&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://emredegirmenci.substack.com/i/196658260?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ba8870e-750d-4ed2-9442-ec5a00dd8087_1216x710.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Apple Developer - WWDC21 - Swift concurrency: Behind the scenes" title="Apple Developer - WWDC21 - Swift concurrency: Behind the scenes" srcset="https://substackcdn.com/image/fetch/$s_!PIrl!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ba8870e-750d-4ed2-9442-ec5a00dd8087_1216x710.png 424w, https://substackcdn.com/image/fetch/$s_!PIrl!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ba8870e-750d-4ed2-9442-ec5a00dd8087_1216x710.png 848w, https://substackcdn.com/image/fetch/$s_!PIrl!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ba8870e-750d-4ed2-9442-ec5a00dd8087_1216x710.png 1272w, https://substackcdn.com/image/fetch/$s_!PIrl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0ba8870e-750d-4ed2-9442-ec5a00dd8087_1216x710.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 aria-hidden="true" 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><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">Apple Developer - WWDC21 - Swift concurrency: Behind the scenes</figcaption></figure></div><p>Nowadays, I&#8217;m brushing up my iOS concurrent programming skills based on my past iOS interview experiences. I&#8217;ve been creating an interview questions pool for the past 5 years based on the interviews that I have so far. Just after a week, the modern Swift Concurrency officially announced by Apple, I had a technical interview with Spotify and faced beautiful concurrency questions. Based on these questions, I decided to write some comparison article between <strong>DispatchGroup(</strong>legacy way<strong>) </strong>and <strong>TaskGroup </strong>(modern way). Let&#8217;s start with the legacy one!</p><h3>DispatchGroup</h3><p>It&#8217;s briefly used when you have a bunch of asynchronous tasks running in parallel and wait for all work to be completed in a given queue and you want to <strong>be notified</strong> when all of them are <strong>finished</strong>. Dispatch group have various work items to execute, and perform another work item once all of our work items are completed. A dispatch group doesn&#8217;t actually execute work and it&#8217;s up to you to decide where and how all of your work runs. It only tracks the number of tasks that you&#8217;ve started, and the number of tasks that you&#8217;ve completed.<br><br>Let&#8217;s look at the example:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;55b4685f-c73f-4229-a350-08189b6933bc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">func fetchUserProfiles() {
    let group = DispatchGroup() // 1
    var results = [Data]()
    let userIDs = [1, 2, 3]
    let urls = userIDs
        .compactMap { URL(string: "https://jsonplaceholder.typicode.com/users/\($0)") }

    for url in urls {
        group.enter() // 3
        URLSession.shared.dataTask(with: url) { data, response, error in
            if let data {
                results.append(data)
            }
            defer { group.leave() } // 4
        }.resume()
    }

    group.notify(queue: .main) { [weak self] in // 2
        self?.textLabel.text = "All jobs have completed!"
    }
}</code></pre></div><p>The code above kicks off an API call for each url, appends the fetched user data to an array, and once all API calls done the <em><strong>textLabel.text</strong></em> will be <em><strong>&#8220;All jobs have completed!&#8221;</strong></em>. </p><p>Let&#8217;s deep dive into comment lines:</p><ol><li><p>Create a new <em><strong>DispatchGroup()</strong></em> to track the number of times we start work, and the number of times we complete work.</p></li><li><p>Schedule a work item (update textLabel&#8217;s text) that will be executed on the main thread (indicated dispatch queue) once all work in the group is done. <em><strong>Note: The notification is itself asynchronous, so it&#8217;s possible to submit more jobs to the group after calling notify, as long as the previously submitted jobs haven&#8217;t already completed. </strong></em>(<a href="https://www.kodeco.com/books/concurrency-by-tutorials/v2.0">Source: Concurrency by Tutorials - Ray Wenderlich</a>)</p></li><li><p>For each url that was created, <strong>enter</strong> the dispatch group. It will increment the counter for the number of running tasks every time we enter the group.</p></li><li><p>Once the API call done, independently from the error or success cases, leave the dispatch group. Otherwise, you will never be signaled of completion. Once the last task is completed, <em><strong>notify(queue:)</strong></em> callback method will be informed.<br><br>As you can see the dispatch groups are very handy tool. Now, let&#8217;s see the Swift Concurrency version.</p></li></ol><h3>TaskGroup</h3><p>It&#8217;s pretty much like <strong>DispatchGroup</strong>, but in the modern <strong>Swift Concurrency</strong> world. A <strong>TaskGroup</strong> manages a dynamic number of child tasks, and automatically waits for all of them to complete before returning. Unlike <strong>DispatchGroup</strong>, it doesn&#8217;t track counters manually (<em>comment line // 3</em> in the previous code block) instead, it ties the lifetime of child tasks directly to the group itself.</p><p>Let&#8217;s look at the example:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;1d1f8cc5-d135-4fdd-a490-a501dd622b65&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">@MainActor
func fetchUserProfiles() async {
    var results = [Data]()
    let userIDs = [1, 2, 3]
    let urls = userIDs
        .compactMap { URL(string: "https://jsonplaceholder.typicode.com/users/\($0)") }

    await withTaskGroup(of: Data?.self) { group in // 1
        for url in urls {
            group.addTask { // 3
                let (data, _) = try? await URLSession.shared.data(from: url)
                return data
            }
        }

        for await data in group { // 2
            if let data {
                results.append(data)
            }
        }
    } // 4

    textLabel.text = "All jobs have completed"
}</code></pre></div><p>I used exact same code example in a <strong>TaskGroup </strong>way. </p><p>Let&#8217;s deep dive into comment lines:</p><ol><li><p><em><strong>withTaskGroup(of:)</strong></em> creates a new task group, similar to <em><strong>DispatchGroup()</strong></em>. Unlike <strong>DispatchGroup</strong> though, you declare upfront what type each child task will produce (i.e <strong>Data?.self</strong>).</p></li><li><p><em><strong>for await data in group</strong></em> replaces <em><strong>group.notify(queue:)</strong></em>. Instead of scheduling a callback to fire when all work is done, you iterate over results as each child task completes. The loop <strong>ends naturally</strong> once every <strong>child task finishes</strong>.</p></li><li><p><em><strong>group.addTask {}</strong></em> replaces the <em><strong>group.enter()</strong></em> + <em>closure</em> + <em><strong>group.leave()</strong></em> threes*me &#128586;. Adding a task implicitly enters the group, and returning from the closure implicitly leaves it, no risk of forgetting a <em><strong>leave()</strong></em>.</p></li><li><p>The closing brace of <em><strong>withTaskGroup</strong></em> is the moment all child tasks are guaranteed to be done. This is where <em><strong>group.notify(queue:)</strong></em> would have fired in the <strong>DispatchGroup</strong> world. After this point, <em><strong>results</strong></em> is safe to use.</p></li></ol><blockquote><p><strong>Note:</strong> Even though <em><strong>fetchUserProfiles()</strong></em> is marked <em><strong>@MainActor</strong></em>, the network calls inside <em><strong>addTask</strong></em> will never run on the main thread. URLSession is in charge of its own execution context, not the caller. So marking the function <em><strong>@MainActor</strong></em> is completely safe and won&#8217;t block the main thread. Thanks Matt<strong> </strong>for pointing out! &#129321;</p><div class="bluesky-wrap outer" style="height: auto; display: flex; margin-bottom: 24px;" data-attrs="{&quot;postId&quot;:&quot;3mldis5besk2g&quot;,&quot;authorDid&quot;:&quot;did:plc:klsh7edzj3jmxucibyjqstb3&quot;,&quot;authorName&quot;:&quot;Matt Massicotte&quot;,&quot;authorHandle&quot;:&quot;massicotte.org&quot;,&quot;authorAvatarUrl&quot;:&quot;https://cdn.bsky.app/img/avatar/plain/did:plc:klsh7edzj3jmxucibyjqstb3/bafkreiczw6bcnaj3tp7vupdgjuqb47e2azwzekwwj7gm5lqivyf5ingucq&quot;,&quot;text&quot;:&quot;First, that's very nice of you!\n\nAlso, this was not a test! I was just wondering because there wasn't enough context! And I really didn't mean to stress you out so late!\n\n(Also, don't forget, there is no way to run the network call on main, no matter what you do - callee is in charge!)&quot;,&quot;createdAt&quot;:&quot;2026-05-08T10:16:38.496Z&quot;,&quot;uri&quot;:&quot;at://did:plc:klsh7edzj3jmxucibyjqstb3/app.bsky.feed.post/3mldis5besk2g&quot;,&quot;imageUrls&quot;:[]}" data-component-name="BlueskyCreateBlueskyEmbed"><iframe id="bluesky-3mldis5besk2g" data-bluesky-id="7489945468491093" src="https://embed.bsky.app/embed/did:plc:klsh7edzj3jmxucibyjqstb3/app.bsky.feed.post/3mldis5besk2g?id=7489945468491093" width="100%" style="display: block; flex-grow: 1;" frameborder="0" scrolling="no"></iframe></div></blockquote><div><hr></div><p>As you can see, <strong>TaskGroup</strong> achieves the exact same goal as <strong>DispatchGroup</strong> but the structured approach eliminates the enter/leave counter belly dance, and <strong>await</strong> replaces the callback entirely, making the code read easily like synchronous code.<br><br>If you find modern concurrency super confusing like me, let&#8217;s start with these <strong>WWDC</strong> videos first:</p><p>- <a href="https://developer.apple.com/videos/play/wwdc2021/10254">WWDC21 - Swift concurrency: Behind the scenes</a><br>- <a href="https://developer.apple.com/videos/play/wwdc2022/110350">WWDC22 - Visualize and optimize Swift Concurrency</a><br>- <a href="https://developer.apple.com/videos/play/wwdc2023/10170">WWDC23 - Beyond the basics of structured concurrency</a><br><br></p>]]></content:encoded></item><item><title><![CDATA[Adapter Pattern in Swift]]></title><description><![CDATA[Adapt to The World]]></description><link>https://emredegirmenci.substack.com/p/adapter-pattern-in-swift</link><guid isPermaLink="false">https://emredegirmenci.substack.com/p/adapter-pattern-in-swift</guid><dc:creator><![CDATA[Emre Degirmenci]]></dc:creator><pubDate>Sun, 03 May 2026 15:27:28 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!6z84!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1107652c-554e-4d6f-811a-fe4af1c78618_694x610.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The Adapter Pattern, converts the protocol of a class into another protocol the clients expect. Adapter lets classes to work together that couldn&#8217;t otherwise, because of incompatible protocols. The intention is, not change the underlying behavior, not remove behavior, not additional behavior but just adapt something. It&#8217;s not complicated, right &#128565;&#8205;&#128171;<br><br>Imagine you are in the UK/UAE (3-pin, rectangular prongs on the wall) with your EU (2-pin) MacBook charger and your battery drained. You go to the store to buy an adapter which adapts your MacBook charger plug into that 3-pin rectangular prongs socket on the wall. <br><br>In this imagination; </p><p>- <strong>the adaptee</strong> <strong>(UK wall socket)</strong> has the incompatible interface <strong>the client</strong> <strong>(MacBook charger)</strong> can&#8217;t plug directly,<br>- your MacBook charger is <strong>the client</strong> that has the protocol it was built with EU 2-pin, <br>- <strong>the adapter </strong>that we bought from the store, sits in between, translating one protocol to the other without changing either side&#8217;s behavior</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!6z84!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1107652c-554e-4d6f-811a-fe4af1c78618_694x610.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!6z84!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1107652c-554e-4d6f-811a-fe4af1c78618_694x610.png 424w, https://substackcdn.com/image/fetch/$s_!6z84!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1107652c-554e-4d6f-811a-fe4af1c78618_694x610.png 848w, https://substackcdn.com/image/fetch/$s_!6z84!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1107652c-554e-4d6f-811a-fe4af1c78618_694x610.png 1272w, https://substackcdn.com/image/fetch/$s_!6z84!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1107652c-554e-4d6f-811a-fe4af1c78618_694x610.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!6z84!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1107652c-554e-4d6f-811a-fe4af1c78618_694x610.png" width="506" height="444.7550432276657" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1107652c-554e-4d6f-811a-fe4af1c78618_694x610.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:610,&quot;width&quot;:694,&quot;resizeWidth&quot;:506,&quot;bytes&quot;:161938,&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://emredegirmenci.substack.com/i/196309763?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1107652c-554e-4d6f-811a-fe4af1c78618_694x610.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_!6z84!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1107652c-554e-4d6f-811a-fe4af1c78618_694x610.png 424w, https://substackcdn.com/image/fetch/$s_!6z84!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1107652c-554e-4d6f-811a-fe4af1c78618_694x610.png 848w, https://substackcdn.com/image/fetch/$s_!6z84!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1107652c-554e-4d6f-811a-fe4af1c78618_694x610.png 1272w, https://substackcdn.com/image/fetch/$s_!6z84!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1107652c-554e-4d6f-811a-fe4af1c78618_694x610.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 aria-hidden="true" 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><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>Here's the Swift code demonstrated on the plug analogy. UK wall socket as Adaptee, MacBook EU charger as Client, and the travel adapter as the Adapter:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;d70b5f5f-6ba5-43ae-aab3-06bd50eaab46&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">// MARK: - Adaptee
// The UK wall socket &#8212; it only has UK 3-pin
 
class UKWallSocket {
    func provideUKPower() -&gt; String {
        return "&#9889;&#65039; 240V via UK 3-pin socket"
    }
}
</code></pre></div><p>The MacBook charger expects EU 2-pin power and it only works with <strong>EUPowerProvider</strong>. It has <strong>no idea</strong> whether the power comes from an <strong>EU socket</strong><br>or a <strong>UK socket</strong> adapted via <strong>TravelAdapter</strong>. MacBookCharger depends on the <strong>protocol</strong> (EUPowerProvider), not the concrete adapter. This keeps it decoupled.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;b91cac5a-a61a-4f02-98bd-c768960558b5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">// MARK: - Client Protocol
// The MacBook charger expects EU 2-pin power
 
protocol EUPowerProvider {
    func provideEUPower() -&gt; String
}

// MARK: - Client
 
class MacBookCharger {
    
    private let powerProvider: EUPowerProvider
 
    init(powerProvider: EUPowerProvider) {
        self.powerProvider = powerProvider
    }
 
    func charge() {
        let power = powerProvider.provideEUPower()
        print("MacBook charging... %\(power)")
    }
}</code></pre></div><p>The travel <strong>adapter</strong> sits <strong>between the two</strong>. It plugs into the UK socket <strong>(Adaptee)</strong> and<br>exposes the EU interface the MacBook charger <strong>(Client)</strong> expects. TravelAdapter <strong>wraps</strong> the <strong>adaptee(UKWallSocket)</strong> via <strong>composition (not inheritance)</strong>, which is the preferred Swift approach.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;cbb41477-3a99-4a70-974c-c5a789823a65&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">// MARK: - Adapter
 
class TravelAdapter: EUPowerProvider {
 
    private let ukSocket: UKWallSocket
 
    init(ukSocket: UKWallSocket) {
        self.ukSocket = ukSocket
    }
 
    func provideEUPower() -&gt; String {
        let ukPower = ukSocket.provideUKPower()
        return "&#128268; Adapted to EU 2-pin -&gt; \(ukPower)"
    }
}</code></pre></div><p>Doing so, the UK socket's behavior is <strong>unchanged</strong> and the adapter only translates, exactly as the pattern intends.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;2039b4c1-9536-4d2a-b8a5-0419a0ff41dc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">// MARK: - Usage
 
let ukSocket = UKWallSocket() // Adaptee
// Plug adapter into the ukSocket
let adapter = TravelAdapter(ukSocket: ukSocket) // Adapter
// Plug MacBook charger into the adapter
let macCharger = MacBookCharger(powerProvider: adapter) // Client
 
macCharger.charge()
// MacBook charging... &#128268; Adapted to EU 2-pin -&gt; &#9889;&#65039; 240V via UK 3-pin socket</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_!oV4t!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4092d949-d860-4f34-b2fc-724c20414550_1400x552.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!oV4t!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4092d949-d860-4f34-b2fc-724c20414550_1400x552.png 424w, https://substackcdn.com/image/fetch/$s_!oV4t!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4092d949-d860-4f34-b2fc-724c20414550_1400x552.png 848w, https://substackcdn.com/image/fetch/$s_!oV4t!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4092d949-d860-4f34-b2fc-724c20414550_1400x552.png 1272w, https://substackcdn.com/image/fetch/$s_!oV4t!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4092d949-d860-4f34-b2fc-724c20414550_1400x552.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!oV4t!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4092d949-d860-4f34-b2fc-724c20414550_1400x552.png" width="1400" height="552" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4092d949-d860-4f34-b2fc-724c20414550_1400x552.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:552,&quot;width&quot;:1400,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:248290,&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://emredegirmenci.substack.com/i/196309763?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4092d949-d860-4f34-b2fc-724c20414550_1400x552.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_!oV4t!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4092d949-d860-4f34-b2fc-724c20414550_1400x552.png 424w, https://substackcdn.com/image/fetch/$s_!oV4t!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4092d949-d860-4f34-b2fc-724c20414550_1400x552.png 848w, https://substackcdn.com/image/fetch/$s_!oV4t!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4092d949-d860-4f34-b2fc-724c20414550_1400x552.png 1272w, https://substackcdn.com/image/fetch/$s_!oV4t!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4092d949-d860-4f34-b2fc-724c20414550_1400x552.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 aria-hidden="true" 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><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><div><hr></div><h3>Conclusion</h3><p>In this article, I explained how the Adapter Pattern is used to make two classes with incompatible APIs work together. I demonstrated how to define an adapter by creating a class that wraps around the object being adapted, conforming to the interface the client expects.</p><p></p><p>Sources:<br>- <a href="https://www.amazon.com/Design-Patterns-Swift-Adam-Freeman/dp/148420395X">https://www.amazon.com/Design-Patterns-Swift-Adam-Freeman/dp/148420395X</a></p>]]></content:encoded></item><item><title><![CDATA[When “AI Did the Game Center Work Blindly” But Your App Ballooned to ~2× Size]]></title><description><![CDATA[LLMs and agentic coding tools are not bad at doing tasks: wire up Game Center, add the .gamekit file, sync with App Store Connect.]]></description><link>https://emredegirmenci.substack.com/p/when-ai-did-the-game-center-work</link><guid isPermaLink="false">https://emredegirmenci.substack.com/p/when-ai-did-the-game-center-work</guid><dc:creator><![CDATA[Emre Degirmenci]]></dc:creator><pubDate>Sat, 02 May 2026 14:12:05 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!5DDN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9353f539-abaf-48c4-9184-a57352a3c50e_444x900.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>LLMs and agentic coding tools are not bad at <em><strong>doing</strong></em> tasks: wire up Game Center, add the <em>.gamekit</em> file, sync with App Store Connect. They are easy to specify for what must not happen<em>, </em>for example <strong>shipping developer-only assets inside the customer build</strong>. This post is about one real case: <strong>Walk Mate &#8211; Daily Route Generator</strong>, where Game Center configuration ended up inside the app bundle, bloating thinned iPhone variants from on the order of <strong>~20&#8211;25 MB compressed</strong> to <strong>~120+ MB compressed</strong>.</p><p>The lesson is not <strong>never use LLMs</strong>. It is <strong>don&#8217;t trust the diff until you&#8217;ve validated size, bundle contents, and build configuration</strong>. The same things we should have checked before shipping anything, human or AI-written.</p><div><hr></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!5DDN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9353f539-abaf-48c4-9184-a57352a3c50e_444x900.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!5DDN!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9353f539-abaf-48c4-9184-a57352a3c50e_444x900.png 424w, https://substackcdn.com/image/fetch/$s_!5DDN!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9353f539-abaf-48c4-9184-a57352a3c50e_444x900.png 848w, https://substackcdn.com/image/fetch/$s_!5DDN!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9353f539-abaf-48c4-9184-a57352a3c50e_444x900.png 1272w, https://substackcdn.com/image/fetch/$s_!5DDN!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9353f539-abaf-48c4-9184-a57352a3c50e_444x900.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!5DDN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9353f539-abaf-48c4-9184-a57352a3c50e_444x900.png" width="444" height="900" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9353f539-abaf-48c4-9184-a57352a3c50e_444x900.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:900,&quot;width&quot;:444,&quot;resizeWidth&quot;:444,&quot;bytes&quot;:131163,&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://emredegirmenci.substack.com/i/196213669?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9353f539-abaf-48c4-9184-a57352a3c50e_444x900.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!5DDN!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9353f539-abaf-48c4-9184-a57352a3c50e_444x900.png 424w, https://substackcdn.com/image/fetch/$s_!5DDN!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9353f539-abaf-48c4-9184-a57352a3c50e_444x900.png 848w, https://substackcdn.com/image/fetch/$s_!5DDN!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9353f539-abaf-48c4-9184-a57352a3c50e_444x900.png 1272w, https://substackcdn.com/image/fetch/$s_!5DDN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9353f539-abaf-48c4-9184-a57352a3c50e_444x900.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 aria-hidden="true" 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><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><h3><br><br>What went wrong</h3><p>Apple&#8217;s <strong>Game Center</strong> integration in modern Xcode often uses a <em>GameCenterResources.gamekit</em> package: JSON plus optional localized images, <strong>Pull / Push</strong> with App Store Connect. That file belongs in the project for authoring and sync.</p><p>What it typically does not need, for a standard <strong>achievement</strong> setup, is to sit in <strong>Copy Bundle Resources</strong> for your <strong>Release</strong> app target. <strong>`GameKit` loads achievement metadata and art from Apple&#8217;s services</strong> when your app uses the right IDs. Much of that art lives on Apple&#8217;s CDN (you will see <strong>`mzstatic.com` URLs</strong> inside <strong>`gameCenterResources.json`</strong>). If some locales instead use <strong>relative paths</strong> (for example <strong>`da/AchievementImage-&#8230;.png`</strong>), those files are candidates to be <strong>bundled</strong>. If the <strong>entire `.gamekit`</strong> tree is copied into the app, <strong>every user pays</strong> for that download and disk use.</p><p>In <a href="https://apple.co/4mz7vev">Walk Mate</a>, the package had been wired like ordinary app resources including many megabytes of achievement images. It had also landed in <strong>more than one target</strong> (including an extension), where it had no runtime purpose.</p><p><strong>That&#8217;s the edge case:</strong> Game Center is configured with only what must ship in the IPA should ship.</p><h3>What I changed</h3><ol><li><p><strong>GameCenterResources.gamekit</strong> was removed from <strong>Copy Bundle Resources</strong> for the main iOS app and the notification service extension (it should never have been duplicated there).</p></li><li><p>The <strong>`.gamekit` bundle stays in the repository</strong> under version control so you can still use <strong>Xcode &#8594; (Game Center editor) &#8230; &#8594; Pull from / Push to App Store Connect</strong>; you simply stop embedding that package in the built product shipped to users.</p></li><li><p><strong>gameCenterResources.json </strong>clarified the mechanics: many locales referenced <strong>CDN URLs, Localization </strong>entries used <strong>local relative paths,</strong> which is how multi-megabyte PNGs became part of the payload while other languages did not!</p></li></ol><p>After a clean <strong>Archive</strong>, the <strong>App Thinning Size Report</strong> moved from roughly <strong>~125&#8211;130 MB compressed</strong> for representative iPhone/iPad variants <strong>down to</strong> roughly <strong>~23&#8211;26 MB compressed</strong> &#128512; for comparable slices, a drop on the order of <strong>~100 MB</strong> of avoidable bundle weight, consistent with removing the mistaken resource package. &#129320;<br></p><h3>How to check size</h3><p>After <strong><a href="https://help.apple.com/xcode/mac/current/#/devf37a1db04">Xcode &#8594; Product &#8594; Archive</a>, </strong>use the workflow that produces <strong>App Thinning </strong>analysis from <strong>Xcode Organizer </strong>when working with an App Store archive. Open <strong>App Thinning Size Report.txt</strong>.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!VxbU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b002a50-bb53-47ed-acd7-5faf0ed924e4_1554x230.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!VxbU!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b002a50-bb53-47ed-acd7-5faf0ed924e4_1554x230.png 424w, https://substackcdn.com/image/fetch/$s_!VxbU!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b002a50-bb53-47ed-acd7-5faf0ed924e4_1554x230.png 848w, https://substackcdn.com/image/fetch/$s_!VxbU!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b002a50-bb53-47ed-acd7-5faf0ed924e4_1554x230.png 1272w, https://substackcdn.com/image/fetch/$s_!VxbU!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b002a50-bb53-47ed-acd7-5faf0ed924e4_1554x230.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!VxbU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b002a50-bb53-47ed-acd7-5faf0ed924e4_1554x230.png" width="1456" height="215" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2b002a50-bb53-47ed-acd7-5faf0ed924e4_1554x230.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:215,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:62027,&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://emredegirmenci.substack.com/i/196213669?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b002a50-bb53-47ed-acd7-5faf0ed924e4_1554x230.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_!VxbU!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b002a50-bb53-47ed-acd7-5faf0ed924e4_1554x230.png 424w, https://substackcdn.com/image/fetch/$s_!VxbU!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b002a50-bb53-47ed-acd7-5faf0ed924e4_1554x230.png 848w, https://substackcdn.com/image/fetch/$s_!VxbU!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b002a50-bb53-47ed-acd7-5faf0ed924e4_1554x230.png 1272w, https://substackcdn.com/image/fetch/$s_!VxbU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2b002a50-bb53-47ed-acd7-5faf0ed924e4_1554x230.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><ul><li><p>Compare <strong>compressed </strong>vs <strong>uncompressed </strong>per variant: <strong>compressed </strong>is<strong> </strong>closest to <strong>download</strong> perception, <strong>uncompressed </strong>reflects expansion for that thinned slice.</p></li><li><p>Keep <strong>two reports </strong>(before / after a change) and diff the <strong>same variant family. </strong>That is a cheap <strong>regression guard </strong>for accidental resource bloat. </p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!AKM4!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F396a1e37-cf5b-42d5-8031-b38da741ef7e_506x1030.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!AKM4!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F396a1e37-cf5b-42d5-8031-b38da741ef7e_506x1030.png 424w, https://substackcdn.com/image/fetch/$s_!AKM4!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F396a1e37-cf5b-42d5-8031-b38da741ef7e_506x1030.png 848w, https://substackcdn.com/image/fetch/$s_!AKM4!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F396a1e37-cf5b-42d5-8031-b38da741ef7e_506x1030.png 1272w, https://substackcdn.com/image/fetch/$s_!AKM4!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F396a1e37-cf5b-42d5-8031-b38da741ef7e_506x1030.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!AKM4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F396a1e37-cf5b-42d5-8031-b38da741ef7e_506x1030.png" width="506" height="1030" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/396a1e37-cf5b-42d5-8031-b38da741ef7e_506x1030.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1030,&quot;width&quot;:506,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:113337,&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://emredegirmenci.substack.com/i/196213669?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F396a1e37-cf5b-42d5-8031-b38da741ef7e_506x1030.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_!AKM4!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F396a1e37-cf5b-42d5-8031-b38da741ef7e_506x1030.png 424w, https://substackcdn.com/image/fetch/$s_!AKM4!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F396a1e37-cf5b-42d5-8031-b38da741ef7e_506x1030.png 848w, https://substackcdn.com/image/fetch/$s_!AKM4!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F396a1e37-cf5b-42d5-8031-b38da741ef7e_506x1030.png 1272w, https://substackcdn.com/image/fetch/$s_!AKM4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F396a1e37-cf5b-42d5-8031-b38da741ef7e_506x1030.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 aria-hidden="true" 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><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><h3>Reducing app size in general</h3><ul><li><p><strong>Judge shipping app size </strong>from<strong> Release/Archive, </strong>not default <strong>Debug Run.</strong></p></li><li><p><strong>Asset catalogs: </strong>correct scales, avoid overweight PNGs where better formats or simplification suffice.</p></li><li><p><strong>Dependencies: </strong>every Swift package and framework has a cost! Periodic review of what links into the main target matters!</p></li><li><p><strong>Extensions: </strong>each target has its own resources rules, so, avoid duplicating large blobs.</p></li><li><p><strong>When prompting an LLM, </strong>state <strong>non-goals </strong>explicitly like: <em>&#8220;<strong>Do not add to Copy Bundle Resources unless runtime-required; verify with Archive + App Thinning diff.&#8221;</strong></em></p></li></ul><h3>Why we shouldn&#8217;t use LLMs blindly</h3><p>Models optimize for edit from your prompt and visible code. For instance, they don&#8217;t automatically enforce your <strong>download size or budget, </strong>the distinction between <strong>authoring / sync assets </strong> and <strong>runtime bundle contents.<br></strong></p><div><hr></div><h3>Conclusion</h3><p>AI can speed things up, -but it&#8217;s anyways your responsibility to triple-check- so you don&#8217;t embed authoring assets nobody needed at runtime &#128517;<br><br><br><br>Source: <a href="https://developer.apple.com/documentation/xcode/reducing-your-app-s-size">https://developer.apple.com/documentation/xcode/reducing-your-app-s-size</a></p>]]></content:encoded></item><item><title><![CDATA[Faster localization using POEditor]]></title><description><![CDATA[Localization is a very important step for mobile applications.]]></description><link>https://emredegirmenci.substack.com/p/faster-localization-using-poeditor</link><guid isPermaLink="false">https://emredegirmenci.substack.com/p/faster-localization-using-poeditor</guid><dc:creator><![CDATA[Emre Degirmenci]]></dc:creator><pubDate>Thu, 16 Apr 2026 18:45:12 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!_Qh8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6adbd5ab-5b1c-4ac0-af68-d9e3af60c5e9_1362x338.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Localization is a very important step for mobile applications. Because it makes it possible to reach a wide range of customers. &#127465;&#127472;&#127475;&#127476;&#127467;&#127470;&#127480;&#127466;&#127470;&#127480;&#127477;&#127473;&#127473;&#127483;</p><p>Even almost every people can speak English in Nordic countries, we&#8217;re supporting 7 different Nordic languages in my company.</p><p>We&#8217;re using <strong>POEditor</strong> for faster localization processes.</p><h1><a href="https://poeditor.com">POEditor</a></h1><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_Qh8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6adbd5ab-5b1c-4ac0-af68-d9e3af60c5e9_1362x338.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_Qh8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6adbd5ab-5b1c-4ac0-af68-d9e3af60c5e9_1362x338.png 424w, https://substackcdn.com/image/fetch/$s_!_Qh8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6adbd5ab-5b1c-4ac0-af68-d9e3af60c5e9_1362x338.png 848w, https://substackcdn.com/image/fetch/$s_!_Qh8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6adbd5ab-5b1c-4ac0-af68-d9e3af60c5e9_1362x338.png 1272w, https://substackcdn.com/image/fetch/$s_!_Qh8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6adbd5ab-5b1c-4ac0-af68-d9e3af60c5e9_1362x338.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_Qh8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6adbd5ab-5b1c-4ac0-af68-d9e3af60c5e9_1362x338.png" width="1362" height="338" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6adbd5ab-5b1c-4ac0-af68-d9e3af60c5e9_1362x338.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:338,&quot;width&quot;:1362,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:135082,&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://emredegirmenci.substack.com/i/194438633?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6adbd5ab-5b1c-4ac0-af68-d9e3af60c5e9_1362x338.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_!_Qh8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6adbd5ab-5b1c-4ac0-af68-d9e3af60c5e9_1362x338.png 424w, https://substackcdn.com/image/fetch/$s_!_Qh8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6adbd5ab-5b1c-4ac0-af68-d9e3af60c5e9_1362x338.png 848w, https://substackcdn.com/image/fetch/$s_!_Qh8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6adbd5ab-5b1c-4ac0-af68-d9e3af60c5e9_1362x338.png 1272w, https://substackcdn.com/image/fetch/$s_!_Qh8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6adbd5ab-5b1c-4ac0-af68-d9e3af60c5e9_1362x338.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><p>In this article, I will explain how to add a language, export, and import localization files in a faster way.</p><p>First of all, we have a <strong>String</strong> extension like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;e1bee13d-b178-4097-8f7f-9e5a0bf32b85&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">extension String {
    public func localized(_ bundle: Bundle = .locales) -&gt; String {
        NSLocalizedString(self, bundle: bundle, comment: "")
    }
}

//Usage
class Foo: UIViewController {
    let title = "settings_title".localized()
}</code></pre></div><p>When the new strings are added to the code base,</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;041c8272-a87b-4c83-9e7f-9f65632bae47&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">&#8220;settings_title&#8221;.localized()</code></pre></div><p>then you need to add these new strings to</p><pre><code>Add the string to the English version of Supporting Files/Localizable.strings
<strong>i.e.: </strong>Project_Directory/Project_Name/Sources/Locales/en.lproj/Localizable.strings</code></pre><p>and send to <strong>POEditor</strong> (either manually or automatically). Then someone(licensed translator) does the translation in <strong>POEditor</strong> manually. <br>After that, you should be able to fetch them (either manually or automatically) from <strong>POEditor</strong>.</p><h2>How do we extract/import translations from/to POEditor automatically?</h2><p>We can either manually send the new strings to <a href="http://poeditor.com/">poeditor.com</a> using<br><strong>fastlane translations_extract</strong> <a href="https://fastlane.tools/">fastlane</a> script or <strong>GitLab CI</strong> doing it for us automatically.</p><p>After someone(licensed translator) does the translation in <strong>POEditor</strong> manually.</p><p>After all of that has been done we can run the following command <br><strong>fastlane translations_import</strong> to fetch the new translations and check them into the codebase.</p><h2>Conclusion</h2><p><strong>POEditor</strong> is a great localization tool to increase user experience and save time since you will be waiting for translations for the new strings.</p>]]></content:encoded></item><item><title><![CDATA[iOS Accessibility VoiceOver]]></title><description><![CDATA[What is VoiceOver?]]></description><link>https://emredegirmenci.substack.com/p/ios-accessibility-voiceover</link><guid isPermaLink="false">https://emredegirmenci.substack.com/p/ios-accessibility-voiceover</guid><dc:creator><![CDATA[Emre Degirmenci]]></dc:creator><pubDate>Thu, 16 Apr 2026 18:39:36 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!MEXZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c57fee2-d535-438e-84b8-9ef9d58898c7_1400x1054.webp" 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_!MEXZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c57fee2-d535-438e-84b8-9ef9d58898c7_1400x1054.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!MEXZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c57fee2-d535-438e-84b8-9ef9d58898c7_1400x1054.webp 424w, https://substackcdn.com/image/fetch/$s_!MEXZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c57fee2-d535-438e-84b8-9ef9d58898c7_1400x1054.webp 848w, https://substackcdn.com/image/fetch/$s_!MEXZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c57fee2-d535-438e-84b8-9ef9d58898c7_1400x1054.webp 1272w, https://substackcdn.com/image/fetch/$s_!MEXZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c57fee2-d535-438e-84b8-9ef9d58898c7_1400x1054.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!MEXZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c57fee2-d535-438e-84b8-9ef9d58898c7_1400x1054.webp" width="1400" height="1054" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0c57fee2-d535-438e-84b8-9ef9d58898c7_1400x1054.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1054,&quot;width&quot;:1400,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:48098,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/webp&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://emredegirmenci.substack.com/i/194437936?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c57fee2-d535-438e-84b8-9ef9d58898c7_1400x1054.webp&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!MEXZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c57fee2-d535-438e-84b8-9ef9d58898c7_1400x1054.webp 424w, https://substackcdn.com/image/fetch/$s_!MEXZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c57fee2-d535-438e-84b8-9ef9d58898c7_1400x1054.webp 848w, https://substackcdn.com/image/fetch/$s_!MEXZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c57fee2-d535-438e-84b8-9ef9d58898c7_1400x1054.webp 1272w, https://substackcdn.com/image/fetch/$s_!MEXZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0c57fee2-d535-438e-84b8-9ef9d58898c7_1400x1054.webp 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 aria-hidden="true" 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><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">https://unsplash.com/photos/1tt7DzXb1WA</figcaption></figure></div><h2>What is VoiceOver?</h2><p>As you guess disabled people use smartphone apps as well. Designing your apps with accessibility in mind helps everyone use them, including people with vision, or hearing disabilities.</p><blockquote><p><em><a href="https://en.wikipedia.org/wiki/VoiceOver">VoiceOver is a screen reader built into Apple Inc.&#8217;s macOS, iOS, tvOS, watchOS, and iPod operating systems. By using VoiceOver, the user can access their Macintosh or iOS device based on spoken descriptions and, in the case of the Mac, the keyboard.</a></em></p></blockquote><h2>Why Accessibility?</h2><p>In my current company, we&#8217;re also supporting Accessibilities, especially VoiceOver. In our e-paper applications, we have <strong>active 2K blind users.</strong></p><ul><li><p>You&#8217;ll reach a larger group.</p></li><li><p>It feels good to know you&#8217;re making a noticeable difference in more people&#8217;s life.</p></li></ul><h2>How to Activate and Use VoiceOver?</h2><p>You can find detailed information about activation and usage of VoiceOver on iPhone on Apple&#8217;s website:<br><a href="https://support.apple.com/tr-tr/guide/iphone/iph3e2e415f/ios">https://support.apple.com/guide/iphone/iph3e2e415f/ios</a></p><ul><li><p><strong>Single-tap</strong> anywhere and VoiceOver will read information from the item&#8217;s accessibility attributes loudly.</p></li><li><p><strong>Single-swipe left or right</strong> and VoiceOver will select the next visible accessibility item and read it loudly.</p></li><li><p><strong>Single-swipe down</strong> to spell the focused item letter-by-letter.</p></li><li><p><strong>Double-tap</strong> to select the specific item.</p></li><li><p><strong>Three-finger-swipe</strong> left or right to navigate forward or backward in a page view.</p></li></ul><p>For the complete list of VoiceOver gestures, check out <a href="https://support.apple.com/guide/iphone/learn-voiceover-gestures-iph3e2e2281/ios">Apple&#8217;s Learn VoiceOver gestures on iPhone</a>. So now you know how VoiceOver works.</p><h2>Accessibility Attributes</h2><p>An accessibility attribute has five properties:</p><p>First of all, you should define the accessibility element of the UI element.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;fd6a9201-ceb5-4ff0-82da-f266679f7424&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">titleLabel.isAccessibilityElement = true</code></pre></div><ol><li><p><strong>accessibilityLabel:</strong> A concise way to identify the control or view.</p></li></ol><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;23e28788-42b9-4294-93c3-34bb0e442ad4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">titleLabel.accessibilityLabel = foo.title</code></pre></div><p>2.<strong>  accessibilityTraits</strong>: These describe the element&#8217;s state or behavior. A cell trait might be <strong>.button</strong>, for example.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;6a2a1e36-c969-47c6-8078-7acf8ed28129&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">cell.accessibilityTraits = .none

cell.accessibilityTraits = .button

cell.accessibilityTraits = .link

cell.accessibilityTraits = .header

cell.accessibilityTraits = .adjustable

cell.accessibilityTraits = .allowsDirectInteraction

cell.accessibilityTraits = .causesPageTurn

cell.accessibilityTraits = .image

cell.accessibilityTraits = .keyboardKey

cell.accessibilityTraits = .notEnabled

cell.accessibilityTraits = .playSound

cell.accessibilityTraits = .searchField

cell.accessibilityTraits = .startsMediaSession

cell.accessibilityTraits = .staticText

cell.accessibilityTraits = .selected

cell.accessibilityTraits = .summaryElement

cell.accessibilityTraits = .tabBar</code></pre></div><p>3.<strong>  accessibilityHint</strong>: Describes the action an element completes. For example:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;da644410-3270-4167-b55d-2a837e9a4b97&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">playButton.accessibilityHint = "double_tap_to_pause"
loginCell.accessibilityHint = "double_tap_to_log_out"</code></pre></div><p>4.<strong> accessibilityFrame</strong>: The frame of the element within the screen, in the format of a <strong>CGRect</strong>. VoiceOver speaks the contents of the <strong>CGRect</strong>.</p><p>5<strong>. accessibilityValue</strong>: The value of an element. For example, with a progress bar or a slider, the current value might read: <strong>5 out of 100</strong>.</p><h2>Using the Accessibility Inspector</h2><p>There&#8217;s a tool named <strong>Accessibility Inspector</strong>, which does the following:</p><ul><li><p>Lets you check the accessibility attributes of UI elements in Inspection Mode.</p></li><li><p>Provides live previews of accessibility elements without leaving your app.</p></li><li><p>Supports all platforms including macOS, iOS, watchOS, and tvOS.</p></li></ul><p>To do all of these things, open it in the Xcode menu by navigating to <strong>Xcode &#9656; Open Developer Tool &#9656; Accessibility Inspector</strong>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!iI4O!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae6add83-7dc9-4f66-8e19-8db67bfe2df2_870x715.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!iI4O!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae6add83-7dc9-4f66-8e19-8db67bfe2df2_870x715.webp 424w, https://substackcdn.com/image/fetch/$s_!iI4O!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae6add83-7dc9-4f66-8e19-8db67bfe2df2_870x715.webp 848w, https://substackcdn.com/image/fetch/$s_!iI4O!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae6add83-7dc9-4f66-8e19-8db67bfe2df2_870x715.webp 1272w, https://substackcdn.com/image/fetch/$s_!iI4O!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae6add83-7dc9-4f66-8e19-8db67bfe2df2_870x715.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!iI4O!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae6add83-7dc9-4f66-8e19-8db67bfe2df2_870x715.webp" width="870" height="715" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ae6add83-7dc9-4f66-8e19-8db67bfe2df2_870x715.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:715,&quot;width&quot;:870,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:36252,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/webp&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://emredegirmenci.substack.com/i/194437936?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae6add83-7dc9-4f66-8e19-8db67bfe2df2_870x715.webp&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!iI4O!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae6add83-7dc9-4f66-8e19-8db67bfe2df2_870x715.webp 424w, https://substackcdn.com/image/fetch/$s_!iI4O!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae6add83-7dc9-4f66-8e19-8db67bfe2df2_870x715.webp 848w, https://substackcdn.com/image/fetch/$s_!iI4O!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae6add83-7dc9-4f66-8e19-8db67bfe2df2_870x715.webp 1272w, https://substackcdn.com/image/fetch/$s_!iI4O!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae6add83-7dc9-4f66-8e19-8db67bfe2df2_870x715.webp 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 aria-hidden="true" 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><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><h2>Conclusion</h2><p>You learned about VoiceOver. You used the Accessibility Inspector to perform audits by scrolling through every accessible element. See you &#128406;&#127996;</p>]]></content:encoded></item><item><title><![CDATA[Demystifying Swift Concurrency: Insights from iOS Conf SG 2023]]></title><description><![CDATA[In this article, I will share key insights from Donny Wals&#8217;s compelling presentation on Swift Concurrency at iOS Conf SG 2023. Swift Concurrency introduces powerful features to manage asynchronous tasks, ensuring a smooth and responsive user experience in Swift applications.]]></description><link>https://emredegirmenci.substack.com/p/demystifying-swift-concurrency-insights</link><guid isPermaLink="false">https://emredegirmenci.substack.com/p/demystifying-swift-concurrency-insights</guid><dc:creator><![CDATA[Emre Degirmenci]]></dc:creator><pubDate>Thu, 16 Apr 2026 18:32:11 GMT</pubDate><enclosure url="https://substackcdn.com/image/youtube/w_728,c_limit/zgCtube1DSg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this article, I will share key insights from Donny Wals&#8217;s compelling presentation on <strong>Swift Concurrency at iOS Conf SG 2023</strong>. Swift Concurrency introduces powerful features to manage asynchronous tasks, ensuring a smooth and responsive user experience in Swift applications.</p><p><strong>@MainActor</strong> Attribute:</p><blockquote><p><em>- The `@MainActor` attribute plays a crucial role in Swift Concurrency by ensuring specific parts of your code always run on the main thread.</em></p><p><em>- Particularly useful in SwiftUI, where UI updates must be performed on the main thread.</em></p><p><em>- SwiftUI views are implicitly marked as `@MainActors` when they have `@ObservableObject` properties like `@StateObject`, `@ObservedObject`, and `@Environ</em>entObject`.</p></blockquote><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;e781a539-f1cb-4e10-af76-e271f21b6d42&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">struct MyView: View {
  @StateObject var vm = MyViewModel()
  
  var body: some View {
   Button {
    Task {
      await vm.performSomeWork()
    }
   } label: {
     Text("Test")
   }
  }
}</code></pre></div><p>Key Points:</p><blockquote><p><em>- `await` doesn&#8217;t block the current thread; it allows suspension of the current task, enabling other operations to continue.</em></p><p><em>- Functions decide the execution context, not where they are called from.</em></p></blockquote><p>Changing Execution Context:</p><blockquote><p><em>- You can explicitly specify the execution context using `@MainActor` or opt out of `MainActor` isolation with `no</em>isolated`.</p></blockquote><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;cb7f7026-df72-4cc9-ac82-e77f4f42228a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">class MyViewModel: ObservableObject {
 // This will always run on the Global executor
 func performSomeWork() async {}
// This will always run on the Main actor / thread
 @MainActor func performSomeWork() async {}
// This will _not_ run on the Main actor / thread
 nonisolated func performSomeWork() async {}
}</code></pre></div><p>Structured Concurrency and Tasks:</p><blockquote><p><em>- A Task represents a unit of work, and there are two types: unstructured and detached.</em></p><p><em>- Unstructured tasks inherit parts of their creation context but are not child tasks of their creation context.</em></p><p><em>- Detached tasks inherit nothing and work independently.</em></p></blockquote><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;03ece46c-ed5c-4427-a76c-1c6d8e4d3538&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">// Creating an unstructured Task
Task {
 // I'm an unstructured task
}
// Creating a detached Task
Task.detached {
 // I'm a detached task
}</code></pre></div><p>Key Takeaways:</p><blockquote><p><em>- Unstructured tasks are not child tasks of their creation context.</em></p><p><em>- Detached tasks inherit nothing and are their own islands of concurrency.</em></p></blockquote><p>Structured Concurrency:</p><blockquote><p><em>- Structured concurrency ensures that child tasks are completed before the parent task finishes.</em></p><p><em>- Unstructured tasks can outlive their parent tasks, but structured tasks enforce completion of child tasks before the parent task concludes.</em></p></blockquote><p>Example:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;8bcfdd29-2153-411f-9822-1cb0ed598948&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">func runTask() async {
  let result = await withTaskGroup(of: Int.self) { group -&gt; Int in
  group.spawn {
  await longRunningTask()
 }

// Wait for all child tasks to finish and collect the results
 var total = 0
 for await result in group {
  total += result
 }
  return total
 }
  print("Result: \(result)")
}</code></pre></div><p>Actor Isolation:</p><blockquote><p><em>- Actors provide a safe way to access and modify mutable state concurrently.</em></p><p><em>- Methods and properties marked with `@MainActor` are actor-isolated.</em></p><p><em>- Using `@MainActor` ensures that the method or property runs on the main thread.</em></p></blockquote><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;5875e82c-159c-4774-99ea-4f1258d6d43b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">@MainActor
class SafeActorIsolatedClassExample {
 var foo = Foo(bar: "baz")

 func thisIsSafe() {
   Task {
     foo.bar = "qux"
   }
  }
}</code></pre></div><p>Key Insights:</p><blockquote><p><em>- Whole classes or properties can be marked as `@MainActor`.</em></p><p><em>- `UIViewController` is actor-isolated to `@MainActor`, ensuring safe concurrent access.</em></p></blockquote><p>Conclusion:</p><p>Swift Concurrency brings powerful features to the table, allowing developers to create responsive and efficient asynchronous code. Understanding the nuances of <strong>`@MainActor`</strong>, structured concurrency, and actor isolation is essential for building robust and reliable Swift applications. Donny Wals&#8217;s presentation at <strong>iOS Conf SG 2023</strong> provides valuable insights into mastering Swift Concurrency.</p><p>Sources:<br>- [Your Brain &#129504; on Swift Concurrency &#8212; iOS Conf SG 2023](</p><div id="youtube2-zgCtube1DSg" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;zgCtube1DSg&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/zgCtube1DSg?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><p>)<br>- [Mutating a mutable Struct within a Swift Task](<a href="https://stackoverflow.com/a/76868102/4442254">https://stackoverflow.com/a/76868102/4442254</a>)</p>]]></content:encoded></item><item><title><![CDATA[SwiftUI Map Annotation Clustering in iOS 17]]></title><description><![CDATA[Introduction:]]></description><link>https://emredegirmenci.substack.com/p/swiftui-map-annotation-clustering</link><guid isPermaLink="false">https://emredegirmenci.substack.com/p/swiftui-map-annotation-clustering</guid><dc:creator><![CDATA[Emre Degirmenci]]></dc:creator><pubDate>Thu, 16 Apr 2026 18:27:23 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!bsEs!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac7ae22e-a739-4d30-b71a-35cd15675abb_1400x1750.webp" 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_!bsEs!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac7ae22e-a739-4d30-b71a-35cd15675abb_1400x1750.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!bsEs!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac7ae22e-a739-4d30-b71a-35cd15675abb_1400x1750.webp 424w, https://substackcdn.com/image/fetch/$s_!bsEs!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac7ae22e-a739-4d30-b71a-35cd15675abb_1400x1750.webp 848w, https://substackcdn.com/image/fetch/$s_!bsEs!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac7ae22e-a739-4d30-b71a-35cd15675abb_1400x1750.webp 1272w, https://substackcdn.com/image/fetch/$s_!bsEs!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac7ae22e-a739-4d30-b71a-35cd15675abb_1400x1750.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!bsEs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac7ae22e-a739-4d30-b71a-35cd15675abb_1400x1750.webp" width="452" height="565" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ac7ae22e-a739-4d30-b71a-35cd15675abb_1400x1750.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:1750,&quot;width&quot;:1400,&quot;resizeWidth&quot;:452,&quot;bytes&quot;:93194,&quot;alt&quot;:&quot;Photo by Tamas Tuzes-Katai on Unsplash&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/webp&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://emredegirmenci.substack.com/i/194436036?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac7ae22e-a739-4d30-b71a-35cd15675abb_1400x1750.webp&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-normal" alt="Photo by Tamas Tuzes-Katai on Unsplash" title="Photo by Tamas Tuzes-Katai on Unsplash" srcset="https://substackcdn.com/image/fetch/$s_!bsEs!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac7ae22e-a739-4d30-b71a-35cd15675abb_1400x1750.webp 424w, https://substackcdn.com/image/fetch/$s_!bsEs!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac7ae22e-a739-4d30-b71a-35cd15675abb_1400x1750.webp 848w, https://substackcdn.com/image/fetch/$s_!bsEs!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac7ae22e-a739-4d30-b71a-35cd15675abb_1400x1750.webp 1272w, https://substackcdn.com/image/fetch/$s_!bsEs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac7ae22e-a739-4d30-b71a-35cd15675abb_1400x1750.webp 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 aria-hidden="true" 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><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">Photo by <a href="https://unsplash.com/@tamas_tuzeskatai?utm_source=medium&amp;utm_medium=referral">Tamas Tuzes-Katai</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure></div><h2>Introduction:</h2><p>Map annotation clustering is a technique used to group nearby annotations on a map into a single cluster, which is represented by a single annotation. This is important for improving performance and user experience, especially when dealing with large datasets. Without clustering, a map with many annotations can become cluttered and difficult to read, leading to a poor user experience. Clustering reduces the number of annotations displayed on the map, making it easier to read and improving performance by reducing the amount of data that needs to be rendered.</p><h2>Overview of Map Annotation Clustering</h2><p>First of all, I got a lot of help from this open-source project (<a href="https://github.com/vospennikov/ClusterMap">ClusterMap</a>) in order to implement map annotation clustering in <strong>iOS 17 SwiftUI Map</strong>. I highly recommend checking it out if you want to learn more about how it works.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!tH-a!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6e88be8-2f5b-4b6c-8721-d07ab3d0931f_636x502.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tH-a!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6e88be8-2f5b-4b6c-8721-d07ab3d0931f_636x502.png 424w, https://substackcdn.com/image/fetch/$s_!tH-a!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6e88be8-2f5b-4b6c-8721-d07ab3d0931f_636x502.png 848w, https://substackcdn.com/image/fetch/$s_!tH-a!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6e88be8-2f5b-4b6c-8721-d07ab3d0931f_636x502.png 1272w, https://substackcdn.com/image/fetch/$s_!tH-a!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6e88be8-2f5b-4b6c-8721-d07ab3d0931f_636x502.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!tH-a!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6e88be8-2f5b-4b6c-8721-d07ab3d0931f_636x502.png" width="636" height="502" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f6e88be8-2f5b-4b6c-8721-d07ab3d0931f_636x502.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:502,&quot;width&quot;:636,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:499551,&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://emredegirmenci.substack.com/i/194436036?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6e88be8-2f5b-4b6c-8721-d07ab3d0931f_636x502.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_!tH-a!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6e88be8-2f5b-4b6c-8721-d07ab3d0931f_636x502.png 424w, https://substackcdn.com/image/fetch/$s_!tH-a!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6e88be8-2f5b-4b6c-8721-d07ab3d0931f_636x502.png 848w, https://substackcdn.com/image/fetch/$s_!tH-a!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6e88be8-2f5b-4b6c-8721-d07ab3d0931f_636x502.png 1272w, https://substackcdn.com/image/fetch/$s_!tH-a!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff6e88be8-2f5b-4b6c-8721-d07ab3d0931f_636x502.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 aria-hidden="true" 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><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><h2>Difference between working with UIKit Map and SwiftUI Map</h2><p>In terms of data handling and performance, ClusterMap library provides a solution for implementing map annotation clustering in <strong>SwiftUI Map()</strong> by changing the data structure <strong>from an array or dictionary</strong> to a <strong>tree</strong>.</p><p>The most critical difference from the native implementation in UIKit is that the ClusterMap library handles adding data asynchronously, avoiding lags when adding significant quantities of data. In contrast, Apple handles adding data to the <strong>MainThread</strong>, which can <strong>cause lags</strong> when scrolling or zooming. The library requires the dimensions of the grid used in clustering to be provided through a custom configuration object. This allows for custom configuration of the grid size used in clustering, improving performance during scrolling or zooming. While Apple&#8217;s implementation is more straightforward, the performance issue could be critical <strong>(e.g. Memory usage reduced from ~500 MB to ~230 MB).</strong></p><h2>Integration with SwiftUI Map</h2><p>When working with <strong>iOS 17</strong>, the library can be placed in an object marked with the <strong>@Observable</strong> macro and added to the hierarchy via the .environment modifier. The map will automatically reload when needed, such as when the camera position changes, and annotations can be added or removed in any other views. In my case I was trying to <strong>MKLocalSearch</strong> to search for places and display them on the map through a tabbar button.</p><p>An asynchronous local search is started using the <strong>MKLocalSearch</strong> class and the initialized request. The result of the search is stored in an async let variable called searchResult. After the search is completed, the clusterManager object is used to remove all existing annotations from the map. The mapItems property of the searchResult is then added to the clusterManager using the add method. Finally, the <strong>reloadAnnotations</strong><code> </code>method is called to update the map view with the new annotations.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;a680e951-6c3c-4b79-ab00-92974d9822aa&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">import ClusterMap
import Foundation
import MapKit
 
struct ExampleClusterAnnotation: Identifiable {
    var id = UUID()
    var coordinate: CLLocationCoordinate2D
    var count: Int
}
    
@Observable
class LocalSearchCompleter: NSObject {
var mapSize: CGSize = .zero
var currentRegion: MKCoordinateRegion = .userLocation
var annotations = [MKMapItem]() // #1 element of the tree
var clusters = [ExampleClusterAnnotation]() // #2 element of the tree
 
let customConfig = ClusterManager&lt;MKMapItem&gt;.Configuration(
    cellSizeForZoomLevel: { (zoom: Int) -&gt; CGSize in
        switch zoom {
        case 13...15: return CGSize(width: 64, height: 64) // grid size used in clustering
        case 16...18: return CGSize(width: 32, height: 32)
        case 19...: return CGSize(width: 16, height: 16)
        default: return CGSize(width: 88, height: 88)
        }
    }
)
 
var clusterManager: ClusterManager&lt;MKMapItem&gt;
 
override init() {
    clusterManager = ClusterManager&lt;MKMapItem&gt;(configuration: customConfig)
}
 
func search(for query: String) async {
        let request = MKLocalSearch.Request()
        request.naturalLanguageQuery = query
        request.region = currentRegion
        do {
            async let searchResult = MKLocalSearch(request: request).start()
            await clusterManager.removeAll()
            try await clusterManager.add(searchResult.mapItems)
            await reloadAnnotations()
        } catch {
            assertionFailure("Error: \(error.localizedDescription)")
        }
    }
 
    func reloadAnnotations() async {
        async let changes = clusterManager.reload(mapViewSize: mapSize, coordinateRegion: currentRegion)
        await applyChanges(changes)
    }
 
    @MainActor
    private func applyChanges(_ difference: ClusterManager&lt;MKMapItem&gt;.Difference) {
        for removal in difference.removals {
            switch removal {
            case .annotation(let annotation):
                annotations.removeAll { $0 == annotation }
            case .cluster(let clusterAnnotation):
                clusters.removeAll { $0.id == clusterAnnotation.id }
            }
        }
        for insertion in difference.insertions {
            switch insertion {
            case .annotation(let newItem):
                annotations.append(newItem)
            case .cluster(let newItem):
                clusters.append(ExampleClusterAnnotation(
                    id: newItem.id,
                    coordinate: newItem.coordinate,
                    count: newItem.memberAnnotations.count
                ))
            }
        }
    }
}
 
extension MKMapItem: CoordinateIdentifiable, Identifiable {
  public var id: String {
      placemark.region?.identifier ?? UUID().uuidString
  } 
 
  public var coordinate: CLLocationCoordinate2D {
      get { placemark.coordinate }
      set(newValue) { }
  }
}</code></pre></div><p>The customConfig object is defined using the <strong>ClusterManager</strong> class, which allows for custom configuration of the grid size used in clustering. Finally, the clusterManager property is initialized with the custom configuration.</p><p>Usage of the <strong>LocalSearchCompleter()</strong> in SwiftUI View:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;d21f5410-d70c-426e-9e92-78867c512b5b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">import SwiftUI
import MapKit
import ClusterMap
 
struct LocationRequestIsOnView: View {
    
    @State private var searchClient = LocalSearchCompleter()
 
    var body: some View {
        NavigationStack {
            ZStack {
                mapView
            }
        }
    }
    
    var mapView: some View {
        Map(
            initialPosition: .region(searchClient.currentRegion),
            selection: $selectedResult
        ) {
            if !showSettingsView {
                ForEach(searchClient.annotations) { result in
                    if systemImageName == "bolt.car.circle.fill" {
                        Marker(result.placemark.name ?? "", systemImage: "bolt.car.circle.fill", coordinate: result.coordinate)
                            .tag(result)
                    } else {
                        Marker(result.placemark.name ?? "", systemImage: "fuelpump.circle.fill", coordinate: result.coordinate)
                            .tag(result)
                    }
                }
 
                ForEach(searchClient.clusters) { result in
                    if systemImageName == "bolt.car.circle.fill" {
                        Marker("\(result.count)", systemImage: "bolt.car.fill", coordinate: result.coordinate)
                    } else {
                        Marker("\(result.count)", systemImage: "fuelpump.circle.fill", coordinate: result.coordinate)
                    }
                }
                
                UserAnnotation()
                
                if let route {
                    MapPolyline(route)
                        .stroke(.blue, lineWidth: 5)
                }
            }
        }
        .readSize(onChange: { newValue in
            searchClient.mapSize = newValue
        })
        .onChange(of: selectedResult) {
            if selectedResult != nil {
                getDirections()
                isShowingBottomSheet = true
            } else {
                isShowingBottomSheet = false
            }                
        }
        .sheet(isPresented: $isShowingBottomSheet) {
            ItemInfoView(
                isShowingBottomSheet: $isShowingBottomSheet,
                route: $route,
                selectedResult: $selectedResult, selectedTabBarButton: selectedTabBarButton,
                url: self.shareLocation()
            )
            .presentationDetents([.medium, .fraction(0.12), .large])
            .presentationBackgroundInteraction(.enabled(upThrough: .medium))
            .presentationDragIndicator(.visible)
        }
        .onMapCameraChange { context in
            searchClient.currentRegion = context.region
        }
        .onMapCameraChange(frequency: .onEnd) { context in
            Task.detached {
                await searchClient.reloadAnnotations()
            }
        }
        .mapControls {
            MapUserLocationButton()
            MapCompass()
            MapScaleView()
        }
        .environment(searchClient)
    }
}</code></pre></div><p>There is a <strong>ForEach</strong> loop that iterates over the <strong>searchClient.annotations</strong> array (which are the first element of the tree). For each annotation, a Marker view is created and displayed on the map. The Marker represents a location on the map and includes a title, a system image, and a coordinate. Similarly, there is another ForEach loop that iterates over the <strong>searchClient.clusters </strong>array (which are the second elements of the tree). For each cluster, a Marker view is created and displayed on the map. The Marker represents a cluster of locations and includes the count of locations in the cluster, a system image, and a coordinate. The <strong>mapView</strong> also includes various modifiers and event handlers. For example, the <strong>readSize</strong> modifier is used to update the <strong>searchClient</strong>'s mapSize property when the size of the map view changes. The <strong>onChange</strong> event handler is used to perform actions when the <strong>selectedResult</strong> changes, such as getting directions and showing or hiding a bottom sheet.</p><h2>Performance Benefits of ClusterMap</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!SKJh!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff59fcd0e-ab3f-4849-bdec-adfb8193c167_1400x909.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!SKJh!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff59fcd0e-ab3f-4849-bdec-adfb8193c167_1400x909.webp 424w, https://substackcdn.com/image/fetch/$s_!SKJh!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff59fcd0e-ab3f-4849-bdec-adfb8193c167_1400x909.webp 848w, https://substackcdn.com/image/fetch/$s_!SKJh!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff59fcd0e-ab3f-4849-bdec-adfb8193c167_1400x909.webp 1272w, https://substackcdn.com/image/fetch/$s_!SKJh!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff59fcd0e-ab3f-4849-bdec-adfb8193c167_1400x909.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!SKJh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff59fcd0e-ab3f-4849-bdec-adfb8193c167_1400x909.webp" width="1400" height="909" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f59fcd0e-ab3f-4849-bdec-adfb8193c167_1400x909.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:909,&quot;width&quot;:1400,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:34084,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/webp&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://emredegirmenci.substack.com/i/194436036?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff59fcd0e-ab3f-4849-bdec-adfb8193c167_1400x909.webp&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!SKJh!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff59fcd0e-ab3f-4849-bdec-adfb8193c167_1400x909.webp 424w, https://substackcdn.com/image/fetch/$s_!SKJh!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff59fcd0e-ab3f-4849-bdec-adfb8193c167_1400x909.webp 848w, https://substackcdn.com/image/fetch/$s_!SKJh!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff59fcd0e-ab3f-4849-bdec-adfb8193c167_1400x909.webp 1272w, https://substackcdn.com/image/fetch/$s_!SKJh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff59fcd0e-ab3f-4849-bdec-adfb8193c167_1400x909.webp 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 aria-hidden="true" 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><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><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_q9C!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92ee80c4-8847-4c62-b350-85328805f859_1400x909.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_q9C!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92ee80c4-8847-4c62-b350-85328805f859_1400x909.webp 424w, https://substackcdn.com/image/fetch/$s_!_q9C!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92ee80c4-8847-4c62-b350-85328805f859_1400x909.webp 848w, https://substackcdn.com/image/fetch/$s_!_q9C!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92ee80c4-8847-4c62-b350-85328805f859_1400x909.webp 1272w, https://substackcdn.com/image/fetch/$s_!_q9C!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92ee80c4-8847-4c62-b350-85328805f859_1400x909.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_q9C!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92ee80c4-8847-4c62-b350-85328805f859_1400x909.webp" width="1400" height="909" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/92ee80c4-8847-4c62-b350-85328805f859_1400x909.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:909,&quot;width&quot;:1400,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:52594,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/webp&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://emredegirmenci.substack.com/i/194436036?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92ee80c4-8847-4c62-b350-85328805f859_1400x909.webp&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!_q9C!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92ee80c4-8847-4c62-b350-85328805f859_1400x909.webp 424w, https://substackcdn.com/image/fetch/$s_!_q9C!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92ee80c4-8847-4c62-b350-85328805f859_1400x909.webp 848w, https://substackcdn.com/image/fetch/$s_!_q9C!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92ee80c4-8847-4c62-b350-85328805f859_1400x909.webp 1272w, https://substackcdn.com/image/fetch/$s_!_q9C!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F92ee80c4-8847-4c62-b350-85328805f859_1400x909.webp 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 aria-hidden="true" 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><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>&#8230;and so many other <strong>hangs, hitchs</strong> and <strong>lags</strong> that I was experiencing with the native MapKit implementation which I couldn&#8217;t add the <strong>Xcode Instruments</strong> screenshots.</p><p>Overall, the project demonstrates how the combination of <strong>SwiftUI Map</strong>, <strong>ClusterMap</strong>, and other technologies can be used to create a performant and customizable map view in <strong>iOS 17</strong>. <strong>&#8220;Thanks Apple &#129315;&#8221;</strong> By following best practices for implementing map annotation clustering, such as using a third-party library and providing custom configuration for the grid size, you can create a better user experience and improve the performance of your MapKit projects.</p><p>See you &#9996;&#127996;</p>]]></content:encoded></item><item><title><![CDATA[My journey with The Composable Architecture]]></title><description><![CDATA[General Overview]]></description><link>https://emredegirmenci.substack.com/p/my-journey-with-the-composable-architecture</link><guid isPermaLink="false">https://emredegirmenci.substack.com/p/my-journey-with-the-composable-architecture</guid><dc:creator><![CDATA[Emre Degirmenci]]></dc:creator><pubDate>Thu, 16 Apr 2026 18:13:14 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!8CI-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecca928a-22c0-4c8a-8743-88f818c88436_1400x706.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>General Overview</h2><blockquote><p><em><a href="https://github.com/pointfreeco/swift-composable-architecture">The Composable Architecture (TCA, for short)(opens in a new tab)</a> is a library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind, developed by <a href="https://github.com/mbrandonw">Brandon Williams(opens in a new tab)</a> and <a href="https://github.com/stephencelis">Stephen Celis(opens in a new tab)</a> from Point-Free. It can be used in SwiftUI, UIKit, and more, and on any Apple platform (iOS, macOS, tvOS, and watchOS).&#8221;</em></p></blockquote><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8CI-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecca928a-22c0-4c8a-8743-88f818c88436_1400x706.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8CI-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecca928a-22c0-4c8a-8743-88f818c88436_1400x706.webp 424w, https://substackcdn.com/image/fetch/$s_!8CI-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecca928a-22c0-4c8a-8743-88f818c88436_1400x706.webp 848w, https://substackcdn.com/image/fetch/$s_!8CI-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecca928a-22c0-4c8a-8743-88f818c88436_1400x706.webp 1272w, https://substackcdn.com/image/fetch/$s_!8CI-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecca928a-22c0-4c8a-8743-88f818c88436_1400x706.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8CI-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecca928a-22c0-4c8a-8743-88f818c88436_1400x706.webp" width="1400" height="706" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ecca928a-22c0-4c8a-8743-88f818c88436_1400x706.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:706,&quot;width&quot;:1400,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:25620,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/webp&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://emredegirmenci.substack.com/i/194429706?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecca928a-22c0-4c8a-8743-88f818c88436_1400x706.webp&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!8CI-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecca928a-22c0-4c8a-8743-88f818c88436_1400x706.webp 424w, https://substackcdn.com/image/fetch/$s_!8CI-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecca928a-22c0-4c8a-8743-88f818c88436_1400x706.webp 848w, https://substackcdn.com/image/fetch/$s_!8CI-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecca928a-22c0-4c8a-8743-88f818c88436_1400x706.webp 1272w, https://substackcdn.com/image/fetch/$s_!8CI-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fecca928a-22c0-4c8a-8743-88f818c88436_1400x706.webp 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 aria-hidden="true" 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><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><h2>Core tools</h2><ul><li><p>State management</p></li></ul><p>How to manage the state of your application using simple value types, and share state across many screens so that mutations in one screen can be immediately observed in another screen.</p><ul><li><p>Composition</p></li></ul><p>How to break down large features into smaller components that can be extracted to their own, isolated modules and be easily glued back together to form the feature.</p><ul><li><p>Side effects</p></li></ul><p>How to let certain parts of the application talk to the outside world in the most testable and understandable way possible.</p><ul><li><p>Testing</p></li></ul><p>How to not only test a feature built in the architecture, but also write integration tests for features that have been composed of many parts, and write end-to-end tests to understand how side effects influence your application. This allows you to make strong guarantees that your business logic is running in the way you expect.</p><ul><li><p>Ergonomics</p></li></ul><p>How to accomplish all of the above in a simple API with as few concepts and moving parts as possible.</p><h2>Basic Usage</h2><p>To build a feature using the Composable Architecture you define some types and values that model your domain:</p><ul><li><p>State: A type that describes the data your feature needs to perform its logic and render its UI.</p></li><li><p>Action: A type that represents all of the actions that can happen in your feature, such as user actions, notifications, event sources and more.</p></li><li><p>Reducer: A function that describes how to evolve the current state of the app to the next state given an action. The reducer is also responsible for returning any effects that should be run, such as API requests, which can be done by returning an Effect value.</p></li><li><p>Store: The runtime that actually drives your feature. You send all user actions to the store so that the store can run the reducer and effects, and you can observe state changes in the store so that you can update UI.</p></li></ul><p>The benefits of doing this are that you will instantly unlock testability of your feature, and you will be able to break large, complex features into smaller domains that can be glued together.</p><h2>Demo Project</h2><p>As a basic example, consider a UI that shows a 2-columns grid list along with &#8220;#&#8221; team/player ids, names and, team logos. When the views appear, there is an API request to fetch NBA teams and players and then displays in a 2-columns grid list.</p><p>&#129489;&#127996;&#8205;&#129459;&#128104;&#127996;&#8205;&#129459; To implement this root (grand-parent) feature we create a new type that will house the root domain and behavior of the feature by conforming to Reducer:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;d347cb68-2df4-4866-a561-b43ad886f95f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">import ComposableArchitecture

struct RootFeature: Reducer {}</code></pre></div><p>In here we need to define a type for the root feature&#8217;s state, which consists of a selected tab, as well as parent feature states that are TeamListFeature.State() and PlayerListFeature.State():</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;06a445a5-7335-4336-9aec-22ca83456ee7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">struct RootFeature: Reducer {
    struct State: Equatable {
        var selectedTab = Tab.teams
        var teamListState = TeamListFeature.State()
        var playerListState = PlayerListFeature.State()
    }
}</code></pre></div><p>We need to define a type for the feature&#8217;s tabs. There are the obvious tabbar items, such as teams, favorites and players.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;022d09f2-1e71-4dec-b27b-b9472d9cc9a1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">struct RootFeature: Reducer {
    struct State: Equatable {
        var selectedTab = Tab.teams
        var teamListState = TeamListFeature.State()
        var playerListState = PlayerListFeature.State()
    }
    enum Tab {
        case teams
        case favorites
        case players
    }
}</code></pre></div><p>We also need to define a type for the root (grand-parent) feature&#8217;s actions. There are the obvious actions, such as tabbar item selected, and the actions that are encapsulate all actions from the child domain/feature of the root (grand-parent) domain/feature, providing a comprehensive and cohesive approach.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;a4ba744f-dc21-4fec-9d82-c3bea5ec785f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">struct RootFeature: Reducer {
    struct State: Equatable {
        var selectedTab = Tab.teams
        var teamListState = TeamListFeature.State()
        var playerListState = PlayerListFeature.State()
    }
    enum Tab {
        case teams
        case favorites
        case players
    }
    enum Action: Equatable {
        case tabSelected(Tab)
        case teamList(TeamListFeature.Action)
        case playerList(PlayerListFeature.Action)
    }
}</code></pre></div><p>And then we implement the reduce method which is responsible for handling the actual logic and behavior for the feature. It describes how to change the current state to the next state, and describes what effects need to be executed. Some actions don&#8217;t need to execute effects, and they can return .none to represent that:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;9203d558-fd0c-4a8d-999a-b610e7088742&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">struct RootFeature: Reducer {
    struct State: Equatable {
        var selectedTab = Tab.teams
        var teamListState = TeamListFeature.State()
        var playerListState = PlayerListFeature.State()
    }
    
    enum Tab {
        case teams
        case favorites
        case players
    }
    
    enum Action: Equatable {
        case tabSelected(Tab)
        case teamList(TeamListFeature.Action)
        case playerList(PlayerListFeature.Action)
    }
    
    // Dependencies
    var fetchTeams: () async throws -&gt; TeamsModel
    var fetchPlayers:  @Sendable () async throws -&gt; PlayersModel
    var uuid: @Sendable () -&gt; UUID
    
    static let live = Self(
        fetchTeams: MatchScoresClient.liveValue.fetchTeams,
        fetchPlayers: MatchScoresClient.liveValue.fetchPlayers,
        uuid: { UUID() }
    )
    
    var body: some Reducer&lt;State, Action&gt; {
        Reduce { state, action in
            switch action {
            case .teamList:
                return .none
            case .playerList:
                return .none
            case .tabSelected(let tab):
                state.selectedTab = tab
                return .none
            }
        }
        Scope(state: \.teamListState, action: /RootFeature.Action.teamList) {
            TeamListFeature(uuid: uuid)
        }
        Scope(state:  \.playerListState, action: /RootFeature.Action.playerList) {
            PlayerListFeature(uuid: uuid)
        }
    }
}</code></pre></div><p>And then finally we define the view that displays the feature. It holds onto a <strong>StoreOf&lt;RootFeature&gt;</strong> so that it can observe all changes to the state and re-render, and we can send all user actions to the store so that state changes. <strong>WithViewStore</strong> is a view helper that transforms a Store into a ViewStore so that its state can be observed by a view builder:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;ba6b4ac3-016a-43bb-8b56-2682d9116c75&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">struct RootView: View {
    let store: StoreOf&lt;RootFeature&gt;
    
    var body: some View {
        WithViewStore(self.store, observe: { $0 }) { viewStore in
            TabView(
                selection: viewStore.binding(
                    get: \.selectedTab,
                    send: RootFeature.Action.tabSelected
                )
            ) {
                TeamListView(
                    store: self.store.scope(
                        state: \.teamListState,
                        action: RootFeature.Action
                            .teamList
                    )
                )
                .tabItem {
                    Image(systemName: "list.bullet")
                    Text("Teams")
                }
                .tag(RootFeature.Tab.teams)
                
                PlayerListView(
                    store: self.store.scope(
                        state: \.playerListState,
                        action: RootFeature.Action.playerList
                    )
                )
                .tabItem {
                    Image(systemName: "person.fill")
                    Text("Players")
                }
                .tag(RootFeature.Tab.players)
            }
            .accentColor(Color("launch-screen-background"))
        }
    }
}</code></pre></div><p>Once we are ready to display these views, for example in the app&#8217;s entry point, we can construct a store. This can be done by specifying the initial state to start the application in, as well as the reducer that will power the application:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;6a9f7c04-732d-418c-9dcd-0543e0398524&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">import SwiftUI
import ComposableArchitecture

@main
struct MatchScoresTCAApp: App {
    var body: some Scene {
        WindowGroup {
            RootView(
                store: Store(initialState: RootDomain.State()) {
                    RootDomain(fetchTeams: { TeamsModel.sample}, fetchPlayers: { PlayersModel.sample }, uuid: { UUID() }
                    )
                }
            )
        }
    }
}</code></pre></div><p>&#128113;&#127995;&#8205;&#9794;&#65039;&#128113;&#127996;To implement this parent feature we create a new type that will house the domain and behavior of the feature by conforming to Reducer:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;f9fc9e58-aeb5-40d2-afc8-f5dc7625c822&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">import ComposableArchitecture

struct TeamListFeature: Reducer {
}</code></pre></div><p>In here we need to define a type for the feature&#8217;s state, which consists of a data loading status, as well as an TeamsModel that is an Identified collection which are designed to solve all of the collection problems by providing data structures for working with collections (teams and meta) of identifiable elements in an ergonomic, performant way:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;46b43c07-1246-4c9f-b065-8a34fb899b67&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">struct TeamListFeature: Reducer {
    struct State: Equatable {
        var dataLoadingStatus = DataLoadingStatus.notStarted
        var resultTeamRequestInFlight: TeamsModel?
        var teamList: IdentifiedArrayOf&lt;TeamFeature.State&gt; = []
        
        var shouldShowError: Bool {
            dataLoadingStatus == .error
        }
        
        var isLoading: Bool {
            dataLoadingStatus == .loading
        }   
    }
}</code></pre></div><p>We also need to define a type for the feature&#8217;s actions. There are the obvious action, such as view on appear and the action that occurs when we receive a response from the team/player API request, and define a <strong>team(id: TeamFeature.State.ID, action: TeamFeature.Action)</strong> case to handle actions sent to the child domain (TeamFeature: Reducer):</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;c7aae2a8-e830-42ac-859d-0de4ac55d9bb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">struct TeamListFeature: Reducer {
    struct State: Equatable {
        var dataLoadingStatus = DataLoadingStatus.notStarted
        var teamList: IdentifiedArrayOf&lt;TeamFeature.State&gt; = []
        var shouldShowError: Bool {
            dataLoadingStatus == .error
        }
        var isLoading: Bool {
            dataLoadingStatus == .loading
        }   
    }
    enum Action: Equatable {
        case fetchTeamResponse(TaskResult&lt;TeamsModel&gt;)
        case team(id: TeamFeature.State.ID, action: TeamFeature.Action)
        case onAppear
    }
}</code></pre></div><p>And then we implement the reduce method which is responsible for handling the actual logic and behavior for the feature. It describes how to change the current state to the next state, and describes what effects need to be executed. Some actions don&#8217;t need to execute effects, and they can return .none to represent that:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;387d037c-af3a-4243-84f7-3d2605fd6806&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">struct TeamListFeature: Reducer {
  struct State: Equatable { /* ... */ }
  enum Action: Equatable { /* ... */ }

func reduce(into state: inout State, action: Action) -&gt; Effect&lt;Action&gt; {
        switch action {
        case .fetchTeamResponse(.failure(let error)):
            state.dataLoadingStatus = .error
            print(error)
            print("Error getting products, try again later.")
            return .none
            
        case let .fetchTeamResponse(.success(teamData)):
            state.teamList = IdentifiedArrayOf(
                uniqueElements: teamData.data.map {
                    TeamFeature.State(
                        id: uuid(),
                        team: $0
                    )
                }
            )
            state.dataLoadingStatus = .loading
            return .none
            
        case .onAppear:
            return .run { send in
                await send (
                    .fetchTeamResponse(
                        TaskResult { try await MatchScoresClient.liveValue.fetchTeams()
                        }
                    )
                )
            }
        case .team:
            return .none
        }
    }
}</code></pre></div><p>And then finally we define the view that displays the feature. It holds onto a <strong>StoreOf&lt;TeamListFeature&gt;</strong> so that it can observe all changes to the state and re-render, and we can send all user actions to the store so that state changes. ForEachStore loops over a store&#8217;s collection with a store scoped to the domain of each element. This allows us to extract and modularize an element&#8217;s view and avoid concerns around collection index math and parent-child store communication:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;d9f837b9-ead4-46b5-852c-ce69f22b93b3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">struct TeamListView: View {
    let store: StoreOf&lt;TeamListFeature&gt;
    
    private let columns = Array(
        repeating: GridItem( .flexible()),
        count: 2
    )
    
    var body: some View {
        WithViewStore(self.store, observe: { $0 }) { viewStore in
            ZStack{
                ScrollView {
                    LazyVGrid(columns: columns,
                              spacing: 16) {
                        ForEachStore(
                            self.store.scope(
                                state: \.teamList,
                                action: TeamListFeature.Action.team(id:action:)
                            )
                        ) {
                            TeamView(store: $0)
                        }
                    }
                              .padding()
                              .accessibilityIdentifier("peopleGrid")
                }
                .refreshable {
                    viewStore.send(.onAppear)
                }
            }
            .navigationTitle("Teams")
            .onAppear {
                viewStore.send(.onAppear)
            }
        }
    }
}</code></pre></div><p>&#128118;&#127996;&#128118;&#127996; To implement this child (grand-child of RootFeature or child of TeamListFeature) feature we create a new type that will house the domain and behavior of the child feature by conforming to Reducer:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;fe4a6de9-a0d7-4b5c-a0a7-b2c5f522cfac&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">struct TeamFeature: Reducer {
    struct State: Equatable, Identifiable {
        let id: UUID
        let team: TeamData
    }
    enum Action: Equatable { }
    
    func reduce(into state: inout State, action: Action) -&gt; Effect&lt;Action&gt; { }
}</code></pre></div><p>And then we define the view that displays the feature. It holds onto a <strong>StoreOf&lt;TeamFeature&gt;</strong> so that it can observe all changes to the state and re-render, and we can send all user actions to the store so that state changes. WithViewStore is a view helper that transforms a Store into a ViewStore so that its state can be observed by a view builder:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;52208574-eab4-4260-a834-ad115150c57d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">import SwiftUI
import ComposableArchitecture

struct TeamView: View {
    let store: StoreOf&lt;TeamFeature&gt;
    
    var body: some View {
        WithViewStore(self.store, observe: {$0} ) { viewStore in
            VStack(spacing: .zero) {
                
                background
                
                VStack(alignment: .leading) {
                    Text(viewStore.team.fullName)
                        .foregroundColor(Theme.text)
                        .font(
                            .system(.largeTitle, design: .rounded)
                        )
                        .background(
                            Image(avatars[viewStore.team.id - 1])
                                .opacity(0.4)
                                .aspectRatio(contentMode: .fill)
                        )
                }
                .frame(maxWidth: .infinity, alignment: .leading)
                .frame(height: 150.0)
                .padding(.horizontal, 8)
                .padding(.vertical, 5)
                .background(Theme.detailBackground)
                
                VStack {
                    PillView(id: viewStore.team.id)
                        .padding(.leading, 10)
                        .padding(.top, 10)
                }
                
            }
            .clipShape(
                RoundedRectangle(cornerRadius: 16,
                                 style: .continuous)
            )
            .shadow(color: Theme.text.opacity(0.1),
                    radius: 2,
                    x: 0,
                    y: 1)
            .edgesIgnoringSafeArea([.top, .leading, .trailing])
        }
    }
}</code></pre></div><h2>Result:</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!UeA8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F393e7d0b-422a-44bf-82d1-dec74a182d38_296x640.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!UeA8!,w_424,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F393e7d0b-422a-44bf-82d1-dec74a182d38_296x640.gif 424w, https://substackcdn.com/image/fetch/$s_!UeA8!,w_848,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F393e7d0b-422a-44bf-82d1-dec74a182d38_296x640.gif 848w, https://substackcdn.com/image/fetch/$s_!UeA8!,w_1272,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F393e7d0b-422a-44bf-82d1-dec74a182d38_296x640.gif 1272w, https://substackcdn.com/image/fetch/$s_!UeA8!,w_1456,c_limit,f_webp,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F393e7d0b-422a-44bf-82d1-dec74a182d38_296x640.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!UeA8!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F393e7d0b-422a-44bf-82d1-dec74a182d38_296x640.gif" width="320" height="691.8918918918919" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/393e7d0b-422a-44bf-82d1-dec74a182d38_296x640.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:640,&quot;width&quot;:296,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:8424070,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://emredegirmenci.substack.com/i/194429706?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F393e7d0b-422a-44bf-82d1-dec74a182d38_296x640.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!UeA8!,w_424,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F393e7d0b-422a-44bf-82d1-dec74a182d38_296x640.gif 424w, https://substackcdn.com/image/fetch/$s_!UeA8!,w_848,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F393e7d0b-422a-44bf-82d1-dec74a182d38_296x640.gif 848w, https://substackcdn.com/image/fetch/$s_!UeA8!,w_1272,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F393e7d0b-422a-44bf-82d1-dec74a182d38_296x640.gif 1272w, https://substackcdn.com/image/fetch/$s_!UeA8!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F393e7d0b-422a-44bf-82d1-dec74a182d38_296x640.gif 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 aria-hidden="true" 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><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>You can find all the codes in <a href="https://github.com/emrdgrmnci/MatchScoresTCA">here</a>.</p><p></p>]]></content:encoded></item></channel></rss>