<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>Shipping small iOS apps | Zubin Khavarian</title><description>Every part of Zubin Khavarian&apos;s series &quot;Shipping small iOS apps&quot;, as it publishes.</description><link>https://zubin.dev/</link><language>en-us</language><atom:link href="https://zubin.dev/rss/shipping-ios.xml" rel="self" type="application/rss+xml"/><lastBuildDate>Mon, 05 Oct 2026 12:00:00 GMT</lastBuildDate><image><url>https://zubin.dev/avatar.png</url><title>Shipping small iOS apps | Zubin Khavarian</title><link>https://zubin.dev/</link></image><item><title>Stamp DEV on the Debug Icon</title><link>https://zubin.dev/blog/debug-build-own-app/</link><guid isPermaLink="true">https://zubin.dev/blog/debug-build-own-app/</guid><description>My Debug builds are their own app: a .dev bundle ID, a Dev name, and the real icon with a DEV ribbon a small script draws across the corner. They install beside the App Store version and never touch its data.</description><pubDate>Mon, 05 Oct 2026 12:00:00 GMT</pubDate><content:encoded><![CDATA[<p>Once one of my apps is on the App Store, I have the store version on my phone like anyone else. I also run Debug builds from Xcode on the same phone all day. If both use the same bundle ID, every Debug run replaces the store app, and they share one sandbox: my half-finished test data lands in the same place as the real progress, and I can never tell at a glance which build I’m tapping.</p>
<p>So in every one of my apps, the Debug build is a different app.</p>
<h2 id="three-build-settings">Three build settings</h2>
<p>The Debug configuration changes three things, and Release keeps the originals:</p>
<pre class="astro-code astro-code-themes zubin-light zubin-dark has-copy-btn" tabindex="0" data-language="plaintext"><code><span class="line"><span>                                   Debug                   Release</span></span>
<span class="line"><span>PRODUCT_BUNDLE_IDENTIFIER          dev.zubin.Mathness.dev  dev.zubin.Mathness</span></span>
<span class="line"><span>INFOPLIST_KEY_CFBundleDisplayName  Mathness! Dev           Mathness!</span></span>
<span class="line"><span>ASSETCATALOG_COMPILER_APPICON_NAME AppIcon-Dev             AppIcon</span></span></code></pre>
<p>A different bundle ID is a different app to iOS: it installs beside the store version, with its own sandbox and its own <code>UserDefaults</code>. The name says “Dev” under the icon. And the icon gets a ribbon.</p>
<p><img src="https://zubin.dev/_astro/dev-icons.U2hzdPPR_2jQbPn.webp" alt="Four App Store app icons in a row (Mathness, Treble Trouble, Solve It and Mahbanou), and below each the same icon with an orange, slightly see-through DEV ribbon across its top-left corner." loading="lazy" decoding="async" width="1160" height="608"></p>
<h2 id="the-ribbon-is-generated">The ribbon is generated</h2>
<p>Nobody draws <code>AppIcon-Dev</code> by hand. A small Swift script, shared by all my apps, copies the app’s icon set and draws the ribbon over every image in it. Each app’s icon script runs it right after rendering the real icon, so the two can’t drift apart.</p>
<figure class="sketch not-prose"><div class="sketch-frame"><p><em>Figure: An app icon with rounded corners and a diagonal DEV ribbon across its top-left corner, drawn to scale. The ribbon's center line is 27% of the icon's width in from the corner, it's 13% of the width thick, and it's 85% opaque, so it stays clear of the rounded corner and the artwork shows through. <a href="https://zubin.dev/blog/debug-build-own-app/">See the drawing in the post.</a></em></p></div><figcaption>The ribbon, to scale: clear of the rounded corner, and see-through.</figcaption></figure>
<p>A few details took more than one try:</p>
<ul>
<li><strong>It sits in the corner.</strong> The first version was a band across the bottom. The corner ribbon covers less of the art, and it stays inside the home screen’s rounded mask.</li>
<li><strong>It’s see-through.</strong> At 85% opacity you can still see what’s underneath, which matters when the icon is the only thing telling apps apart.</li>
<li><strong>Every variant gets one.</strong> iOS icons now come in light, dark and tinted versions. The tinted one is grayscale (iOS supplies the color), so its ribbon is white with black text instead of orange with white.</li>
<li><strong>Opaque stays opaque.</strong> If the source icon has no alpha channel, neither does the stamped one.</li>
</ul>
<h2 id="extensions-follow-the-app">Extensions follow the app</h2>
<p>For a plain app, those three settings are the whole job. <a href="https://zubin.dev/apps/mahbanou/">Mahbanou</a> has home screen widgets, an Apple Watch app and watch complications, and each is its own target with its own bundle ID. They all have to follow the app into <code>.dev</code>, or the build fails: an embedded target’s ID has to start with its app’s.</p>
<figure class="sketch not-prose"><div class="sketch-frame"><p><em>Figure: Mahbanou's four targets under each configuration. App Store: dev.zubin.Mahbanou, its Widgets, its watchkitapp and the watch's Widgets, sharing the app group group.dev.zubin.Mahbanou. Debug: the same four with .dev after Mahbanou, sharing group.dev.zubin.Mahbanou.dev. <a href="https://zubin.dev/blog/debug-build-own-app/">See the drawing in the post.</a></em></p></div><figcaption>Every target follows the app into .dev, and so does the app group.</figcaption></figure>
<p>The watch app also names its phone app (<code>WKCompanionAppBundleIdentifier</code>), so that setting gets a Debug value too.</p>
<p>The part that’s easy to miss is the app group. The widgets read the app’s settings from a shared container, and if Debug used the same group as the store app, the two would still share data through it. So the entitlements use a build setting instead of a literal:</p>
<pre class="astro-code astro-code-themes zubin-light zubin-dark has-copy-btn" tabindex="0" data-language="xml"><code><span class="line"><span>&lt;key&gt;</span><span>com.apple.security.application-groups</span><span>&lt;/key&gt;</span></span>
<span class="line"><span>&lt;array&gt;</span></span>
<span class="line"><span>  &lt;string&gt;</span><span>$(MAHBANOU_APP_GROUP)</span><span>&lt;/string&gt;</span></span>
<span class="line"><span>&lt;/array&gt;</span></span></code></pre>
<p><code>MAHBANOU_APP_GROUP</code> is <code>group.dev.zubin.Mahbanou.dev</code> in Debug, and the code that opens the container picks the same name under <code>#if DEBUG</code>.</p>
<h2 id="the-scripts-follow-too">The scripts follow too</h2>
<p>Everything that launches the app by ID has to know about <code>.dev</code>. The quick screenshot script and the App Store screenshot script both launch <code>dev.zubin.Mathness.dev</code>.</p>
<p>The store screenshots needed one more step. On iPad the app’s name shows in the status bar, so a capture of “Mathness! Dev” would put the wrong name on the store page. That script builds its own copy of the Debug app with the store’s name and icon, overriding the settings on the command line:</p>
<pre class="astro-code astro-code-themes zubin-light zubin-dark has-copy-btn" tabindex="0" data-language="bash"><code><span class="line"><span>xcodebuild</span><span> -project</span><span> Mathness.xcodeproj</span><span> -scheme</span><span> Mathness</span><span> \</span></span>
<span class="line"><span>  -destination</span><span> &#39;generic/platform=iOS Simulator&#39;</span><span> \</span></span>
<span class="line"><span>  -derivedDataPath</span><span> DerivedData/Store</span><span> \</span></span>
<span class="line"><span>  &quot;INFOPLIST_KEY_CFBundleDisplayName=Mathness!&quot;</span><span> \</span></span>
<span class="line"><span>  ASSETCATALOG_COMPILER_APPICON_NAME=AppIcon</span><span> build</span></span></code></pre>
<p>It keeps the <code>.dev</code> bundle ID, so the capture build never replaces anything, and it builds into its own derived data folder, so it doesn’t fight my everyday Debug build.</p>
<h2 id="what-stays-the-same">What stays the same</h2>
<p>Only Debug changes. TestFlight builds are archived with the Release configuration (Xcode’s default), so they keep the real ID, name and icon, and they update the store app the way a real release would. That’s what you want from a beta.</p>
<p>Purchases in Debug come from the local StoreKit configuration file in the scheme, so the different ID doesn’t matter there. The App Store’s sandbox wouldn’t find the real products under a <code>.dev</code> ID, which is one more reason to test real purchases in TestFlight.</p>
<h2 id="worth-copying">Worth copying</h2>
<ul>
<li><strong>Give Debug its own bundle ID</strong>, so it installs beside the store app with its own data.</li>
<li><strong>Change the name and the icon too.</strong> The name is what you read; the ribbon is what you spot.</li>
<li><strong>Generate the Dev icon from the real one</strong>, in the same script, so they never drift.</li>
<li><strong>Move every extension and app group with it</strong>, or the two apps will still share data.</li>
<li><strong>Grep your scripts for the bundle ID</strong> after the change.</li>
</ul>]]></content:encoded><dc:creator>Zubin Khavarian</dc:creator><category>ios</category><category>swift</category><category>app-store</category><category>tooling</category></item></channel></rss>