<?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 | https://x.com/emrdgrmnci | https://emredeg.bsky.social | https://github.com/emrdgrmnci | Download my apps: https://walk-mate.app]]></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>Tue, 09 Jun 2026 11:36:36 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[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 role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>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 role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">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 role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">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 role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">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 role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">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 role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">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="2054630122847354" src="https://embed.bsky.app/embed/did:plc:klsh7edzj3jmxucibyjqstb3/app.bsky.feed.post/3mldis5besk2g?id=2054630122847354" 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 role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>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 role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><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 role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><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 role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><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 role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">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 role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><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 role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">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 role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><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 role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><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 role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>&#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 role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><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 role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>You can find all the codes in <a href="https://github.com/emrdgrmnci/MatchScoresTCA">here</a>.</p><p></p>]]></content:encoded></item><item><title><![CDATA[VisionPro and the EV Charger Finder App]]></title><description><![CDATA[My Experience with VisionPro and the EV Charger Finder App at iOSKonf 24]]></description><link>https://emredegirmenci.substack.com/p/visionpro-and-the-ev-charger-finder</link><guid isPermaLink="false">https://emredegirmenci.substack.com/p/visionpro-and-the-ev-charger-finder</guid><dc:creator><![CDATA[Emre Degirmenci]]></dc:creator><pubDate>Thu, 16 Apr 2026 17:10:11 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!rVBz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4bdf3e49-5791-4195-8c2e-c9a73b63aeea_1400x1867.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently, I had the amazing opportunity to attend <a href="https://www.ioskonf.mk/">iOSKonf 24</a> and try out Apple&#8217;s latest product, VisionPro. It was an exciting experience to be among the first to test this cutting-edge technology. In this blog post, I want to share my journey and highlight the long-distance route planner feature of my EV charger finder app.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!rVBz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4bdf3e49-5791-4195-8c2e-c9a73b63aeea_1400x1867.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!rVBz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4bdf3e49-5791-4195-8c2e-c9a73b63aeea_1400x1867.webp 424w, https://substackcdn.com/image/fetch/$s_!rVBz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4bdf3e49-5791-4195-8c2e-c9a73b63aeea_1400x1867.webp 848w, https://substackcdn.com/image/fetch/$s_!rVBz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4bdf3e49-5791-4195-8c2e-c9a73b63aeea_1400x1867.webp 1272w, https://substackcdn.com/image/fetch/$s_!rVBz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4bdf3e49-5791-4195-8c2e-c9a73b63aeea_1400x1867.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!rVBz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4bdf3e49-5791-4195-8c2e-c9a73b63aeea_1400x1867.webp" width="1400" height="1867" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4bdf3e49-5791-4195-8c2e-c9a73b63aeea_1400x1867.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1867,&quot;width&quot;:1400,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:252656,&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/194429381?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4bdf3e49-5791-4195-8c2e-c9a73b63aeea_1400x1867.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_!rVBz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4bdf3e49-5791-4195-8c2e-c9a73b63aeea_1400x1867.webp 424w, https://substackcdn.com/image/fetch/$s_!rVBz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4bdf3e49-5791-4195-8c2e-c9a73b63aeea_1400x1867.webp 848w, https://substackcdn.com/image/fetch/$s_!rVBz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4bdf3e49-5791-4195-8c2e-c9a73b63aeea_1400x1867.webp 1272w, https://substackcdn.com/image/fetch/$s_!rVBz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4bdf3e49-5791-4195-8c2e-c9a73b63aeea_1400x1867.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 role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>                                                 First Touch with the Vision Pro :D</p><h2>Experience at <a href="https://x.com/iOSKonf">iOSKonf</a> 24:</h2><p>iOSKonf 24 was a remarkable event that brought together developers, enthusiasts, and industry experts from around the world. The conference was filled with insightful sessions, engaging discussions, and hands-on experiences with the latest advancements in iOS development. One of the highlights of the event was the chance to get my hands on VisionPro.</p><h2>Testing the EV Charger Finder App:</h2><p>As a passionate advocate for sustainable transportation, I have been developing an EV charger finder application that aims to enhance the accessibility and convenience of electric vehicle charging. One of the main functionalities of the application is a long-distance route planner, which assists EV owners in planning their trips by identifying charging stations along their route.</p><p>During my time at iOSKonf 24, I had the opportunity to put this feature to the test. With VisionPro&#8217;s advanced capabilities, the route planner provided accurate and real-time information about nearby charging stations, their availability, and compatibility with my EV model. This feature proved to be invaluable, especially during long-distance trips where finding reliable charging infrastructure is crucial.</p><h2>Conclusion:</h2><p>In conclusion, my experience with VisionPro and the EV charger finder app at iOSKonf 24 was nothing short of amazing. The combination of Apple&#8217;s latest technology and my app&#8217;s long-distance route planner feature has the potential to revolutionize the way electric vehicle owners plan their journeys. I am grateful to iOSKonf and the organizers for providing me with this opportunity, and I look forward to further enhancing my app&#8217;s capabilities based on the valuable feedback I received.</p><p>Stay tuned for more updates and exciting developments in the world of EV charging and route planning!<br><br>Download here: <a href="https://apple.co/3NWejUz">EV Charge Stations Map</a></p><p>Here is the demo video of the EV Charger Finder App on VisionPro:</p><div id="youtube2-flGjG5D-o00" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;flGjG5D-o00&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/flGjG5D-o00?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[Migrating from UserDefaults to iCloud: A Practical Guide for iOS Developers]]></title><description><![CDATA[Photo by Art Wall - Kittenprint on Unsplash]]></description><link>https://emredegirmenci.substack.com/p/migrating-from-userdefaults-to-icloud</link><guid isPermaLink="false">https://emredegirmenci.substack.com/p/migrating-from-userdefaults-to-icloud</guid><dc:creator><![CDATA[Emre Degirmenci]]></dc:creator><pubDate>Thu, 16 Apr 2026 17:06:55 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!vUOv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae47a668-1740-48d7-9f95-bf4e706de36d_5616x3744.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_!vUOv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae47a668-1740-48d7-9f95-bf4e706de36d_5616x3744.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vUOv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae47a668-1740-48d7-9f95-bf4e706de36d_5616x3744.jpeg 424w, https://substackcdn.com/image/fetch/$s_!vUOv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae47a668-1740-48d7-9f95-bf4e706de36d_5616x3744.jpeg 848w, https://substackcdn.com/image/fetch/$s_!vUOv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae47a668-1740-48d7-9f95-bf4e706de36d_5616x3744.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!vUOv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae47a668-1740-48d7-9f95-bf4e706de36d_5616x3744.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!vUOv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae47a668-1740-48d7-9f95-bf4e706de36d_5616x3744.jpeg" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ae47a668-1740-48d7-9f95-bf4e706de36d_5616x3744.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2335714,&quot;alt&quot;:null,&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;:&quot;https://emredegirmenci.substack.com/i/194428671?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae47a668-1740-48d7-9f95-bf4e706de36d_5616x3744.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!vUOv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae47a668-1740-48d7-9f95-bf4e706de36d_5616x3744.jpeg 424w, https://substackcdn.com/image/fetch/$s_!vUOv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae47a668-1740-48d7-9f95-bf4e706de36d_5616x3744.jpeg 848w, https://substackcdn.com/image/fetch/$s_!vUOv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae47a668-1740-48d7-9f95-bf4e706de36d_5616x3744.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!vUOv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fae47a668-1740-48d7-9f95-bf4e706de36d_5616x3744.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 role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Photo by <a href="https://unsplash.com/@artwall_hd?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Art Wall - Kittenprint</a> on <a href="https://unsplash.com/photos/black-and-silver-turntable-on-black-table-9Wq1HpghQ4A?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></p><p>When users uninstall your app, all their data stored in <em><strong>UserDefaults</strong></em> is lost forever. For a walking app like <strong><a href="https://apps.apple.com/us/app/walk-mate-weight-loss/id6739468121">Walk Mate</a></strong>, this means losing:</p><p>- Years of walking statistics</p><p>- Favorite routes they&#8217;ve curated</p><p>- Milestones and achievements unlocked</p><p>- Personal preferences and settings</p><p>This was unacceptable. I needed a solution that would persist user data even after app uninstallation, while seamlessly syncing across all their devices.</p><h2>The Solution: A Hybrid iCloud Approach</h2><p>After evaluating several options (<strong><a href="https://www.pointfree.co/blog/posts/184-sqlitedata-1-0-an-alternative-to-swiftdata-with-cloudkit-sync-and-sharing">SQLiteData</a></strong> with CloudKit, Core Data with CloudKit, and direct CloudKit), I settled on a <strong>hybrid approach</strong> that balances simplicity, reliability, and performance:</p><p>1. <strong>iCloud Key-Value Store (KVS)</strong> for small, frequently accessed data (milestones, medal counts, stats)</p><p>2. <strong>File-based iCloud Storage</strong> for larger, structured data (routes, favorites, avoided segments)</p><p>This approach gives us:</p><p>&#9989; Automatic iCloud sync across devices</p><p>&#9989; Data persistence even after app uninstallation</p><p>&#9989; Simple, straightforward API</p><p>&#9989; No complex database migrations</p><p>&#9989; Built-in conflict resolution</p><h2>Architecture Overview</h2><h3>iCloud Key-Value Store Wrapper</h3><p>For small data like milestone progress and medal counts, I created a wrapper around <strong><a href="https://developer.apple.com/documentation/foundation/nsubiquitouskeyvaluestore">NSUbiquitousKeyValueStore</a></strong>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;b884669a-ea1b-4b3e-b74a-b9674a9651ee&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">class iCloudKeyValueStore {
    static let shared = iCloudKeyValueStore()
    
    private let store = NSUbiquitousKeyValueStore.default
    private let migrationKeyPrefix = "iCloudMigration_"
    
    private init() {
        // Listen for iCloud sync notifications
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(handleiCloudSync),
            name: NSUbiquitousKeyValueStore.didChangeExternallyNotification,
            object: store
        )
    }
    
    /// Migrates a key from UserDefaults to iCloud KVS if not already migrated
    func migrateIfNeeded(key: String, from userDefaults: UserDefaults = .standard) -&gt; Bool {
        let migrationKey = migrationKeyPrefix + key
        
        // Check if already migrated
        if store.bool(forKey: migrationKey) {
            return false
        }
        
        // Check if data exists in UserDefaults
        guard let value = userDefaults.object(forKey: key) else {
            store.set(true, forKey: migrationKey)
            store.synchronize()
            return false
        }
        
        // Migrate the value
        store.set(value, forKey: key)
        store.set(true, forKey: migrationKey)
        store.synchronize()
        
        AppLogger.info("Migrated key '\(key)' from UserDefaults to iCloud KVS")
        return true
    }
    
    // Convenience methods for common types
    func set(_ value: Int, forKey key: String) {
        store.set(Int64(value), forKey: key)
        store.synchronize()
    }
    
    func integer(forKey key: String) -&gt; Int {
        return Int(store.longLong(forKey: key))
    }
    
    // ... similar methods for String, Double, Date, [String], etc.
}</code></pre></div><p><strong>Key Features:</strong></p><p>- Automatic directory creation</p><p>- Handles files that aren&#8217;t downloaded yet</p><p>- One-time migration from UserDefaults</p><p>- Fallback support for offline scenarios</p><h3>Implementation Example: Milestone Storage</h3><p>Here&#8217;s how I migrated milestone data to use iCloud KVS:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;3ef3c245-b2d4-4045-96ea-e894a1dbf991&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">class MilestoneStorage {
    private static let store = iCloudKeyValueStore.shared
    private static let milestonesKey = "milestones"
    private static let totalWalksKey = "totalWalks"
    private static let totalDistanceKey = "totalDistanceWalked"
    /// Migrates all milestone data from UserDefaults to iCloud KVS
    static func migrateFromUserDefaults() {
        let userDefaults = UserDefaults.standard
        _ = store.migrateIfNeeded(key: milestonesKey, from: userDefaults)
        _ = store.migrateIfNeeded(key: totalWalksKey, from: userDefaults)
        _ = store.migrateIfNeeded(key: totalDistanceKey, from: userDefaults)
    }
    static func saveMilestones(_ milestones: [Milestone]) {
        do {
            let data = try JSONEncoder().encode(milestones)
            store.set(data, forKey: milestonesKey)
        } catch {
            AppLogger.error("Failed to save milestones", error: error)
        }
    }
    static func loadMilestones() -&gt; [Milestone] {
        guard let data = store.data(forKey: milestonesKey) else {
            return initializeDefaultMilestones()
        }
        do {
            return try JSONDecoder().decode([Milestone].self, from: data)
        } catch {
            AppLogger.error("Failed to load milestones", error: error)
            return initializeDefaultMilestones()
        }
    }
    static func incrementTotalWalks() {
        let current = store.integer(forKey: totalWalksKey)
        store.set(current + 1, forKey: totalWalksKey)
    }
}</code></pre></div><h3>Implementation Example: Route Storage</h3><p>For routes, I use file-based storage:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;7f0f7033-6ca7-4f99-9360-e6b1ae350eb2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">class RouteStorage {
    private static let fileStorage = iCloudFileStorage.shared
    private static let routesKey = "routes"
    private static let fileName = "routes.json"
    /// Migrates routes from UserDefaults to iCloud file
    static func migrateFromUserDefaults() {
        _ = fileStorage.migrateArrayIfNeeded(
            Route.self,
            from: routesKey,
            to: fileName
        )
    }
    static func saveRoutes(_ routes: [Route]) {
        do {
            try fileStorage.save(routes, to: fileName)
        } catch {
            AppLogger.error("Failed to save routes to iCloud", error: error)
            // Fallback to UserDefaults if iCloud is unavailable
            do {
                let data = try JSONEncoder().encode(routes)
                UserDefaults.standard.set(data, forKey: routesKey)
            } catch {
                AppLogger.error("Failed to save routes to UserDefaults fallback", error: error)
            }
        }
    }
    static func loadRoutes() -&gt; [Route] {
        // Try loading from iCloud first
        let routes = fileStorage.loadArray(Route.self, from: fileName)
        if !routes.isEmpty {
            return routes
        }
        // Fallback to UserDefaults if iCloud file doesn't exist
        guard let data = UserDefaults.standard.data(forKey: routesKey) else {
            return []
        }
        do {
            return try JSONDecoder().decode([Route].self, from: data)
        } catch {
            AppLogger.error("Failed to load routes from UserDefaults fallback", error: error)
            return []
        }
    }
}</code></pre></div><h3>App Launch Integration</h3><p>I trigger migrations once on app launch:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;581b5167-6f43-4382-9d7f-3751afa443b0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">@main
struct WalkMateApp: App {
    init() {
        MilestoneStorage.migrateFromUserDefaults()
        MedalStorage.migrateFromUserDefaults()
        RouteStorage.migrateFromUserDefaults()
        FavoriteRouteStorage.migrateFromUserDefaults()
        AvoidedRouteManager.migrateFromUserDefaults()
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}</code></pre></div><h2>Key Lessons Learned</h2><h3>1. Migration Tracking is Critical</h3><p>Always track which keys have been migrated to prevent duplicate migrations. I use a simple boolean flag in iCloud KVS:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;2db5bc5a-0aef-4a2c-9823-be812b0dffc6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">let migrationKey = "iCloudMigration_\(originalKey)"
if store.bool(forKey: migrationKey) {
    return false
}</code></pre></div><h3>2. Handle Offline Scenarios</h3><p>iCloud might not always be available. Always provide a fallback to UserDefaults:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;59c583b6-7f9b-4077-af09-a4ca469aee13&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">static func saveRoutes(_ routes: [Route]) {
    do {
        try fileStorage.save(routes, to: fileName)
    } catch {
        let data = try JSONEncoder().encode(routes)
        UserDefaults.standard.set(data, forKey: routesKey)
    }
}</code></pre></div><h3>3. Files May Not Be Downloaded Yet</h3><p>When loading files from iCloud, they might exist in the cloud but not be downloaded locally yet. Always check the download status:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;5248fcb3-8403-4536-8fd7-32b11715ddca&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">if let status = resourceValues.ubiquitousItemDownloadingStatus,
   status == .notDownloaded {
    try? fileManager.startDownloadingUbiquitousItem(at: fileURL)
    return nil
}</code></pre></div><h3>4. Use Atomic Writes</h3><p>Always use <strong>.atomic</strong> option when writing files to prevent corruption:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;ff70e34c-85dc-4d41-8b6a-a324c833213d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">try jsonData.write(to: fileURL, options: .atomic)</code></pre></div><h3>5. Synchronize After Writes</h3><p>For iCloud KVS, always call <strong>synchronize()</strong> after writes to ensure data is synced:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;3d5458c0-3ffd-4787-afe2-41272f673bf6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">func set(_ value: Int, forKey key: String) {
    store.set(Int64(value), forKey: key)
    store.synchronize() // Important!
}</code></pre></div><h2>Benefits of This Approach</h2><p><strong>1. Simple API:</strong> No complex database queries, just save/load methods</p><p><strong>2. Automatic Sync:</strong> iCloud handles syncing automatically</p><p><strong>3. Data Persistence:</strong> Data survives app uninstallation</p><p><strong>4. Cross-Device:</strong> Works seamlessly across iPhone, iPad, and Mac</p><p><strong>5. No Server Costs:</strong> Uses Apple&#8217;s free iCloud storage</p><p><strong>6. Privacy:</strong> Data is encrypted and stored in user&#8217;s iCloud account</p><h2>When to Use Each Approach</h2><p><strong>Use iCloud Key-Value Store for:</strong></p><p>- Small data (&lt; 1 MB total)</p><p>- Simple key-value pairs</p><p>- Frequently accessed data</p><p>- Settings and preferences</p><p>- Counters and statistics</p><p><strong>Use File-Based Storage for:</strong></p><p>- Larger data structures</p><p>- Complex objects and arrays</p><p>- Data that changes infrequently</p><p>- Files that users might want to access outside the app</p><h2>Conclusion</h2><p>Migrating from UserDefaults to iCloud storage doesn&#8217;t have to be complicated. By using a hybrid approach with iCloud Key-Value Store for small data and file-based storage for larger data, I achieved:</p><p>&#9989; Zero data loss on app uninstallation</p><p>&#9989; Seamless cross-device sync</p><p>&#9989; Simple, maintainable code</p><p>&#9989; Automatic migration from existing UserDefaults data</p><p>The implementation is straightforward, and the user experience is significantly improved. Users can now uninstall and reinstall the app without losing their progress, and their data automatically syncs across all their devices.</p><p>This migration was implemented for <strong><a href="https://apps.apple.com/us/app/walk-mate-weight-loss/id6739468121">WalkMate</a></strong>, a daily route generator app that helps users discover new walking routes. The app uses <strong><a href="https://www.pointfree.co/collections/composable-architecture">The Composable Architecture (TCA)</a></strong> and SwiftUI, with data persistence handled through iCloud.</p><p><strong>Resources:</strong></p><ul><li><p><a href="https://developer.apple.com/icloud/">Apple&#8217;s iCloud Documentation</a></p></li><li><p><a href="https://developer.apple.com/documentation/foundation/nsubiquitouskeyvaluestore">NSUbiquitousKeyValueStore Reference</a></p></li><li><p><a href="https://developer.apple.com/documentation/foundation/filemanager/1411653-url">FileManager iCloud Methods</a></p></li></ul>]]></content:encoded></item><item><title><![CDATA[Why I Chose Game Center Over a Custom Backend for Walk Mate’s Social Features]]></title><description><![CDATA[Photo by Element5 Digital on Unsplash]]></description><link>https://emredegirmenci.substack.com/p/why-i-chose-game-center-over-a-custom</link><guid isPermaLink="false">https://emredegirmenci.substack.com/p/why-i-chose-game-center-over-a-custom</guid><dc:creator><![CDATA[Emre Degirmenci]]></dc:creator><pubDate>Thu, 16 Apr 2026 16:59:52 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!FzUZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc68fa943-9a75-4214-9f4d-77d4d5187800_6016x4016.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_!FzUZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc68fa943-9a75-4214-9f4d-77d4d5187800_6016x4016.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!FzUZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc68fa943-9a75-4214-9f4d-77d4d5187800_6016x4016.jpeg 424w, https://substackcdn.com/image/fetch/$s_!FzUZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc68fa943-9a75-4214-9f4d-77d4d5187800_6016x4016.jpeg 848w, https://substackcdn.com/image/fetch/$s_!FzUZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc68fa943-9a75-4214-9f4d-77d4d5187800_6016x4016.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!FzUZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc68fa943-9a75-4214-9f4d-77d4d5187800_6016x4016.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!FzUZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc68fa943-9a75-4214-9f4d-77d4d5187800_6016x4016.jpeg" width="1456" height="972" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c68fa943-9a75-4214-9f4d-77d4d5187800_6016x4016.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:972,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1608535,&quot;alt&quot;:null,&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;:&quot;https://emredegirmenci.substack.com/i/194427932?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc68fa943-9a75-4214-9f4d-77d4d5187800_6016x4016.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!FzUZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc68fa943-9a75-4214-9f4d-77d4d5187800_6016x4016.jpeg 424w, https://substackcdn.com/image/fetch/$s_!FzUZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc68fa943-9a75-4214-9f4d-77d4d5187800_6016x4016.jpeg 848w, https://substackcdn.com/image/fetch/$s_!FzUZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc68fa943-9a75-4214-9f4d-77d4d5187800_6016x4016.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!FzUZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc68fa943-9a75-4214-9f4d-77d4d5187800_6016x4016.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 role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Photo by <a href="https://unsplash.com/@element5digital?utm_source=medium&amp;utm_medium=referral">Element5 Digital</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></p><p>When building <strong><a href="https://walk-mate.app/apps/walkmate.html">Walk Mate</a></strong>, a daily route generator app that gamifies walking with medals and achievements, I faced a critical decision: build a <strong>custom leaderboard</strong> and <strong>achievements</strong> backend, or use <strong><a href="https://developer.apple.com/documentation/gamekit/">Apple&#8217;s Game Center (GameKit)</a></strong>?</p><p>Many developers suggested building a custom solution for more control and flexibility. But I went with <strong>Game Center</strong>, and here&#8217;s why it was the right choice, plus a complete implementation guide with code examples.</p><h2>Why Game Center?</h2><ul><li><p><em><strong>Native iOS Integration:</strong></em> Game Center is built into iOS. Users already have accounts, and the integration feels native to the platform.</p></li><li><p><em><strong>Zero Backend Maintenance:</strong></em> No servers to manage, no databases to maintain, no scaling concerns. Apple handles everything.</p></li><li><p><em><strong>Built-in Social Features:</strong></em> Friend lists, challenges, and multiplayer support come out of the box.</p></li><li><p><em><strong>User Trust:</strong></em> Users are already familiar with Game Center, and it&#8217;s a trusted Apple service.</p></li><li><p><em><strong>Cost-Effective:</strong></em> Free to use, with no infrastructure costs.</p></li></ul><p>Sometimes the simplest solution is the best!</p><h2>Architecture Overview</h2><p>I created a centralized <strong>GameCenterManager</strong> class using Swift&#8217;s <strong><a href="https://developer.apple.com/documentation/Observation/Observable">@Observable</a></strong><a href="https://developer.apple.com/documentation/Observation/Observable">macro</a> (iOS 17+) to manage all Game Center interactions.</p><p>This singleton class handles:</p><ul><li><p><em><strong>Authentication</strong></em> &#128273;</p></li><li><p><em><strong>Leaderboard submissions and loading</strong></em> &#128081;</p></li><li><p><em><strong>Achievement unlocking &#128275;</strong></em></p></li><li><p><em><strong>Challenge management &#9939;&#65039;&#8205;&#128165;</strong></em></p></li><li><p><em><strong>Friend loading &#128109;</strong></em></p></li></ul><p>Let&#8217;s dive into the implementation &#129343;</p><h3>1. Setting Up Game Center Manager</h3><p>First, let&#8217;s create the manager class:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;d19712d0-3a5a-4802-a71d-b330e375eede&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">import Foundation
import GameKit
import Observation
import OSLog

@MainActor
@Observable
class GameCenterManager {
    static let shared = GameCenterManager()
    
    var isAuthenticated = false
    var localPlayer: GKLocalPlayer?
    var authenticationError: Error?
    var pendingChallenges: [GKChallenge] = []
    
    private let leaderboardID = "walkmate_total_medals"
    
    private init() {
        authenticatePlayer()
    }
}</code></pre></div><p>The <strong>@Observable</strong> macro makes this class automatically observable, perfect for SwiftUI views that need to react to authentication state changes.</p><h3>2. Authentication</h3><p>Game Center authentication is straightforward. The system handles the UI, and we just need to set up the authentication handler:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;f5cca134-7bfb-4e12-9072-28f6a565d246&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">func authenticatePlayer() {
    localPlayer = GKLocalPlayer.local
    
    localPlayer?.authenticateHandler = { [weak self] viewController, error in
        guard let self = self else { return }
        
        if let viewController = viewController {
            // Present authentication view controller
            if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
               let rootViewController = windowScene.windows.first?.rootViewController {
                rootViewController.present(viewController, animated: true)
            }
            return
        }
        
        if let error = error {
            self.authenticationError = error
            self.isAuthenticated = false
            return
        }
        
        if self.localPlayer?.isAuthenticated == true {
            self.isAuthenticated = true
            // Sync existing data after authentication
            Task {
                await self.syncAllData()
                await self.loadChallenges()
            }
        }
    }
}</code></pre></div><p>The authentication handler is called automatically by the system. If a view controller is provided, we present it. If there&#8217;s an error, we handle it. If authentication succeeds, we sync our app&#8217;s data to Game Center.</p><h3>3. Leaderboards</h3><p><em><strong>Submitting Scores:</strong></em></p><p>Submitting scores to a leaderboard is simple with the modern async/await API:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;9aa4ccec-b15e-4ec6-8a5c-e8d7ace15604&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">func submitMedalsToLeaderboard(_ medalCount: Int) async {
    guard isAuthenticated, let localPlayer = localPlayer else {
        return
    }
    
    do {
        try await GKLeaderboard.submitScore(
            medalCount,
            context: 0,
            player: localPlayer,
            leaderboardIDs: [leaderboardID]
        )
    } catch {
        AppLogger.error("Failed to submit score", error: error)
    }
}</code></pre></div><p><em><strong>Loading Leaderboard Entries:</strong></em></p><p>Loading leaderboard entries requires two steps: first load the leaderboard object, then load its entries:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;83e413f1-0904-4992-8dcc-dc6716f40404&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">func loadLeaderboard(limit: Int = 100) async throws -&gt; [GKLeaderboard.Entry] {
    guard isAuthenticated else {
        throw GameCenterError.notAuthenticated
    }
    
    // Load the leaderboard object
    let leaderboards = try await GKLeaderboard.loadLeaderboards(IDs: [leaderboardID])
    guard let leaderboard = leaderboards.first else {
        throw GameCenterError.leaderboardNotFound
    }
    
    // Load entries
    let result = try await leaderboard.loadEntries(
        for: .global,
        timeScope: .allTime,
        range: NSRange(location: 1, length: limit)
    )
    
    return result.1 // Returns the entries array
}</code></pre></div><p><em><strong>Displaying in SwiftUI:</strong></em></p><p>Here&#8217;s how we display the leaderboard in a SwiftUI view:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;e0ee5bd1-df74-4914-bd7f-de893ee0ec12&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">struct LeaderboardView: View {
    @State private var leaderboardEntries: [GKLeaderboard.Entry] = []
    @State private var isLoading = false

    private let gameCenterManager = GameCenterManager.shared    

    var body: some View {
        ScrollView {
            if isLoading {
                ProgressView()
            } else {
                ForEach(leaderboardEntries, id: \.player.gamePlayerID) { entry in
                    LeaderboardRowView(entry: entry)
                }
            }
        }
        .task {
            await loadLeaderboard()
        }
    }
    
    private func loadLeaderboard() async {
        isLoading = true
        do {
            leaderboardEntries = try await gameCenterManager.loadLeaderboard()
        } catch {}
        isLoading = false
    }
}</code></pre></div><h3>4. Achievements</h3><p><em><strong>Unlocking Achievements:</strong></em></p><p>When a player reaches a milestone, we unlock the corresponding achievement:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;6ed5c63d-447b-4418-9e0f-2fef3a1a3c13&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">func unlockAchievement(for milestoneType: MilestoneType) async {
    guard isAuthenticated else { return }
    
    guard let achievementID = achievementIDs[milestoneType] else {
        return
    }
    
    do {
        let achievement = GKAchievement(identifier: achievementID)
        achievement.percentComplete = 100.0
        achievement.showsCompletionBanner = true
        
        try await GKAchievement.report([achievement])
    } catch {
        AppLogger.error("Failed to unlock achievement", error: error)
    }
}</code></pre></div><p><strong>Incremental Achievements:</strong></p><p>For achievements that track progress (like <strong>&#8220;Walk 100km total&#8221;</strong>), we can update progress incrementally:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;8e10659e-3c17-4318-92d5-fae0dcef5ed9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">func updateAchievementProgress(for milestoneType: MilestoneType, progress: Double) async {
    guard isAuthenticated else { return }
    
    guard let achievementID = achievementIDs[milestoneType] else {
        return
    }
    
    // Check if already completed
    let achievements = try await GKAchievement.loadAchievements()
    if let existingAchievement = achievements.first(where: { $0.identifier == achievementID }),
       existingAchievement.percentComplete &gt;= 100.0 {
        return
    }
    
    let achievement = GKAchievement(identifier: achievementID)
    achievement.percentComplete = min(100.0, progress)
    achievement.showsCompletionBanner = progress &gt;= 100.0
    
    try await GKAchievement.report([achievement])
}</code></pre></div><h3>5. Challenges (The Tricky Part)</h3><p>Game Center&#8217;s challenge feature is powerful but has a deprecated UI. The <em><strong>`challengeComposeController`</strong></em> method presents an outdated interface that doesn&#8217;t match modern iOS design.</p><p><em><strong>The Problem:</strong></em></p><p>The deprecated <em><strong>`challengeComposeController`</strong></em> method shows an old, ugly UI that doesn&#8217;t fit modern apps.</p><p><em><strong>The Solution (Custom SwiftUI UI):</strong></em></p><p>I built a custom SwiftUI interface for challenge composition, then use the deprecated API internally with preselected friends:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;d4a17773-daa8-4653-8d88-0cc0a05f9187&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">struct ChallengeComposeView: View {
    let friends: [GKPlayer]
    let defaultMessage: String
    let onSend: ([GKPlayer], String) -&gt; Void
    let onCancel: () -&gt; Void
    
    @State private var selectedFriendIDs: Set&lt;String&gt; = []
    @State private var message: String
    @State private var searchText = ""
    
    var body: some View {
        NavigationStack {
            VStack(spacing: AppConstants.UI.defaultRadius) {
                // Message Section
                VStack(alignment: .leading, spacing: AppConstants.UI.standardSpacing) {
                    Text(String(localized: "Challenge Message"))
                        .heading3()
                    
                    TextEditor(text: $message)
                        .frame(minHeight: AppConstants.minHeight)
                        .padding(AppConstants.UI.smallSpacing)
                        .background(Color(.secondarySystemGroupedBackground))
                        .clipShape(.rect(cornerRadius: AppConstants.UI.standardRadius))
                    
                    Text(String(localized: "\(message.count) characters"))
                        .caption()
                }
                .padding()
                
                Divider()
                
                // Friends Selection Section
                VStack(alignment: .leading, spacing: AppConstants.UI.standardSpacing) {
                    Text(String(localized: "Select Friends"))
                        .heading3()
                    
                    // Search Bar
                    HStack {
                        Image(systemName: UIConstants.SystemImages.magnifyingGlass)
                        TextField(String(localized: "Search friends", text: $searchText))
                    }
                    .padding(12)
                    .background(Color(.secondarySystemGroupedBackground))
                    .clipShape(.rect(cornerRadius: AppConstants.UI.standardRadius))
                    
                    // Friends List
                    ScrollView {
                        LazyVStack(spacing: 8) {
                            ForEach(filteredFriends, id: \.gamePlayerID) { friend in
                                FriendSelectionRow(
                                    friend: friend,
                                    isSelected: selectedFriendIDs.contains(friend.gamePlayerID),
                                    onToggle: {
                                        if selectedFriendIDs.contains(friend.gamePlayerID) {
                                            selectedFriendIDs.remove(friend.gamePlayerID)
                                        } else {
                                            selectedFriendIDs.insert(friend.gamePlayerID)
                                        }
                                    }
                                )
                            }
                        }
                    }
                }
                .padding()
            }
            .navigationTitle("Challenge Friends")
            .toolbar {
                ToolbarItem(placement: .topBarLeading) {
                    Button("Cancel") { onCancel() }
                }
                ToolbarItem(placement: .topBarTrailing) {
                    Button("Send") {
                        let selectedFriends = friends.filter { 
                            selectedFriendIDs.contains($0.gamePlayerID) 
                        }
                        onSend(selectedFriends, message)
                    }
                    .disabled(selectedFriendIDs.isEmpty)
                }
            }
        }
    }
}</code></pre></div><p><em><strong>Sending Challenges:</strong></em></p><p>The manager method uses the deprecated API internally but with preselected friends, minimizing exposure to the old UI:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;24f31d90-7b00-43b4-95e3-ed73f9e184f3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">func sendScoreChallenge(
    score: Int, 
    message: String, 
    players: [GKPlayer],
    completion: @escaping (Bool, [GKPlayer]?) -&gt; Void
) async {
    guard isAuthenticated else {
        completion(false, nil)
        return
    }
    
    // Get leaderboard entry
    let leaderboards = try await GKLeaderboard.loadLeaderboards(IDs: [leaderboardID])
    guard let leaderboard = leaderboards.first else {
        completion(false, nil)
        return
    }
    
    let result = try await leaderboard.loadEntries(
        for: .global,
        timeScope: .allTime,
        range: NSRange(location: 1, length: 1)
    )
    
    guard let entry = result.0 else {
        completion(false, nil)
        return
    }
    
    // Use deprecated method but with preselected players
    let controller = entry.challengeComposeController(
        withMessage: message,
        players: players, // Pre-selected from our custom UI
        completion: { viewController, didIssueChallenge, playersSentChallenge in
            viewController.dismiss(animated: true)
            completion(didIssueChallenge, playersSentChallenge)
        }
    )
    
    // Present the controller
    if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
       let rootViewController = windowScene.windows.first?.rootViewController {
        var topController = rootViewController
        while let presented = topController.presentedViewController {
            topController = presented
        }
        topController.present(controller, animated: true)
    }
}</code></pre></div><p>This approach gives users a modern UI for friend selection and message composition, while still using Game Center&#8217;s API for the actual challenge sending.</p><h3>6. Loading and Managing Challenges</h3><p>Players can receive challenges from friends. Here&#8217;s how to load and display them:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;45da83bb-ba69-4022-8b5e-f38fab5e2cab&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">func loadChallenges() async -&gt; Error? {
    guard isAuthenticated else { return nil }
    
    do {
        let challenges = try await withCheckedThrowingContinuation { 
            (continuation: CheckedContinuation&lt;[GKChallenge], Error&gt;) in
            GKChallenge.loadReceivedChallenges { challenges, error in
                if let error = error {
                    continuation.resume(throwing: error)
                } else {
                    continuation.resume(returning: challenges ?? [])
                }
            }
        }
        
        await MainActor.run {
            self.pendingChallenges = challenges
        }
        
        return nil
    } catch {
        return error
    }
}</code></pre></div><p>Challenges are automatically accepted when the player beats the score or achievement. There&#8217;s no explicit accept method&#8230; Game Center handles it automatically!</p><h3>7. Data Synchronization</h3><p>When a user authenticates, we sync their existing app data to Game Center:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;8e08f82a-cbf4-48c9-981d-8833469009f3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">func syncAllData() async {
    guard isAuthenticated else { return }
    
    // Sync total medals to leaderboard
    let totalMedals = MedalStorage.getTotalMedalsCollected()
    await submitMedalsToLeaderboard(totalMedals)
    
    // Sync unlocked milestones as achievements
    let milestones = MilestoneStorage.loadMilestones()
    for milestone in milestones where milestone.unlocked {
        await unlockAchievement(for: milestone.type)
    }
}</code></pre></div><p>This ensures that existing players don&#8217;t lose their progress when they first sign in to Game Center.</p><h3>8. Best Practices and Tips</h3><p><em><strong>Error Handling:</strong></em></p><p>Game Center can return various error codes. Some are gentle (like <strong>&#8220;no challenges available&#8221;</strong>), so handle them gracefully:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;f4b71701-a7b3-46fd-a851-02940fe5c166&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">let nsError = error as NSError
if nsError.domain == "GKErrorDomain" {
    switch nsError.code {
    case 2, 6, 15, 38:
        // Gentle errors - treat as "no data available"
        return []
    default:
        throw error
    }
}</code></pre></div><p><em><strong>Testing:</strong></em></p><ul><li><p>Use <strong><a href="https://developer.apple.com/help/app-store-connect/configure-game-center/overview-of-testing-game-center/">sandbox Game Center accounts</a></strong> for testing</p></li><li><p>Challenges require the app to be <strong>approved</strong> in App Store Connect</p></li><li><p>Test with multiple accounts to verify friend functionality</p></li></ul><p><em><strong>App Store Connect Setup:</strong></em></p><p>Before your app goes live, you need to:</p><p>1. <strong>Create leaderboards</strong> in App Store Connect</p><p>2. <strong>Create achievements</strong> in App Store Connect</p><p>3. <strong>Submit your app version</strong> with Game Center items</p><p>4. <strong>Wait for approval</strong> (challenges become available after approval)</p><h3>Privacy</h3><p>Game Center requires a privacy description in your <strong>`Info.plist`</strong>:</p><pre><code>&lt;key&gt;NSGKFriendListUsageDescription&lt;/key&gt;
&lt;string&gt;Walk Mate needs access to your Game Center friends list to allow you to challenge friends and compete together.&lt;/string&gt;</code></pre><h3>Conclusion</h3><p>Game Center provided everything Walk Mate needed for social features without the complexity of a custom backend. The implementation was straightforward, and users get a native iOS experience.</p><p>The only challenge was the deprecated challenge UI, which I solved with a custom SwiftUI interface. This gives us the best of both worlds: modern design and Game Center&#8217;s robust infrastructure.</p><p>If you&#8217;re building an iOS app with social gaming features, give Game Center a serious look. It might be the simplest solution for your needs too.</p><p><strong>Walk Mate</strong> is available on the App Store.</p><p>Check it out at: (<a href="https://walk-mate.app/apps/walkmate.html">https://walk-mate.app/apps/walkmate.html</a>)</p>]]></content:encoded></item><item><title><![CDATA[Build a Smooth Overlapping Carousel in iOS with EDCarousel]]></title><description><![CDATA[If you ever tried to build a carousel in iOS using UICollectionView, you know it can be painful.]]></description><link>https://emredegirmenci.substack.com/p/build-a-smooth-overlapping-carousel</link><guid isPermaLink="false">https://emredegirmenci.substack.com/p/build-a-smooth-overlapping-carousel</guid><pubDate>Wed, 15 Apr 2026 21:14:30 GMT</pubDate><enclosure 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" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you ever tried to build a carousel in iOS using UICollectionView, you know it can be painful. You have to deal with custom flow layouts, scaling animations, content offsets, and snapping behavior. I built <a href="https://github.com/emrdgrmnci/EDCarousel.git">EDCarousel</a> to solve exactly this problem. In this post, I&#8217;ll walk you through what <a href="https://github.com/emrdgrmnci/EDCarousel.git">EDCarousel</a> does, how it works under the hood, and how you can add it to your project in minutes.</p><h2>What is EDCarousel?</h2><p><a href="https://github.com/emrdgrmnci/EDCarousel">EDCarousel</a> is a lightweight Swift library that gives you a carousel-style UICollectionView layout with overlapping items and smooth scaling animations. The center item appears at full scale, while side items shrink, fade, and shift slightly giving you that polished carousel look you see in many popular apps.</p><p>It&#8217;s built on top of UICollectionViewFlowLayout, so it works with the UIKit patterns you already know. No need to learn a new framework.</p><p>Here&#8217;s what it looks like in action:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!d73-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff34692b9-7fee-44ef-85bc-ed93207cae5d_295x640.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!d73-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff34692b9-7fee-44ef-85bc-ed93207cae5d_295x640.gif 424w, https://substackcdn.com/image/fetch/$s_!d73-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff34692b9-7fee-44ef-85bc-ed93207cae5d_295x640.gif 848w, https://substackcdn.com/image/fetch/$s_!d73-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff34692b9-7fee-44ef-85bc-ed93207cae5d_295x640.gif 1272w, https://substackcdn.com/image/fetch/$s_!d73-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff34692b9-7fee-44ef-85bc-ed93207cae5d_295x640.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!d73-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff34692b9-7fee-44ef-85bc-ed93207cae5d_295x640.gif" width="320" height="694.2372881355932" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f34692b9-7fee-44ef-85bc-ed93207cae5d_295x640.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:640,&quot;width&quot;:295,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:4617361,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://emredegirmenci.substack.com/i/194344556?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff34692b9-7fee-44ef-85bc-ed93207cae5d_295x640.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_!d73-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff34692b9-7fee-44ef-85bc-ed93207cae5d_295x640.gif 424w, https://substackcdn.com/image/fetch/$s_!d73-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff34692b9-7fee-44ef-85bc-ed93207cae5d_295x640.gif 848w, https://substackcdn.com/image/fetch/$s_!d73-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff34692b9-7fee-44ef-85bc-ed93207cae5d_295x640.gif 1272w, https://substackcdn.com/image/fetch/$s_!d73-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff34692b9-7fee-44ef-85bc-ed93207cae5d_295x640.gif 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Features</h2><ul><li><p>Smooth scale, alpha, and shift animations while scrolling</p></li><li><p>Two spacing modes: fixed spacing or overlapping items</p></li><li><p>Custom page control with image-based indicators</p></li><li><p>Type-safe UICollectionView cell registration and dequeuing</p></li><li><p>Supports both Swift Package Manager and CocoaPods</p></li><li><p>Works with iOS 14.0+ and Swift 5.9+</p></li></ul><h2>Installation</h2><h3>Swift Package Manager</h3><p>In Xcode, go to File &#8594; Add Package Dependencies and enter:</p><pre><code><a href="https://github.com/emrdgrmnci/EDCarousel.git">https://github.com/emrdgrmnci/EDCarousel.git</a></code></pre><p>Or add it directly to your Package.swift:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;a49fc63d-3631-4db9-9b07-871dc34a9541&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">dependencies: [
    .package(url: "https://github.com/emrdgrmnci/EDCarousel.git", from: "1.0.0")
]</code></pre></div><h2>CocoaPods</h2><p>Add this to your Podfile:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;6ff0dbd3-d40d-4e4d-b035-06ab1b27adab&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">pod 'EDCarousel'</code></pre></div><p>Then run:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;c4bfb71f-ecde-466a-9ab0-0b99e389f638&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">pod install</code></pre></div><h2>How to Use It</h2><h3>Step 1: Set Up the Carousel Layout</h3><p>You can set the layout either from Interface Builder or from code. I&#8217;ll show the code approach here since it&#8217;s more clear:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;9c11338b-5e92-4197-b53c-fc950362d1fb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">import EDCarousel

let layout = CarouselFlowLayout()
layout.sideItemScale = 0.8
layout.sideItemAlpha = 0.8
layout.sideItemShift = 0.8
layout.spacingMode = .overlap(visibleOffset: 30)
layout.scrollDirection = .horizontal
layout.itemSize = CGSize(width: 250, height: 350)

let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)</code></pre></div><p>That&#8217;s it. You now have a carousel with overlapping items that scale down as they move away from the center.</p><p>Let me explain what each property does:</p><ul><li><p><em><strong>sideItemScale</strong></em> &#8212; How much the side items shrink. 0.8 means they&#8217;re 80% of the center item&#8217;s size.</p></li><li><p><em><strong>sideItemAlpha </strong></em>&#8212; The opacity of side items. Lower values make them more transparent.</p></li><li><p><em><strong>sideItemShift</strong></em> &#8212; How much side items shift vertically (or horizontally, depending on scroll direction).</p></li><li><p><em><strong>spacingMode </strong></em>&#8212; This is the interesting one. You have two options:</p></li><li><p><em><strong>.fixed(spacing: 10)</strong></em> &#8212; Items have a fixed gap between them.</p></li><li><p><em><strong>.overlap(visibleOffset: 30)</strong></em> &#8212; Items overlap each other. The visibleOffset controls how much of the side items peek out.</p></li></ul><h3>Step 2: Register and Dequeue Cells</h3><p>EDCarousel includes type-safe extensions for UICollectionView, so you don&#8217;t have to deal with string-based reuse identifiers:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;44a1cfdb-1115-42da-9f07-d53afe9a0134&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">// Register
collectionView.register(MyCell.self)

// Or register from a nib file
collectionView.registerNib(MyNibCell.self)

// Dequeue
if let cell = collectionView.dequeue(MyCell.self, for: indexPath) {
    // Configure your cell
}</code></pre></div><p>Clean, simple, and no more typos in reuse identifier strings.</p><h3>Step 3: Add a Custom Page Control</h3><p>EDCarousel comes with CustomPageControl, a UIPageControl subclass that lets you use custom images for the dots:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;25ece3b6-ad52-446d-8c04-35eebe7552c6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">let pageControl = CustomPageControl()
pageControl.numberOfPages = 5
pageControl.currentPageImage = UIImage(systemName: "circle.fill")
pageControl.otherPagesImage = UIImage(systemName: "circle")</code></pre></div><p>The dots update automatically when you change currentPage or numberOfPages. No extra calls needed.</p><h3>Step 4: Track the Current Page While Scrolling</h3><p>To keep the page control in sync with scrolling, use the scrollViewWillEndDragging delegate method:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;cd64aa8a-12e3-444a-9ad1-3a3f669275d9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">func scrollViewWillEndDragging(
    _: UIScrollView,
    withVelocity _: CGPoint,
    targetContentOffset: UnsafeMutablePointer&lt;CGPoint&gt;
) {
    let targetOffset = targetContentOffset.pointee.x
    let width = (collectionView.frame.size.width - padding) / 1.21
    let rounded = Double((images.count / 2)) * abs(targetOffset / width)
    let scale = round(rounded)
    pageControl.currentPage = Int(scale)
}</code></pre></div><h3>Step 5: Navigate Programmatically</h3><p>You can add previous/next buttons to navigate between pages:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;fcbd2cd9-6797-40ee-b23e-df3a641d3041&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">// Go to previous page
let prevIndex = max(pageControl.currentPage - 1, 0)
let indexPath = IndexPath(item: prevIndex, section: 0)
pageControl.currentPage = prevIndex
collectionView.isPagingEnabled = false
collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)

// Go to next page
let nextIndex = min(pageControl.currentPage + 1, images.count - 1)
let indexPath = IndexPath(item: nextIndex, section: 0)
pageControl.currentPage = nextIndex
collectionView.isPagingEnabled = false
collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)</code></pre></div><p><em><strong>Important:</strong> Make sure isPagingEnabled is set to false. The carousel layout handles the snapping behavior on its own. If paging is enabled, it conflicts with the custom layout.</em></p><h2>How It Works Under the Hood</h2><p>If you&#8217;re curious about the internals, here&#8217;s a quick overview. CarouselFlowLayout overrides a few key methods from <strong>UICollectionViewFlowLayout</strong>:</p><ol><li><p><strong>prepare():</strong> Checks if the layout state (size + scroll direction) has changed. If it has, it recalculates insets and spacing.</p></li></ol><p>2.<strong> layoutAttributesForElements(in:)</strong> Returns the layout attributes for all visible items, with custom transformations applied.</p><p>3. <strong>transformLayoutAttributes(_:)</strong> This is where the magic happens. For each cell, it calculates the distance from the collection view&#8217;s center and uses that to determine the scale, alpha, shift, and z-index. Items closer to the center get a higher scale and opacity.</p><p>4. <strong>targetContentOffset(forProposedContentOffset:withScrollingVelocity:) </strong>Handles the snapping. When the user stops scrolling, it finds the nearest item and adjusts the content offset to center it.</p><p>The result is a smooth, performant carousel that works with standard UIKit patterns.</p><h2>A Full Example</h2><p>Here&#8217;s a data source setup from the example project that shows how everything comes together:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;074db679-c9c1-41ef-87d0-09213b210d75&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">import EDCarousel

final class ExampleCollectionDataSource: NSObject, UICollectionViewDataSource {
    private var items = [Onboarding]()
    private weak var collectionView: UICollectionView?

    init(collectionView: UICollectionView) {
        self.collectionView = collectionView
        super.init()
        setupLayout(collectionView: collectionView)
    }

    private func setupLayout(collectionView: UICollectionView) {
        guard let layout = collectionView.collectionViewLayout as? CarouselFlowLayout else { return }
        let padding = 10.0
        layout.itemSize = CGSize(
            width: (collectionView.frame.size.width - padding) / 1.21,
            height: (collectionView.frame.size.width - padding) / 0.76
        )
        layout.spacingMode = .overlap(visibleOffset: 200)
        layout.sideItemScale = 0.8
        layout.scrollDirection = .horizontal
        collectionView.registerNib(ExampleCollectionViewCell.self)
        collectionView.isPagingEnabled = false
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -&gt; UICollectionViewCell {
        guard let cell = collectionView.dequeue(ExampleCollectionViewCell.self, for: indexPath) else {
            fatalError("Unable to dequeue cell")
        }
        let item = items[indexPath.row]
        cell.set(item: item)
        return cell
    }

    func collectionView(_: UICollectionView, numberOfItemsInSection _: Int) -&gt; Int {
        return items.count
    }
}</code></pre></div><p>You can also check out the full example project on GitHub: <a href="https://github.com/emrdgrmnci/EDCarousel.git">EDCarousel Example</a></p><h2>Why I Built This</h2><p>Building carousels in iOS with UICollectionView always felt like reinventing the wheel. Every time I needed one, I ended up writing the same flow layout code, the same snapping logic, the same scaling math. EDCarousel packages all of that into a reusable library that you can drop into any project.</p><h2>Get Started</h2><ul><li><p>GitHub: <a href="https://github.com/emrdgrmnci/EDCarousel">https://github.com/emrdgrmnci/EDCarousel</a></p></li><li><p>Requirements: iOS 14.0+, Swift 5.9+, Xcode 15.0+</p></li></ul><p>If you find <a href="https://github.com/emrdgrmnci/EDCarousel.git">EDCarousel</a> useful, give it a star on GitHub. And if you run into any issues or have ideas for improvements, feel free to open an issue. Contributions are always welcome.</p>]]></content:encoded></item><item><title><![CDATA[Walk Mate’s Live Activity + Widgets: Shipping Motivation at a Glance]]></title><description><![CDATA[When I built Walk Mate&#8217;s daily route experience, I wanted the app to feel present even when it isn&#8217;t open.]]></description><link>https://emredegirmenci.substack.com/p/walk-mates-live-activity-widgets</link><guid isPermaLink="false">https://emredegirmenci.substack.com/p/walk-mates-live-activity-widgets</guid><dc:creator><![CDATA[Emre Degirmenci]]></dc:creator><pubDate>Wed, 15 Apr 2026 21:06:19 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!uLBd!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F484afac4-de77-4e59-aa24-80131a4c3aca_1206x386.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When I built <a href="https://walk-mate.app/apps/walkmate.html">Walk Mate&#8217;s</a> daily route experience, I wanted the app to feel present even when it isn&#8217;t open. Live Activities and widgets turned out to be the best way to surface progress, streaks, and motivation without interrupting a walk. This post shares how I wired ActivityKit and WidgetKit together, what worked, and the pitfalls I hit.</p><h2>Goals</h2><ul><li><p>Keep the route in view during a walk (Live Activity + Dynamic Island).</p></li><li><p>Keep motivation visible throughout the day (home screen widgets).</p></li><li><p>Make the UX &#8220;summary-first&#8221;: streak, medals, and distance.</p></li></ul><h2>Live Activity Architecture</h2><p>At the core is a compact set of <strong>ActivityAttributes</strong> that capture just the data I want to present: distances, medals, and unit preferences. I use a <strong>@MainActor</strong> manager to create, update, and end activities keeping activity state in a single place and avoiding multiple active routes.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;77f28f69-9d55-4a85-aa28-13d7ee8baf8e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">import ActivityKit

struct WalkActivityAttributes: ActivityAttributes {
    struct ContentState: Codable, Hashable {
        var distanceWalked: Double
        var totalDistance: Double
        var medalsCollected: Int
        var medalsTotal: Int
        var distanceUnitRawValue: String
    }

    var routeId: String
    var routeName: String
}</code></pre></div><p>The manager handles:</p><ul><li><p>Route switching (end previous activity when the route changes).</p></li><li><p>Update vs. create logic (update if already active, otherwise request a new activity).</p></li><li><p>End and cleanup in a single function.</p></li></ul><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;e25b5429-b7df-42c3-b38d-1f2086e8a52a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">@MainActor
final class LiveActivityManager {
    static let shared = LiveActivityManager()

    private var activity: Activity&lt;WalkActivityAttributes&gt;?
    private var currentRouteId: String?

    func startActivity(route: Route, distanceWalked: Double, distanceUnit: DistanceUnit) async {
        guard #available(iOS 16.1, *) else { return }

        let routeId = route.id.uuidString
        if let currentRouteId, currentRouteId != routeId {
            await endActivity(dismissalPolicy: .immediate)
        }

        let state = WalkActivityAttributes.ContentState(
            distanceWalked: distanceWalked,
            totalDistance: route.distance,
            medalsCollected: route.medals.filter(\.isCollected).count,
            medalsTotal: route.medals.count,
            distanceUnitRawValue: distanceUnit.rawValue
        )

        if let activity {
            let content = ActivityContent(state: state, staleDate: nil)
            await activity.update(content)
            return
        }

        let attributes = WalkActivityAttributes(
            routeId: routeId,
            routeName: AppConstants.App.name
        )

        do {
            let content = ActivityContent(state: state, staleDate: nil)
            activity = try Activity.request(
                attributes: attributes,
                content: content,
                pushType: nil
            )
            currentRouteId = routeId
        } catch {
            activity = nil
            currentRouteId = nil
        }
    }
}</code></pre></div><h2>Lock Screen + Dynamic Island UI</h2><p>The Live Activity view emphasizes &#8220;walked vs total&#8221; and medal progress. The Dynamic Island gives a compact icon + stat treatment, then expands into progress.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!uLBd!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F484afac4-de77-4e59-aa24-80131a4c3aca_1206x386.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!uLBd!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F484afac4-de77-4e59-aa24-80131a4c3aca_1206x386.webp 424w, https://substackcdn.com/image/fetch/$s_!uLBd!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F484afac4-de77-4e59-aa24-80131a4c3aca_1206x386.webp 848w, https://substackcdn.com/image/fetch/$s_!uLBd!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F484afac4-de77-4e59-aa24-80131a4c3aca_1206x386.webp 1272w, https://substackcdn.com/image/fetch/$s_!uLBd!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F484afac4-de77-4e59-aa24-80131a4c3aca_1206x386.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!uLBd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F484afac4-de77-4e59-aa24-80131a4c3aca_1206x386.webp" width="1206" height="386" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/484afac4-de77-4e59-aa24-80131a4c3aca_1206x386.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:386,&quot;width&quot;:1206,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:9814,&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/194344025?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F484afac4-de77-4e59-aa24-80131a4c3aca_1206x386.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_!uLBd!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F484afac4-de77-4e59-aa24-80131a4c3aca_1206x386.webp 424w, https://substackcdn.com/image/fetch/$s_!uLBd!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F484afac4-de77-4e59-aa24-80131a4c3aca_1206x386.webp 848w, https://substackcdn.com/image/fetch/$s_!uLBd!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F484afac4-de77-4e59-aa24-80131a4c3aca_1206x386.webp 1272w, https://substackcdn.com/image/fetch/$s_!uLBd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F484afac4-de77-4e59-aa24-80131a4c3aca_1206x386.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 role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;2ea4b808-407d-43f3-b69a-391665a14834&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">@available(iOS 16.1, *)
struct WalkMateLiveActivity: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: WalkActivityAttributes.self) { context in
            WalkMateLiveActivityLockScreenView(context: context)
        } dynamicIsland: { context in
            DynamicIsland {
                DynamicIslandExpandedRegion(.leading) {
                    HStack(spacing: 8) {
                        Image(systemName: "figure.walk")
                            .font(.title2)
                            .foregroundStyle(.white)
                            .frame(width: 32, height: 32)
                            .background(.blue, in: .rect(cornerRadius: 8))
                        VStack(alignment: .leading, spacing: 2) {
                            Text("\(context.state.medalsCollected)/\(context.state.medalsTotal)")
                                .font(.headline)
                                .bold()
                            Text(String(localized: "Medals"))
                                .font(.caption2)
                                .foregroundStyle(.secondary)
                        }
                    }
                    .padding(.top, 4)
                    .padding(.bottom, 4)
                    .padding(.leading, 4)
                }
                // ... trailing + bottom progress
            } compactLeading: {
                Image(systemName: "figure.walk")
                    .foregroundStyle(.white)
                    .frame(width: 22, height: 22)
                    .background(.blue, in: .rect(cornerRadius: 6))
            } compactTrailing: {
                Text(formattedDistance(context.state.totalDistance, unitRawValue: context.state.distanceUnitRawValue))
                    .font(.caption2)
                    .bold()
            } minimal: {
                Image(systemName: "figure.walk")
                    .foregroundStyle(.white)
                    .frame(width: 18, height: 18)
                    .background(.blue, in: .rect(cornerRadius: 5))
            }
        }
    }
}</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_!FtjK!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6f92231-c920-4d27-80eb-5d73e42faa07_1206x2439.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!FtjK!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6f92231-c920-4d27-80eb-5d73e42faa07_1206x2439.webp 424w, https://substackcdn.com/image/fetch/$s_!FtjK!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6f92231-c920-4d27-80eb-5d73e42faa07_1206x2439.webp 848w, https://substackcdn.com/image/fetch/$s_!FtjK!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6f92231-c920-4d27-80eb-5d73e42faa07_1206x2439.webp 1272w, https://substackcdn.com/image/fetch/$s_!FtjK!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6f92231-c920-4d27-80eb-5d73e42faa07_1206x2439.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!FtjK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6f92231-c920-4d27-80eb-5d73e42faa07_1206x2439.webp" width="1206" height="2439" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b6f92231-c920-4d27-80eb-5d73e42faa07_1206x2439.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:2439,&quot;width&quot;:1206,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:24678,&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/194344025?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6f92231-c920-4d27-80eb-5d73e42faa07_1206x2439.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_!FtjK!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6f92231-c920-4d27-80eb-5d73e42faa07_1206x2439.webp 424w, https://substackcdn.com/image/fetch/$s_!FtjK!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6f92231-c920-4d27-80eb-5d73e42faa07_1206x2439.webp 848w, https://substackcdn.com/image/fetch/$s_!FtjK!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6f92231-c920-4d27-80eb-5d73e42faa07_1206x2439.webp 1272w, https://substackcdn.com/image/fetch/$s_!FtjK!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb6f92231-c920-4d27-80eb-5d73e42faa07_1206x2439.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 role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Widgets: Two &#8220;at a glance&#8221; surfaces</h2><p>I shipped two widgets with a shared provider:</p><ul><li><p><strong>Progress Snapshot:</strong> streak + distance + next milestone.</p></li><li><p><strong>Motivation Highlights:</strong> achievements + medals + gentle nudge.</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_!4m6j!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91d36ff5-7fad-421f-89c1-2b5c3aa06ea1_1206x2622.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!4m6j!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91d36ff5-7fad-421f-89c1-2b5c3aa06ea1_1206x2622.webp 424w, https://substackcdn.com/image/fetch/$s_!4m6j!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91d36ff5-7fad-421f-89c1-2b5c3aa06ea1_1206x2622.webp 848w, https://substackcdn.com/image/fetch/$s_!4m6j!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91d36ff5-7fad-421f-89c1-2b5c3aa06ea1_1206x2622.webp 1272w, https://substackcdn.com/image/fetch/$s_!4m6j!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91d36ff5-7fad-421f-89c1-2b5c3aa06ea1_1206x2622.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!4m6j!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91d36ff5-7fad-421f-89c1-2b5c3aa06ea1_1206x2622.webp" width="1206" height="2622" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/91d36ff5-7fad-421f-89c1-2b5c3aa06ea1_1206x2622.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:2622,&quot;width&quot;:1206,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:65044,&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/194344025?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91d36ff5-7fad-421f-89c1-2b5c3aa06ea1_1206x2622.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_!4m6j!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91d36ff5-7fad-421f-89c1-2b5c3aa06ea1_1206x2622.webp 424w, https://substackcdn.com/image/fetch/$s_!4m6j!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91d36ff5-7fad-421f-89c1-2b5c3aa06ea1_1206x2622.webp 848w, https://substackcdn.com/image/fetch/$s_!4m6j!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91d36ff5-7fad-421f-89c1-2b5c3aa06ea1_1206x2622.webp 1272w, https://substackcdn.com/image/fetch/$s_!4m6j!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F91d36ff5-7fad-421f-89c1-2b5c3aa06ea1_1206x2622.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 role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;8bf2e36b-754c-4b10-a09b-6c20e5c40669&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">@main
struct WalkMateWidgetsBundle: WidgetBundle {
    var body: some Widget {
        ProgressSnapshotWidget()
        MotivationHighlightsWidget()
    }
}</code></pre></div><p>The provider uses a simple timeline that refreshes hourly:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;44dbc25d-2e42-4d56-802f-9f0454786d40&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">struct WalkMateWidgetProvider: TimelineProvider {
    func placeholder(in context: Context) -&gt; WalkMateWidgetEntry {
        WidgetStatsStore.placeholderEntry()
    }

    func getSnapshot(in context: Context, completion: @escaping (WalkMateWidgetEntry) -&gt; Void) {
        completion(WidgetStatsStore.snapshotEntry())
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline&lt;WalkMateWidgetEntry&gt;) -&gt; Void) {
        let entry = WidgetStatsStore.snapshotEntry()
        let nextUpdate = Calendar.current.date(byAdding: .hour, value: 1, to: entry.date) ?? entry.date.addingTimeInterval(3600)
        completion(Timeline(entries: [entry], policy: .after(nextUpdate)))
    }
}</code></pre></div><h2>Shared storage: keep widgets simple</h2><p>I store aggregate stats in <strong><a href="https://developer.apple.com/documentation/foundation/nsubiquitouskeyvaluestore">NSUbiquitousKeyValueStore</a></strong>, which lets the widget fetch the latest totals without needing the app to be running.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;4ed97041-072c-4150-a987-aa2faf2066d0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">static func snapshotEntry() -&gt; WalkMateWidgetEntry {
    let store = NSUbiquitousKeyValueStore.default
    store.synchronize()

    let totalDistance = store.double(forKey: totalDistanceKey)
    let totalRoutes = Int(store.longLong(forKey: totalWalksKey))
    let longestStreak = Int(store.longLong(forKey: longestStreakKey))
    let currentStreak = Int(store.longLong(forKey: consecutiveDaysKey))
    let totalMedals = totalMedalsCollected(from: store)

    let milestones = loadMilestones(from: store)
    let achievementsUnlocked = milestones.filter { $0.unlocked }.count
    let achievementsTotal = milestones.count
    let nextMilestone = nextMilestoneProgress(from: milestones)
    let motivationText = motivationText(currentStreak: currentStreak, unlockedCount: achievementsUnlocked)

    return WalkMateWidgetEntry(
        date: Date(),
        totalMedals: totalMedals,
        totalDistanceMeters: totalDistance,
        totalRoutes: totalRoutes,
        currentStreak: currentStreak,
        longestStreak: longestStreak,
        achievementsUnlocked: achievementsUnlocked,
        achievementsTotal: achievementsTotal,
        nextMilestone: nextMilestone,
        motivationText: motivationText
    )
}</code></pre></div><h2>UX Decisions That Mattered</h2><ul><li><p><strong>Distance formatting is intentional:</strong> Live Activity view formats meters into km/mi (and uses unit saved in state), while widgets use lightweight ~km formatting to keep layout stable.</p></li><li><p><strong>Progress-driven copy:</strong> the motivation widget&#8217;s text changes based on streak or milestone unlocks.</p></li></ul><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;344a22df-12fb-4224-b7f5-d69784096559&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">private static func motivationText(currentStreak: Int, unlockedCount: Int) -&gt; String {
    if currentStreak &gt;= 7 {
        return String(localized: "Week streak. Keep going.")
    }
    if currentStreak &gt;= 1 {
        return String(localized: "Day \(currentStreak) streak. Keep walking.")
    }
    if unlockedCount &gt; 0 {
        return String(localized: "New goals are within reach.")
    }
    return String(localized: "Start your first walk.")
}</code></pre></div><h2>Lessons Learned (the honest list)</h2><ul><li><p><strong>Activity lifetime is tricky:</strong> always clean up old activities when a new route starts, or you&#8217;ll end up with stale islands. I explicitly end old activities when the route ID changes.</p></li><li><p><strong>Timeline updates are conservative:</strong> hourly refresh is stable and safe for battery. More frequent updates require a good reason.</p></li><li><p><strong>Different formatting per surface:</strong> Live Activity can be more precise; widgets should be glanceable.</p></li><li><p><strong>Context limits are real:</strong> keep ActivityKit state small and only include what you need to render.</p></li><li><p><strong>Localization should be baked in:</strong> I use String(localized:) for widget copy to avoid English-only UI.</p></li></ul><h2>What I&#8217;d Do Next</h2><ul><li><p>Add a widget configuration intent to let users choose metric vs. imperial.</p></li><li><p>Add an &#8220;in-walk&#8221; widget that surfaces current route only when active.</p></li><li><p>Explore activity relevance and alerting to make &#8220;in walk&#8221; more visible.</p></li></ul>]]></content:encoded></item><item><title><![CDATA[From JSON Blobs to Syncable SQLite: How I Migrated Walk Mate to SQLiteData]]></title><description><![CDATA[A practical migration path from UserDefaults + JSON to normalized, CloudKit-ready persistence]]></description><link>https://emredegirmenci.substack.com/p/from-json-blobs-to-syncable-sqlite</link><guid isPermaLink="false">https://emredegirmenci.substack.com/p/from-json-blobs-to-syncable-sqlite</guid><dc:creator><![CDATA[Emre Degirmenci]]></dc:creator><pubDate>Wed, 15 Apr 2026 18:19:17 GMT</pubDate><enclosure 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" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I recently migrated <a href="https://apple.co/4mz7vev">Walk Mate</a>&#8217;s persistence layer from a mix of <code>UserDefaults</code>, JSON files, and legacy SQLite blob storage to a <code>SQLiteData</code> + GRDB-backed model with <a href="https://developer.apple.com/icloud/cloudkit/">CloudKit</a> sync support.</p><p>This wasn&#8217;t just a storage swap. It was about making the app more resilient, queryable, and future-proof without breaking users&#8217; existing data.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://emredegirmenci.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h2>Why Migrate at All?</h2><p>For a while, persistence looked like this:</p><ul><li><p>state spread across <code>UserDefaults</code> keys</p></li><li><p>arrays in iCloud Drive JSON files (<strong>routes.json</strong>, <strong>favoriteRoutes.json</strong>, <strong>avoidedSegments.json</strong>)</p></li></ul><p>That works early on, but eventually you hit limits:</p><ul><li><p>hard to evolve schema safely</p></li><li><p>hard to merge data from multiple sources/devices</p></li><li><p>hard to reason about partial corruption or shape drift</p></li><li><p>hard to support structured <strong>CloudKit sync</strong> semantics</p></li></ul><p>So I moved to normalized tables with explicit migrations, and kept backward compatibility during rollout.</p><h2>The New Storage Core</h2><p>The app now centralizes persistence in SQLitePersistenceStore, which owns database setup, migration, CRUD, and cross-source merge behavior:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;0a06f260-7c07-4b55-b7ac-b606905dab19&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">func rebuildFromLegacySourcesIfNeeded() {
    guard !UserDefaults.standard.bool(forKey: Constants.migrationKey) else { return }
    ...
    writeFullState(state)
    UserDefaults.standard.set(true, forKey: Constants.migrationKey)
}</code></pre></div><p>At app startup, <strong>database + sync</strong> dependencies are prepared first, then migration runs once:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;7a0e8f35-3d7b-4230-9aef-41b2ce1a87c7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">let database = SQLitePersistenceStore.makeDatabase()
do {
    let syncEngine = try WalkMateSyncEngineFactory.make(for: database)
    prepareDependencies {
        $0.defaultDatabase = database
        $0.defaultSyncEngine = syncEngine
    }
} catch { ... }

SQLitePersistenceStore.shared.configure(database: database)
SQLitePersistenceStore.shared.rebuildFromLegacySourcesIfNeeded()</code></pre></div><h2>Schema Design: What&#8217;s Normalized vs What Stays as Payload</h2><p>I used <strong>@Table</strong> models for stable entities:</p><ul><li><p><em>wm_user_stats</em></p></li><li><p><em>wm_milestones</em></p></li><li><p><em>wm_routes</em></p></li><li><p><em>wm_favorite_routes</em></p></li><li><p><em>wm_avoided_segments</em></p></li><li><p><em>wm_neighborhoods</em></p></li></ul><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;6956af1a-7128-437d-9ee4-b99bceb3244d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">@Table("wm_user_stats")
nonisolated struct WalkMateUserStatsRow: Hashable {
    @Column(primaryKey: true)
    var id: String
    var totalMedalsCollected: Int = 0
    ...
}</code></pre></div><p>A deliberate hybrid choice: complex domain objects like routes/favorites are still stored as encoded payload blobs (<strong>Data</strong>) inside normalized row identities. That keeps schema churn lower while still enabling proper table-level sync/versioning.</p><h2>Migration Strategy in Practice</h2><p>I split migration into two layers:</p><ol><li><p><strong>GRDB schema migrations</strong> (versioned)</p></li><li><p><strong>One-shot app-layer import</strong> from legacy sources</p></li></ol><h3>v1: Create new tables + import legacy blob if present</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;521586fe-caa0-4b1e-a9ef-c81bd0682646&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">static func register(migrator: inout DatabaseMigrator) {
    migrator.registerMigration("walkmate_wm_tables_v1") { db in
        ...
        try migrateLegacyAppStorageBlobIfPresent(db: db)
        try ensureDefaultUserStatsRow(db: db)
    }
}</code></pre></div><h3>v2: CloudKit compatibility cleanup</h3><p>CloudKit sync has stricter assumptions. In my case I had to:</p><ul><li><p>remove non-primary UNIQUE constraints that SyncEngine dislikes</p></li><li><p>migrate tables to UUID primary keys for distributed row identity</p></li></ul><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;3e5e33c6-1849-423e-b6f9-55c859807ab2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">migrator.registerMigration("walkmate_cloudkit_v2") { db in
    try migrateMilestonesDropMilestoneTypeUniqueIfNeeded(db)
    try migrateAvoidedSegmentsUUIDPrimaryKey(db)
    try migrateNeighborhoodsUUIDPrimaryKey(db)
}</code></pre></div><p>This was one of the most important &#8220;learned the hard way&#8221; points: <strong>CloudKit-friendly schema rules should be baked in early</strong> <strong>:D</strong></p><h2>Data Rebuild: Merging Multiple Legacy Sources Safely</h2><p>The app layer migration (<strong>rebuildFromLegacySourcesIfNeeded</strong>) reconstructs a canonical state by merging:</p><ul><li><p>existing table data</p></li><li><p>iCloud key-value entries (stats/streak/etc.)</p></li><li><p>iCloud JSON files (routes/favorites/avoided)</p></li><li><p>UserDefaults fallback blobs</p></li></ul><p>Merging is intentionally &#8220;max/union&#8221; oriented (keep higher counters, merge arrays by IDs, single neighborhoods), reducing risk of silent data loss from stale sources.</p><p>That design gives users a better chance of retaining progress even if one source is out of date.</p><h2>Sync Integration with SQLiteData</h2><p>The <strong>SyncEngine</strong> wiring is straightforward once schema is ready:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;239445d0-f889-4b9d-9e93-fe61152cbca0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">enum WalkMateSyncEngineFactory {
    static func make(for database: any DatabaseWriter) throws -&gt; SyncEngine {
        try SyncEngine(
            for: database,
            privateTables: WalkMateUserStatsRow.self,
            WalkMateMilestoneRow.self,
            WalkMateRouteRow.self,
            ...
        )
    }
}</code></pre></div><p>A nice side effect: sync concerns are isolated from UI and feature modules. Existing feature APIs (<strong>RouteStorage, FavoriteRouteStorage, MilestoneStorage</strong>) stayed mostly stable, while internals swapped to SQLite backed operations.</p><h2>Operational Details That Matter</h2><p>A few production-minded choices made this migration safer:</p><ul><li><p><strong>One-time migration flag</strong> (sqliteDataMigration_v1_completed) prevents repeated destructive rebuilds.</p></li><li><p><strong>On-disk DB with in-memory fallback</strong> if file opening fails.</p></li><li><p><strong>Mirror writes to iCloud files</strong> for continuity.</p></li><li><p><strong>Sync status snapshot</strong> in settings for observability (iCloud availability, last sync date, debug counts).</p></li><li><p><strong>Widget snapshot refresh</strong> after mutations to keep widgets consistent.</p></li></ul><h2>What I&#8217;d Recommend If You&#8217;re Doing Similar</h2><ol><li><p>Model migration as <strong>identical steps</strong>, not one giant conversion.</p></li><li><p>Keep old read paths for one release while writing to new storage.</p></li><li><p>Use conservative merge rules (max, union, identical by stable ID).</p></li><li><p>If CloudKit is in scope, validate schema against sync constraints early.</p></li><li><p>Add lightweight in-app observability so you can debug real user devices.</p></li></ol><h2>Closing</h2><p>This migration moved <a href="https://apple.co/4mz7vev">Walk Mate</a> from fragile, fragmented persistence to a sync-capable SQLite backbone without forcing users to lose historical progress.</p><p>If you&#8217;re shipping an app that started with <strong>UserDefaults + JSON</strong> shortcuts, this is your sign: you can migrate safely, incrementally, and with far less risk than a hard cutover.</p><div><hr></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://emredegirmenci.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item></channel></rss>