<?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"><channel><title><![CDATA[NEXT PLANET - UX and iOS dev]]></title><description><![CDATA[My name's Kris. I am a UX Designer and iOS Developer. I design and make apps under the brand NEXT PLANET.]]></description><link>https://blog.next-planet.com</link><generator>RSS for Node</generator><lastBuildDate>Fri, 10 Apr 2026 09:25:54 GMT</lastBuildDate><atom:link href="https://blog.next-planet.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How to add keyboard shortcuts to switch tabs in your iPadOS or macOS app]]></title><description><![CDATA[Recently, I added keyboard shortcuts to my time-tracking app Moons allowing users to switch tabs by pressing ⌘1, ⌘2, ⌘3 and ⌘4. I also added corresponding commands to the app’s menu. Here’s a comparison of the View menu in my app BEFORE and AFTER the...]]></description><link>https://blog.next-planet.com/how-to-add-keyboard-shortcuts-to-switch-tabs-in-your-ipados-or-macos-app</link><guid isPermaLink="true">https://blog.next-planet.com/how-to-add-keyboard-shortcuts-to-switch-tabs-in-your-ipados-or-macos-app</guid><category><![CDATA[ios app development]]></category><category><![CDATA[SwiftUI]]></category><category><![CDATA[Swift]]></category><category><![CDATA[Build In Public]]></category><category><![CDATA[ipados]]></category><category><![CDATA[macOS]]></category><dc:creator><![CDATA[Kris Slazinski]]></dc:creator><pubDate>Wed, 02 Jul 2025 13:56:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1751455890971/86c32e46-ff08-4805-8766-91142ddedbf5.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently, I added keyboard shortcuts to my time-tracking app <a target="_blank" href="https://apps.apple.com/app/id1632174045">Moons</a> allowing users to switch tabs by pressing <strong>⌘1</strong>, <strong>⌘2</strong>, <strong>⌘3</strong> and <strong>⌘4</strong>. I also added corresponding commands to the app’s menu. Here’s a comparison of the View menu in my app BEFORE and AFTER these changes:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751456973229/47776331-cf54-4de1-a5f5-05db91c53518.jpeg" alt class="image--center mx-auto" /></p>
<h2 id="heading-tabs">Tabs</h2>
<p>I’d like to share the code so anyone can add such shortcuts to their apps. It’s fairly simple. I needed to use two modifiers for this: <a target="_blank" href="https://developer.apple.com/documentation/swiftui/scene/commands\(content:\)">commands</a> and <a target="_blank" href="https://developer.apple.com/documentation/swiftui/view/keyboardshortcut\(_:modifiers:\)">keyboardShortcut</a>. I’ll come back to these, but let’s start from the beginning. To switch tabs, we need a <code>TabView</code> first. <code>TabView</code> in my ContentView looks like this:</p>
<pre><code class="lang-swift"><span class="hljs-type">TabView</span>(selection: $appState.selectedTab) {
    <span class="hljs-type">Tab</span>(<span class="hljs-string">"Timer"</span>, systemImage: <span class="hljs-string">"timer.circle.fill"</span>, value: .timer) {
        <span class="hljs-type">TimerView</span>()
    }

    <span class="hljs-type">Tab</span>(<span class="hljs-string">"Projects"</span>, image: <span class="hljs-string">"moons.project.done"</span>, value: .projects) {
        <span class="hljs-type">ProjectsView</span>()
    }

    <span class="hljs-type">Tab</span>(<span class="hljs-string">"Time Entries"</span>, systemImage: <span class="hljs-string">"list.bullet.circle.fill"</span>, value: .timeEntries) {
        <span class="hljs-type">TimeEntriesView</span>()
    }

    <span class="hljs-type">Tab</span>(<span class="hljs-string">"More"</span>, systemImage: <span class="hljs-string">"ellipsis.circle.fill"</span>, value: .more) {
        <span class="hljs-type">MoreView</span>()
    }
}
</code></pre>
<p>As you can see, I’m using <code>$appState.selectedTab</code> as the selection. This <code>appState</code> is an <code>EnvironmentObject</code> of a custom type <code>AppState</code> - an ObservableObject I defined in another file. It looks like this:</p>
<pre><code class="lang-swift"><span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppState</span>: <span class="hljs-title">ObservableObject</span> </span>{
    <span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">Tab</span>: <span class="hljs-title">Hashable</span> </span>{
        <span class="hljs-keyword">case</span> timer, projects, timeEntries, more
    }

    @<span class="hljs-type">Published</span> <span class="hljs-keyword">var</span> selectedTab: <span class="hljs-type">Tab?</span> = .timer
<span class="hljs-comment">//    ...</span>
<span class="hljs-comment">//    Some other properties I need for other shortcuts.</span>
}
</code></pre>
<p>In the <code>AppState</code> class, I have an enum called <code>Tab</code> and a Published var <code>selectedTab</code> of type <code>Tab</code>.</p>
<p>Basically, every time user switches a tab in the app (by tapping or clicking a tab), the value of <code>selectedTab</code> changes: .timer for TimerView(), .projects for ProjectsView() and so on. You can see this value in the <code>Tab</code> part of the code I showed you earlier:</p>
<pre><code class="lang-swift"><span class="hljs-type">Tab</span>(<span class="hljs-string">"Timer"</span>, systemImage: <span class="hljs-string">"timer.circle.fill"</span>, value: .timer) { <span class="hljs-comment">// &lt;- here</span>
    <span class="hljs-type">TimerView</span>()
}
</code></pre>
<h2 id="heading-commands">Commands</h2>
<p>To enable tab switching not only by tapping or clicking on the tabs, but also through the app’s menu, I needed to add the <code>commands</code> modifier to the WindowGroup in the Scene of the App struct.</p>
<pre><code class="lang-swift"><span class="hljs-keyword">var</span> body: some <span class="hljs-type">Scene</span> {
    <span class="hljs-type">WindowGroup</span> {
        <span class="hljs-type">ContentView</span>()
            .environmentObject(appState)
    }
    .commands {
        <span class="hljs-type">CommandGroup</span>(before: .sidebar) {
            <span class="hljs-type">Divider</span>()

            <span class="hljs-type">Button</span>(<span class="hljs-string">"Timer"</span>) {
                appState.selectedTab = .timer
            }

            <span class="hljs-type">Button</span>(<span class="hljs-string">"Projects"</span>) {
                appState.selectedTab = .projects
            }

            <span class="hljs-type">Button</span>(<span class="hljs-string">"Time Entries"</span>) {
                appState.selectedTab = .timeEntries
            }

            <span class="hljs-type">Button</span>(<span class="hljs-string">"More"</span>) {
                appState.selectedTab = .more
            }

            <span class="hljs-type">Divider</span>()
        }
    }
}
</code></pre>
<p>What is happening here… In the <code>commands</code> modifier, I’ve added a <code>CommandGroup</code>, which is a group of controls that appear in the app’s menu. Notice the <code>(before: .sidebar)</code> part - it tells SwiftUI to place my custom commands before the <strong>Show/Hide Sidebar</strong> and <strong>Enter/Exit Full Screen</strong>.</p>
<p>This group contains four buttons, with a divider above and below them. Each button updates the value of <code>appState.selectedTab</code>.</p>
<p>Here’s a comparison of my app BEFORE and AFTER adding this code:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751459951713/78ae7e52-d26e-4faa-ab02-aaab5dd31f1e.jpeg" alt class="image--center mx-auto" /></p>
<p>As you may have noticed, the new commands now appear in the View menu (before the Hide Sidebar and Enter Full Screen, as specified). However, there are no keyboard shortcuts yet.</p>
<h2 id="heading-keyboard-shortcuts">Keyboard Shortcuts</h2>
<p>To assign keyboard shortcuts to commands, we need to add the <code>keyboardShortcut</code> modifier to each button in the <code>CommandGroup</code>. Like this:</p>
<pre><code class="lang-swift">.commands {
    <span class="hljs-type">CommandGroup</span>(before: .sidebar) {
        <span class="hljs-type">Divider</span>()

        <span class="hljs-type">Button</span>(<span class="hljs-string">"Timer"</span>) {
            appState.selectedTab = .timer
        }
        .keyboardShortcut(<span class="hljs-string">"1"</span>, modifiers: [.command])

        <span class="hljs-type">Button</span>(<span class="hljs-string">"Projects"</span>) {
            appState.selectedTab = .projects
        }
        .keyboardShortcut(<span class="hljs-string">"2"</span>, modifiers: [.command])

        <span class="hljs-type">Button</span>(<span class="hljs-string">"Time Entries"</span>) {
            appState.selectedTab = .timeEntries
        }
        .keyboardShortcut(<span class="hljs-string">"3"</span>, modifiers: [.command])

        <span class="hljs-type">Button</span>(<span class="hljs-string">"More"</span>) {
            appState.selectedTab = .more
        }
        .keyboardShortcut(<span class="hljs-string">"4"</span>, modifiers: [.command])

        <span class="hljs-type">Divider</span>()
    }
}
</code></pre>
<p>Since the <strong>⌘Command</strong> key is the default modifier for keyboard shortcuts, we can also write it like this:</p>
<pre><code class="lang-swift">.keyboardShortcut(<span class="hljs-string">"1"</span>)

<span class="hljs-comment">// Instead of this:</span>
<span class="hljs-comment">// .keyboardShortcut("1", modifiers: [.command])</span>
</code></pre>
<p>With the <code>keyboardShortcut</code> modifiers, my commands now have shortcuts assigned to them:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1751456973229/47776331-cf54-4de1-a5f5-05db91c53518.jpeg" alt class="image--center mx-auto" /></p>
<h2 id="heading-alternative-code-optional">Alternative code (Optional)</h2>
<p>Your code may be simpler than mine. I’ve created my ObservableObject <code>AppState</code> for other reasons. For example, I’ve added keyboard shortcuts to create a New Project or New Task with ⌘N and ⌘T, and I have some conditional logic as well.</p>
<p>However, if your app is simpler and all you need are keyboard shortcuts to switch tabs, you can just use a State variable along with a <code>Tab</code> enum:</p>
<pre><code class="lang-swift"><span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">Tab</span>: <span class="hljs-title">Hashable</span> </span>{
    <span class="hljs-keyword">case</span> timer, projects, timeEntries, more
}
</code></pre>
<p>Since we want to switch tabs using commands attached to the <code>WindowGroup</code>, we need to have a <code>selectedTab</code> variable in the <code>App</code> struct:</p>
<pre><code class="lang-swift">@<span class="hljs-type">State</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> selectedTab: <span class="hljs-type">Tab?</span> = .timer
</code></pre>
<p>The <code>commands</code> in the <code>App</code> would use this State variable and look like this:</p>
<pre><code class="lang-swift">.commands {
    <span class="hljs-type">CommandGroup</span>(before: .sidebar) {
        <span class="hljs-type">Divider</span>()

        <span class="hljs-type">Button</span>(<span class="hljs-string">"Timer"</span>) {
            selectedTab = .timer
        }
        .keyboardShortcut(<span class="hljs-string">"1"</span>)

        <span class="hljs-type">Button</span>(<span class="hljs-string">"Projects"</span>) {
            selectedTab = .projects
        }
        .keyboardShortcut(<span class="hljs-string">"2"</span>)

        <span class="hljs-type">Button</span>(<span class="hljs-string">"Time Entries"</span>) {
            selectedTab = .timeEntries
        }
        .keyboardShortcut(<span class="hljs-string">"3"</span>)

        <span class="hljs-type">Button</span>(<span class="hljs-string">"More"</span>) {
            selectedTab = .more
        }
        .keyboardShortcut(<span class="hljs-string">"4"</span>)

        <span class="hljs-type">Divider</span>()
    }
}
</code></pre>
<p>In <code>ContentView</code>, we need to have a binding to the <code>selectedTab</code> variable:</p>
<pre><code class="lang-swift">@<span class="hljs-type">Binding</span> <span class="hljs-keyword">var</span> selectedTab: <span class="hljs-type">Tab?</span>
</code></pre>
<p>And the <code>Tabview</code> would look like this:</p>
<pre><code class="lang-swift"><span class="hljs-type">TabView</span>(selection: $selectedTab) {
    <span class="hljs-type">Tab</span>(<span class="hljs-string">"Timer"</span>, systemImage: <span class="hljs-string">"timer.circle.fill"</span>, value: .timer) {
        <span class="hljs-type">TimerView</span>()
    }

    <span class="hljs-comment">// Other tabs</span>
}
</code></pre>
<p>So basically, we would use <code>$selectedTab</code> as the <code>selection</code>, not <code>$appState.selectedTab</code>.</p>
<p>And you would need to pass <code>selectedTab</code> value to <code>ContentView</code> in your <code>WindowGroup</code>, like this:</p>
<pre><code class="lang-swift"><span class="hljs-type">WindowGroup</span> {
    <span class="hljs-type">ContentView</span>(selectedTab: $selectedTab)
}
</code></pre>
<p>And that’s it! Now you know how to add keyboard shortcuts and commands to switch tabs in your iPadOS or macOS apps 🙂</p>
<h2 id="heading-thank-you-for-reading">Thank you for reading!</h2>
<p>If you want to support my work, please like, comment or share the article.<br />And most importantly...</p>
<p>📱Check out my apps on the App Store:<br /><a target="_blank" href="https://apps.apple.com/developer/next-planet/id1495155532">https://apps.apple.com/developer/next-planet/id1495155532</a></p>
<p>☕ If you like what I do, consider supporting me on Ko-fi! Every little bit means the world!<br /><a target="_blank" href="https://ko-fi.com/kslazinski">https://ko-fi.com/kslazinski</a></p>
]]></content:encoded></item><item><title><![CDATA[iOS app on Macs with Apple silicon]]></title><description><![CDATA[Before I make a native macOS version of my app Wins, I decided to implement a temporary solution: making the iPad version of my app available on Macs with Apple silicon. At least in my case, this turned out to be easier than expected.
Making the app ...]]></description><link>https://blog.next-planet.com/ios-app-on-macs-with-apple-silicon</link><guid isPermaLink="true">https://blog.next-planet.com/ios-app-on-macs-with-apple-silicon</guid><category><![CDATA[iOS]]></category><category><![CDATA[ios app development]]></category><category><![CDATA[macOS]]></category><category><![CDATA[SwiftUI]]></category><category><![CDATA[Swift]]></category><category><![CDATA[apple silicon]]></category><dc:creator><![CDATA[Kris Slazinski]]></dc:creator><pubDate>Thu, 12 Sep 2024 14:13:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1726126576672/119a95d0-09d9-4d20-8b1a-4c67bf93a33d.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Before I make a native macOS version of my app <a target="_blank" href="https://apple.co/3mk0NjK">Wins</a>, I decided to implement a temporary solution: making the iPad version of my app available on Macs with Apple silicon. At least in my case, this turned out to be easier than expected.</p>
<p>Making the app available on Macs is a simple process. The first step is to add the correct destination for the app target in Xcode:</p>
<ol>
<li><p>Select the app target (in my case, Wins).</p>
</li>
<li><p>Go to <strong>General</strong> tab and the <strong>Supported Destinations</strong> section.</p>
</li>
<li><p>Click the plus button.</p>
</li>
<li><p>Choose: Mac / Designed for iPad (or "Designed for iPhone" if the app is only for iPhone).</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726121518279/e0372fa0-e39e-4de7-9849-968c44be17e3.jpeg" alt class="image--center mx-auto" /></p>
<p>Now when I select <strong>My Mac (Designed for iPad)</strong> as the Run Destination and run the app, I can see how my iPad app looks on a Mac.</p>
<h2 id="heading-mac-and-ipad-apps-are-not-the-same">Mac and iPad apps are not the same</h2>
<p>Let’c clarify something - an iPad app on a Mac doesn’t look exactly like it does on an iPad. It looks very similar, but there are some differences:</p>
<ul>
<li><p>UI components are a bit different on macOS (Search, pickers, etc.)</p>
</li>
<li><p>animations are different (openine and closing sheets, for example)</p>
</li>
<li><p>colors (list backgrounds, for example)</p>
</li>
</ul>
<p>While most colors look the same, I am using <code>Color(.tertiarySystemBackground)</code> as the background color for some UI elements in my app. Why? Because I needed a white background in Light Mode and gray background in Dark Mode and thanks to system colors it’s easy to achieve without defining custom colors.</p>
<p>But… surprise, surprise - <code>tertiarySystemBackground</code> on macOS is not white 😅</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726144662455/c772543c-b636-428f-b911-87154e050b66.jpeg" alt class="image--center mx-auto" /></p>
<p>To fix this, I needed to conditionaly change the background color to <code>Color(.secondarySystemGroupedBackground)</code>, which on macOS is white in Light Mode and gray in Dark Mode - exactly what I need.</p>
<p>But I’ve encountered a small problem…</p>
<h2 id="heading-how-to-conditionally-change-the-ui-on-macos">How to conditionally change the UI on macOS</h2>
<p>My first reaction was to use Swift’s compiler directive <code>#if</code>, like this:</p>
<pre><code class="lang-swift">#<span class="hljs-keyword">if</span> os(macOS)
.background(<span class="hljs-type">Color</span>(.secondarySystemGroupedBackground))
#<span class="hljs-keyword">else</span>
.background(<span class="hljs-type">Color</span>(.tertiarySystemBackground))
#endif
</code></pre>
<p>But… since this condition runs at compile time and the iPad and Mac use the same build in this case, it doesn’t solve the problem.</p>
<p>Another idea I had was to use <code>userInterfaceIdiom</code>:</p>
<pre><code class="lang-swift"><span class="hljs-keyword">var</span> backgroundColor: <span class="hljs-type">Color</span> {
    <span class="hljs-keyword">if</span> <span class="hljs-type">UIDevice</span>.current.userInterfaceIdiom == .mac {
        <span class="hljs-type">Color</span>(.secondarySystemGroupedBackground)
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-type">Color</span>(.tertiarySystemBackground)
    }
}

<span class="hljs-comment">// And later use backgroundColor in the modifier like this:</span>
.background(backgroundColor)
</code></pre>
<p>But again, this doesn’t work. When an iPad app runs on a Mac, <code>userInterfaceIdiom</code> returns <code>.pad</code>, which means that <code>backgroundColor</code> would always be set to <code>tertiarySystemBackground</code>, regardless of whether the app is running on an iPad or a Mac.</p>
<p>Fortunately, there is another option: we can use the <a target="_blank" href="https://developer.apple.com/documentation/foundation/processinfo">ProcessInfo</a> class to handle this. Here’s how:</p>
<pre><code class="lang-swift"><span class="hljs-keyword">var</span> backgroundColor: <span class="hljs-type">Color</span> {
    <span class="hljs-keyword">if</span> <span class="hljs-type">ProcessInfo</span>.processInfo.isiOSAppOnMac {
        <span class="hljs-type">Color</span>(.secondarySystemGroupedBackground)
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-type">Color</span>(.tertiarySystemBackground)
    }
}

<span class="hljs-comment">// And later use backgroundColor in the modifier like this:</span>
.background(backgroundColor)
</code></pre>
<p>And now, colors in my app look the same as I wanted on both iPad and Mac 😍</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726144731802/4e478392-babb-488b-99ab-2635e010d771.jpeg" alt class="image--center mx-auto" /></p>
<h2 id="heading-app-store-connect">App Store Connect</h2>
<p>The last step was to set the availability on App Store Connect. I needed to check “Make this app available” on the <strong>Pricing and Availability</strong> page, under the section <strong>iPhone and iPad Apps on Apple Silicon Macs.</strong></p>
<p>After I checked the “Make this app available”, I also needed to click the <strong>Verify Compatibility</strong> button. The prompt says: <em>“(…) If you've tested your app on Apple silicon Mac, and it functions as intended, verify it below.”</em><br />So, it’s up to the developer to test whether the app works as intended. If it does, we simply need to confirm that we have verified compatibility, which removes the “Not verified for macOS” label on the App Store that you may see on some apps.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726145535577/8d66fc57-888a-4633-8b2b-c269fe0d5c20.jpeg" alt class="image--center mx-auto" /></p>
<p>I set my app’s availability after my new build with the color corrections was approved, and the new version of my app was released and… Voilà! My iOS and iPadOS app now works on Macs with Apple silicon and looks as intended. Next step: native macOS app! 😀</p>
<h2 id="heading-thank-you-for-reading">Thank you for reading!</h2>
<p>If you want to support my work, please like, comment or share the article.<br />And most importantly...</p>
<p>📱Check out my apps on the App Store:<br /><a target="_blank" href="https://apps.apple.com/developer/next-planet/id1495155532">https://apps.apple.com/developer/next-planet/id1495155532</a></p>
<p>☕ If you like what I do, consider supporting me on Ko-fi! Every little bit means the world!<br /><a target="_blank" href="https://ko-fi.com/kslazinski">https://ko-fi.com/kslazinski</a></p>
]]></content:encoded></item><item><title><![CDATA[How to display percentage values in SwiftUI]]></title><description><![CDATA[Lately, one user of my mood tracking app Emo requested that I add percentage values to my Statistics screen. Since displaying percentage values is a little harder than displaying numbers, I thought I would share a few ways of doing this in this artic...]]></description><link>https://blog.next-planet.com/how-to-display-percentage-values-in-swiftui</link><guid isPermaLink="true">https://blog.next-planet.com/how-to-display-percentage-values-in-swiftui</guid><category><![CDATA[ios app development]]></category><category><![CDATA[Swift]]></category><category><![CDATA[SwiftUI]]></category><category><![CDATA[app development]]></category><category><![CDATA[Indie Maker]]></category><category><![CDATA[indiedev]]></category><category><![CDATA[iOS]]></category><category><![CDATA[string]]></category><dc:creator><![CDATA[Kris Slazinski]]></dc:creator><pubDate>Fri, 21 Jun 2024 07:20:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1718952443822/87385767-f1d9-4ada-aafc-d4401cb52347.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Lately, one user of my mood tracking app <a target="_blank" href="https://apple.co/3Vh0en4">Emo</a> requested that I add percentage values to my Statistics screen. Since displaying percentage values is a little harder than displaying numbers, I thought I would share a few ways of doing this in this article.</p>
<p>Let's say we have two Doubles and a total value equal to the sum of them.</p>
<pre><code class="lang-swift"><span class="hljs-keyword">let</span> value1: <span class="hljs-type">Double</span> = <span class="hljs-number">1</span>
<span class="hljs-keyword">let</span> value2: <span class="hljs-type">Double</span> = <span class="hljs-number">2</span>
<span class="hljs-keyword">let</span> total = value1 + value2 <span class="hljs-comment">// 3</span>
</code></pre>
<h2 id="heading-option-1">Option 1</h2>
<p>Now, if we want to check what percentage of the total is value1 and display this percentage in SwiftUI with 0 fraction digits, we could do something like this:</p>
<pre><code class="lang-swift"><span class="hljs-type">Text</span>(<span class="hljs-string">"\(String(format: "</span>%.0f<span class="hljs-string">", (value1 / total * 100)))%"</span>)
</code></pre>
<h2 id="heading-option-2">Option 2</h2>
<p>Since iOS 15, there is another way, though, and you may find it more readable:</p>
<pre><code class="lang-swift"><span class="hljs-type">Text</span>((value1 / total).formatted(
    .percent.precision(.fractionLength(<span class="hljs-number">0</span>)))
)
</code></pre>
<p>Both options will result in the same text:<br /><strong>33% (for value1)<br />67% (for value2)</strong></p>
<p>As you can probably guess, to change the precision to display 2 fraction digits in your percentages, you can modify both versions of the code as follows:</p>
<pre><code class="lang-swift"><span class="hljs-type">Text</span>(<span class="hljs-string">"\(String(format: "</span>%.2f<span class="hljs-string">", (value1 / total * 100)))%"</span>) <span class="hljs-comment">// %.2f</span>

<span class="hljs-type">Text</span>((value1 / total).formatted(
    .percent.precision(.fractionLength(<span class="hljs-number">2</span>))) <span class="hljs-comment">// fractionLength(2)</span>
)
</code></pre>
<p>When you display fraction digits, you will notice another difference between both methods. The first one will always display percentage values the same way, as it's a hard-coded format (for example, <strong>33.33%</strong>).</p>
<p>The second one, using the <code>formatted()</code> method, takes the user's locale into consideration. So the percentage value may be displayed as 33.33% or 33,33%, depending on the locale. The same applies to other locale-specific rules, like grouping separators (e.g., <strong>"10,000"</strong> in one country and <strong>"10 000"</strong> in another).</p>
<p>This means that the second option is not only more readable but also a better choice if the app is localized, as with the <code>formatted()</code> method, all the values will be properly formatted based on the user's locale.</p>
<h2 id="heading-update-option-3">UPDATE: Option 3</h2>
<p>After I published this article, <a target="_blank" href="https://mastodon.world/@luckkerr">Łukasz Rutkowski</a> reminded me on Mastodon about another similar method (which also formats numbers properly based on locale):</p>
<pre><code class="lang-swift"><span class="hljs-type">Text</span>((value1 / total),
    format: .percent.precision(.fractionLength(<span class="hljs-number">0</span>))
)
</code></pre>
<p>If you know another way to display percentages in SwiftUI, feel free to share it in the comments.</p>
<h2 id="heading-thank-you-for-reading">Thank you for reading!</h2>
<p>If you want to support my work, please like, comment, share the article and most importantly...</p>
<p>📱Check out my apps on the App Store:<br /><a target="_blank" href="https://apps.apple.com/developer/next-planet/id1495155532">https://apps.apple.com/developer/next-planet/id1495155532</a></p>
<p>☕ If you like what I do, consider supporting me on Ko-fi! Every little bit means the world!<br /><a target="_blank" href="https://ko-fi.com/kslazinski">https://ko-fi.com/kslazinski</a></p>
<h3 id="heading-credits">Credits</h3>
<p>Thank you to <a target="_blank" href="https://x.com/sowenjub">Arnaud Joubay</a> and <a target="_blank" href="https://mastodon.social/@averyvine">Avery Vine</a> for your input on app localization, and to <a target="_blank" href="https://mastodon.world/@luckkerr">Łukasz Rutkowski</a> for suggesting Option 3.</p>
]]></content:encoded></item><item><title><![CDATA[Problems with AB testing]]></title><description><![CDATA[Two months ago, I decided to run an AB test for one of my iOS apps, a time tracker named Moons. It's a free-to-download app with basic functionality available free of charge and some pro features behind a paywall. I wanted to check how changing my ap...]]></description><link>https://blog.next-planet.com/problems-with-ab-testing</link><guid isPermaLink="true">https://blog.next-planet.com/problems-with-ab-testing</guid><category><![CDATA[iOS]]></category><category><![CDATA[ios app development]]></category><category><![CDATA[appstore]]></category><category><![CDATA[ASO]]></category><category><![CDATA[App Store Optimization]]></category><category><![CDATA[buildinpublic]]></category><category><![CDATA[#ABTesting]]></category><dc:creator><![CDATA[Kris Slazinski]]></dc:creator><pubDate>Fri, 08 Mar 2024 10:56:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1709886360440/5ef8af44-aed8-4f20-bd02-c31abdfb8b78.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Two months ago, I decided to run an AB test for one of my iOS apps, a time tracker named <a target="_blank" href="https://apple.co/49soMAO">Moons</a>. It's a free-to-download app with basic functionality available free of charge and some pro features behind a paywall. I wanted to check how changing my app screenshots would affect my app's Conversion Rate.</p>
<p>To clarify, here is how Apple defines this metric on the App Store Connect where we can run such tests:</p>
<p><em>"The estimated percentage of users who download or pre-order the app after seeing this variant of the product page."</em></p>
<p>This basically means that I want to improve <a target="_blank" href="https://apple.co/49soMAO">Moon's</a> Product Page so that more people download the app. Whether these people decide to pay for the pro features later is a different story. Here, I simply want to increase the number of downloads to have more potential customers.</p>
<h2 id="heading-hypothesis">Hypothesis</h2>
<p>My idea is simple. I will add some cool new image to my product page to make my app stand out from the crowd. This image will resonate with users on an emotional level, inspiring them to achieve their goals and boost their productivity. This will result in increased downloads and, ideally, more paying customers.</p>
<p>Simple and brilliant and the question is not IF but HOW EFFECTIVE it will be.</p>
<h2 id="heading-creative-screenshots">Creative Screenshots</h2>
<p>Apple calls them <strong>Screenshots</strong> on the App Store Connect. But in the context of App Store Optimization, they are often called <strong>Creatives</strong>. Basically, they are images people see when they are searching for apps on the App Store.</p>
<p>I decided to get creative with my <strong>Creatives</strong> 🙃 And here's the result: an astronaut on the moon, a few catchy marketing slogans and a glimpse of my app's UI in the foreground. Pretty cool, isn't it?</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709886667915/e9f1272e-c3f0-46f7-94b2-311a1a6c7c37.jpeg" alt class="image--center mx-auto" /></p>
<p>I wanted to include this particular image in my creatives. Here's a comparison between my Original Product Page and Treatment A (this is Apple's term for sets of images used in AB tests, which you'll see in the results later in this article).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709886630500/4f266ce1-9967-47fc-b432-ebf1150afa48.jpeg" alt class="image--center mx-auto" /></p>
<p>Why does adding or changing just one image matter? Because users typically see the first three creatives for each app in the search results. Therefore, to improve the initial step of my sales funnel - the search results - I've decided to test these two variants:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709886777317/1002d4e6-b724-4a68-8c48-99afe9d12f9f.jpeg" alt class="image--center mx-auto" /></p>
<h2 id="heading-ab-testing-on-family-and-friends">AB testing on family and friends</h2>
<p>I showed this new creative to some friends and people in my family. I heard things like:</p>
<p><em>"Smart marketing!"</em></p>
<p><em>"Great idea! It tells people that with your app they can reach their goals. And anything is possible, even landing on a moon."</em></p>
<p>At this point I am a creative genius! And people believe in my success. Awesome!</p>
<h2 id="heading-ab-testing-on-social-media">AB testing on Social Media</h2>
<p>Right before writing this article I posted my creative on social media and asked people post-factum:</p>
<p><em>"Can you guess by how much adding this one image to my Product Page changed my Conversion Rate?"</em></p>
<p>iOS dev community is really great! Full of positive and supportive people. I got many answers with numbers between +7% to +500%. Here are 2 of the comments I really liked:</p>
<p><em>"(...) I think the impressions/downloads rate must have gone up dramatically.<br />Perhaps even a few hundred percent.<br />Because seeing a plain more or less standard iOS screenshot doesn't stop people in their App Store scrolling tracks."</em><br />(Wow! That's so aligned with my hypothesis.)</p>
<p><em>"I’d say it increased a lot and mostly by people that want to go to the moon.🌙😂"</em><br />(Positive and funny. I love it!)</p>
<h2 id="heading-ab-testing-on-the-app-store">AB testing on the App Store</h2>
<p>Now let's find out how did the App Store test go. My AB test was active for 64 days.</p>
<h2 id="heading-day-1">Day 1</h2>
<p>That's strange 🤔 Conversion Rate lower by 68.09%? But no worries. It's just one day. Not enough data. It will look much better soon ☺️</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709886867861/f1ff72aa-b70f-426b-af77-0036075b4861.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-day-7">Day 7</h2>
<p>See? 😀 I was right! After one week we have a nice improvement! +10.22% to my Conversion Rate and +10 my business skills.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709886877919/fa69a580-07a0-4ebe-9ce7-b54b3531a75b.png" alt class="image--center mx-auto" /></p>
<p>One day earlier I posted my first results on social media:</p>
<p><em>"🥸 I don’t want to brag, but… I managed to improve conversion rate of my iOS app Moons by 8.78% with a… 0.1% confidence 😅"</em></p>
<p>I got a lot of support from my wonderful indie dev community. Likes, virtual high-fives. And I am sure that CEOs of big app companies were getting anxious at this point. That's one small step for an indie developer, one giant leap for the whole indie community!</p>
<p>But I kept testing... Because everyone ignored one part of my post:</p>
<p><em>"(...) with a… 0.1% confidence 😅"</em></p>
<p>Which basically means that I didn't have enough data yet.</p>
<h2 id="heading-day-51">Day 51</h2>
<p><em>"😱Noooooo! My numbers are going down!"</em></p>
<p>At some point my Conversion Rate went down by 15.32%. Breath, Kris! Don't panic. Stay calm and keep going.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709886887963/29202458-bb07-4273-8cab-d1fcf151a1b5.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-day-64">Day 64</h2>
<p>Now that's not funny anymore! 🥲 After the initial spike there were small changes, but overall my brilliant and creative Creative (AKA Treatment A) performs worse than the Original Product Page.</p>
<p>I decided to stop the test at this point. Especially that I released new version of the app with new localization and new screenshots and this would only skew my results at this point.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709886897682/6a038ce6-e5f0-4f53-813f-63b7ad2b60e5.png" alt class="image--center mx-auto" /></p>
<p>Please note, however, that these results are marked as <strong>Inconclusive</strong>.</p>
<p>Apple says:<br /><em>"There is not enough data to determine whether this variant performs differently from the baseline."</em></p>
<p>And I will come back to this later. But the main question is... What went wrong?!</p>
<p>Remember this one comment?</p>
<p><em>"I’d say it increased a lot and mostly by people that want to go to the moon.🌙😂"</em></p>
<p>This person predicted that I will have positive results, but maybe the most interesting part is this one:</p>
<p><em>"(...) mostly by people that want to go to the moon."</em></p>
<p>Maybe my creative image would work great for an Earth to Moon transportation company but not for a time tracking app? But that's not the only problem.</p>
<h2 id="heading-problems-with-ab-testing">Problems with AB testing</h2>
<p>Here are some key takeaways:</p>
<ul>
<li><p><strong>AB testing on friends and social media will not give you accurate results</strong><br />  Definitely do it! It's motivating. You will hear great ideas. Maybe you will learn from someone who already did some tests before. But you simply won't have enough data. And also your family and friends are very biased. They care about you and wish you well, so their decisions are often influenced by emotions rather than financial considerations.</p>
</li>
<li><p><strong>Confidence matters</strong><br />  Don't ignore this metric. My Conversion Rate increased by 10% after one week, but then decreased by 10% after two months. And yes, I am aware that my results are inconclusive. With a confidence level of 5.8%, there is nearly a 95% probability that the final results could differ significantly.</p>
</li>
<li><p><strong>You need a lot of data</strong><br />  And that's the main reason why I stopped the test. I can't wait another 6 months for conclusive results. I need to get more impressions first (for example, using Apple's Search Ads) and I may run the same test again in the future.</p>
</li>
<li><p><strong>Not every change is a big change</strong><br />  I want to run some other tests and check if making other changes will be more impactful. The bigger the change, the less data I would potentially need.</p>
</li>
<li><p><strong>Marketing images instead of app screenshots may or may not work</strong></p>
<p>  I need to experiment more with this idea. Whole concept may be wrong. Or just the photo should be different. Or the text/message should be different. We won't know if we will not try.</p>
</li>
<li><p><strong>Your results may be completely different</strong><br />  Using the same creatives, the same images and marketing slogans you could see completely different results. Different app. Different audience. Different keywords. There are so many factors. The only way to know what works for your app is to test it.</p>
</li>
<li><p><strong>Your gut and your data are two different things</strong></p>
<p>  Many people say that you should trust your gut. And I agree. You should definitely hire James Gut in your company. He's such a creative guy! But if you understand his strengths and weaknesses, you will put him in the Hypothesis department along with your family and friends. You want Stephanie Data in your Business Decissions department.</p>
</li>
</ul>
<h2 id="heading-thank-you-for-reading">Thank you for reading!</h2>
<p>I hope you found this article useful or at least entertaining. Please follow me for more tips on how to decrease your Conversion Rate 😅</p>
<p>And if you want to support my work, please like, comment, share the article and most importantly...</p>
<p>📱Check out my apps on the App Store:<br /><a target="_blank" href="https://apps.apple.com/developer/next-planet/id1495155532">https://apps.apple.com/developer/next-planet/id1495155532</a></p>
<p>☕ If you like what I do, consider supporting me on Ko-fi! Every little bit means the world!<br /><a target="_blank" href="https://ko-fi.com/kslazinski">https://ko-fi.com/kslazinski</a></p>
]]></content:encoded></item><item><title><![CDATA[2 years on the App Store]]></title><description><![CDATA[When I published my first iOS app on the App Store in February 2022 I didn’t know what to expect. My main goals were: to learn how to code and make apps that will help me solve my own problems. And while I was also thinking that it would be cool to m...]]></description><link>https://blog.next-planet.com/2-years-on-the-app-store</link><guid isPermaLink="true">https://blog.next-planet.com/2-years-on-the-app-store</guid><category><![CDATA[app development]]></category><category><![CDATA[ios app development]]></category><category><![CDATA[Indie Maker]]></category><category><![CDATA[appstore]]></category><category><![CDATA[Build In Public]]></category><category><![CDATA[iOS]]></category><category><![CDATA[indie-hacker]]></category><dc:creator><![CDATA[Kris Slazinski]]></dc:creator><pubDate>Fri, 16 Feb 2024 08:14:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1708001293922/72a1bc6e-139a-4a7f-b43e-48e47a2da107.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When I published my first iOS app on the App Store in February 2022 I didn’t know what to expect. My main goals were: to learn how to code and make apps that will help me solve my own problems. And while I was also thinking that it would be cool to make money on this, I didn’t have a clue when I will make my first dollar and how much money can I earn a month from my apps.</p>
<p>After exactly 2 years I am starting to understand what’s possible and how much time and effort it takes. And I hope that with this article you will see this as well. Important note, though - condition of the app business depends on many factors. I personally know many indie app creators that are more successful financially but also many folks that are not there yet. But I will share some insights and maybe this will give you some idea how the financial part of making iOS apps looks like if you are an indie developer.</p>
<h2 id="heading-the-product-my-apps-on-the-app-store">The product - my apps on the App Store</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708069923397/914d29cf-da6f-47d7-8bfa-a75f90da76b8.png" alt class="image--center mx-auto" /></p>
<p>Shoutout to future me with his 100 apps on all Apple devices. Poor guy needs a whole month and 3 AI assistants just to prepare screenshots for all of this.</p>
<p>But luckily, right now I still have only 5 iOS apps. And here they are with release dates - so if you want, you can compare this to my Proceeds charts to see how each of the apps affected my revenue.</p>
<ul>
<li><p><a target="_blank" href="https://apple.co/49soMAO">Moons</a> - Time tracking and project management<br />  November 10, 2023</p>
</li>
<li><p><a target="_blank" href="https://apple.co/3mk0NjK">Wins</a> - Setting and tracking personal goals<br />  April 12, 2023</p>
</li>
<li><p><a target="_blank" href="https://apple.co/3WjbjnR">Numi</a> - Expense tracker<br />  December 22, 2022</p>
</li>
<li><p><a target="_blank" href="https://apple.co/3Vh0en4">Emo</a> - Emotional inventory and journaling<br />  October 10, 2022</p>
</li>
<li><p><a target="_blank" href="https://apple.co/3BWk1zf">Skoro</a> - Scoreboard app for sports and games<br />  February 26, 2022</p>
</li>
</ul>
<h2 id="heading-sales-vs-proceeds">Sales vs Proceeds</h2>
<p>All the screenshots you will see here are from Apple’s App Store Connect - a platform for app developers. Apple shows us a lot of data there. Two of the many metrics are Sales and Proceeds.</p>
<p><strong>Sales</strong> - this is how much money your apps generated.<br /><strong>Proceeds</strong> - Sales minus some country specific taxes and Apple’s fee. For most developers this fee is 15% or 30% of your Sales.<br />So in other words, Proceeds is what you will actually get from Apple.</p>
<p>Because I want to show you my real revenue as an app developer, all the screenshots you can see here show my Proceeds, not Sales.</p>
<h2 id="heading-2023">2023</h2>
<p>I made $1.270 on my apps in 2023. For some it's a lot, for some it's not. I always remind myself that it's not a sprint but a marathon. I am just starting and it takes time. And while I am aiming for better results in 2024, I am happy with the results so far.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708004077889/2556d217-a48f-46b7-8410-58f4f8188293.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-2022-vs-2023">2022 vs 2023</h2>
<p>To give you more perspective, this is how last year compares to 2022 when I started.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708004484589/13eb6edd-293a-402c-957e-27d2fcbfdd16.png" alt class="image--center mx-auto" /></p>
<p>There is some progress here, right? But it doesn't look super constant. There are ups and downs. But you know what? Something that I learned after 2 years - you can't focus too much on the short-term results.</p>
<p>Let's zoom in for a moment to see how my daily proceeds looked like.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708004628884/ca428262-b1d5-464c-9a2e-bd978a6a8736.png" alt class="image--center mx-auto" /></p>
<p>Looking at daily results you might think that I had a 1 great day. Can we stop for a moment here? I just want to say that I appreciate you, November 10 🙌 I wish more days were like you 😅</p>
<p>But even though not all my days brought $109, the long-term results are made of many many small steps. So let's zoom out to see my Proceeds per Quarter.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708004927102/6cd7f8da-c893-4124-89d1-0bef454f6baf.png" alt class="image--center mx-auto" /></p>
<p>Looks better, right? If we zoom out even more and you look at Proceeds by Year, this progress looks even better.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708005107533/dcecfb8d-60cf-4521-b7bc-df7c34c3892b.png" alt class="image--center mx-auto" /></p>
<p>Again - someone will say it's a lot. For someone else it's not much. If you are building your business, you need to remember exactly this - it's YOUR business. And only you can decide if it's good or not.</p>
<p>And you definitely need some patience.<br />Step by step. Ooh baby! Gonna get to you... wealth.</p>
<h2 id="heading-january-2024">January 2024</h2>
<p>2024 started really good for me. I reached a new record: $217 in one month. Which is slightly better than November 2023 (my previous best).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708005723469/b6da9786-db42-49a6-9d67-b68cbe16ad72.png" alt class="image--center mx-auto" /></p>
<p>But again, it's important how you look at your data. If we compare January 2024 to January 2023, the situation looks pretty good.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708005775863/44a5a649-7a45-4d64-94fe-60ea33a54f4d.png" alt class="image--center mx-auto" /></p>
<p>Will my proceeds keep growing? By 271% again? Slower? Or maybe faster? I do hope. I am definitely very excited to see where my app business will be next year. But you know, I am a bit biased 😉</p>
<h2 id="heading-costs"><strong>Costs</strong></h2>
<p>Let's talk about costs for a moment. Having only the proceeds part doesn’t tell you the whole story and I want to be very transparent here.</p>
<p>I didn’t spend a lot. I pay $99/year for the Apple Developer account, so it’s $198 for 2 years. (There is also a Free account, but to distribute apps on the App Store you need the paid one). I tried Apple’s Search Ads a few months ago, but I only used the free $100 credit you get from Apple when you create your Ads account. And I turned off the ads few days after I used the free $100 so I spent maybe $10 of my own money.</p>
<p>And that’s it. I decided to not use any paid APIs in my apps. And so far I am not outsourcing any design or developer work. This could change if I would make more money and have the budget for this, but I wanted to be profitable since day 1, so right now it’s just me.</p>
<h2 id="heading-who-and-what-helped-me">Who and what helped me</h2>
<p>But to make it a "build in public", not a "brag in public" article, I would like to share some general info who and what helped me to get to this point.</p>
<ul>
<li><p>iOSdev community - many people helped me with their advice, emotional support. Sometimes someone helped me with some code, often people were offering their help with localizing my app.<br />  I would like to thank everyone collectively. I always do this individually anytime someone helpes me. I definitely need to write a separate article about all of you folks, because I wouldn't be here without you ❤️</p>
</li>
<li><p>#buildinpublic - sharing what you know and how you build your apps wth others is not just a great marketing but it's also a great networking opportunity and a chance to learn from others.</p>
</li>
<li><p>ASO - researching keywords with <a target="_blank" href="https://tryastro.app?aff=bDP8D">Astro</a></p>
</li>
<li><p>Localizations - I localized my apps to many languages. And my keywords to even more languages.</p>
</li>
<li><p>New apps - everytime I released a new app, it boosted my sales. But I don't think it's very scalable. More apps doesn't neccesarily mean more money. There are pros and cons of having many apps and I think it deserves a separate article.</p>
</li>
</ul>
<p>To be honest, all of these things need a more in-depth articles. And I will try to share more soon. If you are interested in any of these topics or have some other questions, please leave a comment or contact me directly.</p>
<h2 id="heading-whats-next">What's next</h2>
<p>As many indie creators, I would like to change my app business into a sustainbale one. My focus is still on building useful apps with the best quality and User Experience that I can deliver right now with the knowledge and resources I have. But to make it possible for the long run I need improve the business aspect of this.</p>
<p>Here are some of my ideas how to get there:</p>
<ul>
<li><p>macOS versions of some of my apps</p>
</li>
<li><p>More ASO experiments</p>
</li>
<li><p>Submitting my apps to be featured on the App Store</p>
</li>
<li><p>Experimenting with business models of my apps</p>
</li>
<li><p>Trying Apple Search Ads again</p>
</li>
</ul>
<h2 id="heading-thank-you-for-reading">Thank you for reading!</h2>
<p>If you want to support my work, please like, comment, share the article and most importantly...</p>
<p>📱Check out my apps on the App Store:<br /><a target="_blank" href="https://apps.apple.com/developer/next-planet/id1495155532">https://apps.apple.com/developer/next-planet/id1495155532</a></p>
<p>☕ If you like what I do, consider supporting me on Ko-fi! Every little bit means the world!<br /><a target="_blank" href="https://ko-fi.com/kslazinski">https://ko-fi.com/kslazinski</a></p>
]]></content:encoded></item><item><title><![CDATA[Naming conventions - Core Data and SwiftData]]></title><description><![CDATA[In this article, I will show you what naming conventions I am using for Core Data entities and attributes and how SwiftData changed my approach. I am not saying that this convention is the best one and the one you should use. But I want to tell you w...]]></description><link>https://blog.next-planet.com/naming-conventions-core-data-and-swiftdata</link><guid isPermaLink="true">https://blog.next-planet.com/naming-conventions-core-data-and-swiftdata</guid><category><![CDATA[iOS]]></category><category><![CDATA[backend]]></category><category><![CDATA[data structures]]></category><category><![CDATA[Core data]]></category><category><![CDATA[SwiftData]]></category><dc:creator><![CDATA[Kris Slazinski]]></dc:creator><pubDate>Wed, 25 Oct 2023 05:56:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1698212421873/4a6e0739-928f-4bbf-b6e9-f5134cdf529f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this article, I will show you what naming conventions I am using for Core Data entities and attributes and how SwiftData changed my approach. I am not saying that this convention is the best one and the one you should use. But I want to tell you why you may consider adopting it in your apps.</p>
<p>Renaming variables and constants in your code is mostly not problematic at all. But when it comes to creating your data model, it is good to plan ahead. While of course, you can rename your entities, classes and attributes later and migrate your data model, it's just easier if you use good names from the start.</p>
<h2 id="heading-core-data-entities-and-attributes">Core Data entities and attributes</h2>
<p>Let's start with an example of an app that helps you manage your trips when you travel. In most tutorials for Core Data and SwiftData, you will see the following convention:</p>
<ul>
<li><p><strong>Trip</strong> - for a Core Data entity or SwiftData class</p>
</li>
<li><p><strong>attribute</strong> - an attribute of the Trip entity</p>
</li>
<li><p><strong>trips</strong> - a collection of objects of type Trip</p>
</li>
<li><p><strong>trip</strong> - a single object from the "trips" collection</p>
</li>
</ul>
<p>That's quite simple for a single word. Titlecase singular form for entity/class, lowercase plural name for an attribute and collection and lowercase singular form for a single object.</p>
<p>Now let's say that in your app you have another entity/class for booked hotels. For names made of few words you would use this:</p>
<ul>
<li><p><strong>BookedHotel</strong> - entity/class</p>
</li>
<li><p><strong>otherAttribute</strong> - an attribute of the BookedHotel entity</p>
</li>
<li><p><strong>bookedHotels</strong> - a collection of objects</p>
</li>
<li><p><strong>bookedHotel</strong> - one object</p>
</li>
</ul>
<p>Same as before, but with a camel case for multiple words.</p>
<p>Knowing the conventions, I started to use them in my apps. And it mostly worked. Mostly 😅 But I ran into a problem very quickly with my first ever data model in my app <a target="_blank" href="https://apps.apple.com/app/id1636232810">Numi</a>, a finance app for tracking expenses.</p>
<p>For one of the entities I created in Numi, I used the name <strong>Transaction</strong>. My code wasn't working as expected and I spent many hours on debugging. At the time, I learned that there were already some Structs with the same name. Two examples are:</p>
<ul>
<li><p>in Swiftui - <a target="_blank" href="https://developer.apple.com/documentation/swiftui/transaction">https://developer.apple.com/documentation/swiftui/transaction</a></p>
</li>
<li><p>in StoreKit - <a target="_blank" href="https://developer.apple.com/documentation/storekit/transaction">https://developer.apple.com/documentation/storekit/transaction</a></p>
</li>
</ul>
<p>When I was creating a transaction object, Xcode was sometimes mistakenly assigning it to the wrong Transaction type. Most likely it was clashing with the SwiftUI's Transaction struct.</p>
<pre><code class="lang-swift"><span class="hljs-keyword">let</span> transaction = <span class="hljs-type">Transaction</span>(context: viewContext)
<span class="hljs-comment">// Or...</span>
<span class="hljs-keyword">init</span>(transaction: <span class="hljs-type">Transaction</span>) {
</code></pre>
<p>As Xcode didn't tell me what was happening, it took me some time to find what was the cause of the problem.</p>
<p>I tried a few solutions to fix this issue. And honestly, I forgot about some of them as they didn't work. What fixed my problem was very trivial. I simply added a prefix to my Core Data entities.</p>
<h2 id="heading-cde-prefix">CDE prefix</h2>
<p>Since the problem with Transaction, I started using the CDE prefix for all my Core Data entities. For example, I renamed the <strong>Transaction</strong> entity to <strong>CDETransaction</strong> and boom! Suddenly my code works without any issues.</p>
<p>You can use another prefix, of course. One example would be <strong>MyAppTransaction</strong> with a MyApp prefix. But it is not as clear as with the CDE prefix that <strong>MyAppTransaction</strong> is a Core Data Entity. And also, what if at some point you will need to change the name of your app?</p>
<p>You can see that Apple recommends a similar naming convention to my CDE prefix in this article:<br /><a target="_blank" href="https://developer.apple.com/documentation/coredata/adopting_swiftdata_for_a_core_data_app">https://developer.apple.com/documentation/coredata/adopting_swiftdata_for_a_core_data_app</a></p>
<p>For example, when they give us an example of Core Data and SwiftData co-existing in the same app, they are using the class <strong>Trip</strong> for SwiftData:</p>
<pre><code class="lang-swift">@<span class="hljs-type">Model</span> <span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Trip</span> </span>{
</code></pre>
<p>But for the Core Data entity, they are using <strong>CDTrip</strong> (with CD prefix), like this:</p>
<pre><code class="lang-swift"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CDTrip</span>: <span class="hljs-title">NSManagedObject</span> </span>{
</code></pre>
<p>And later, for adding a new Trip:</p>
<pre><code class="lang-swift"><span class="hljs-keyword">let</span> newTrip = <span class="hljs-type">CDTrip</span>(context: viewContext)
</code></pre>
<h2 id="heading-how-swiftdata-changed-my-approach">How SwiftData changed my approach</h2>
<p>During WWDC2023, Apple introduced SwiftData as a replacement for Core Data. After watching Apple's WWDC videos and reading some SwiftData tutorials, it looks like SwiftData is in most cases simpler to use than Core Data. But there are 2 problems:</p>
<ol>
<li><p>SwiftData is not mature enough in 2023. Which means there is still some functionality missing. And also there are fewer tutorials and documentation than for Core Data.</p>
</li>
<li><p>SwiftData works only with iOS 17 and up. With most people still currently using iOS 16, choosing SwiftData over Core Data would significantly limit my potential user base.</p>
</li>
</ol>
<p>What's great is that migrating from Core Data to SwiftData looks fairly simple, so I can use Core Data for now and migrate to SwiftData in the future when I decide to drop support for iOS 16.</p>
<p>But what about my naming convention with the CDE prefix? It looks like CDE wasn't the best idea, because after the SwiftData migration, the CDE prefix will not be very on point with SwiftData.</p>
<p>I want to avoid potential problems like I had with my <strong>Transaction</strong> entity, so I would prefer to use a prefix. Just not the <strong>CDE</strong>.</p>
<p>I decided to name Core Data entities in my new apps using the <strong>DM</strong> prefix instead. DM as in the Data Model, to make it less likely to introduce problems of having no prefix at all. And also to more clearly differentiate my Core Data entities from other Structs and Classes.</p>
<p>During future migration to SwiftData, I won't need to change the names of my entities and the DM prefix will be a better fit for SwiftData than the CDE prefix.</p>
<h2 id="heading-new-naming-convention">New naming convention</h2>
<p>So to summarize, that's how I am planning to name my Core Data entities, SwiftData classes and attributes:</p>
<ul>
<li><p><strong>DMTrip</strong> - entity/class in my Data Model</p>
</li>
<li><p><strong>attribute</strong> - an attribute of DMTrip in my Data Model</p>
</li>
<li><p><strong>trips</strong> - a collection of DMTrip objects</p>
</li>
<li><p><strong>trip</strong> - a single DMTrip object</p>
</li>
</ul>
<p>And for multiple words:</p>
<ul>
<li><p><strong>DMBookedHotel</strong> - entity/class in my Data Model</p>
</li>
<li><p><strong>otherAttribute</strong> - an attribute of DMBookedHotel in my Data Model</p>
</li>
<li><p><strong>bookedHotels</strong> - a collection of DMBookedHotel objects</p>
</li>
<li><p><strong>bookedHotel</strong> - a single DMBookedHotel object</p>
</li>
</ul>
<p>I'm not claiming that this is the best naming convention, but based on my experience, it's something that works for me. It helps me avoid potential problems and clearly marks my classes as part of my Data Model.</p>
<h2 id="heading-thank-you-for-reading">Thank you for reading!</h2>
<p>If you want to support my work, please like, comment, share the article and most importantly...</p>
<p>📱Check out my apps on the App Store:<br /><a target="_blank" href="https://apps.apple.com/developer/next-planet/id1495155532">https://apps.apple.com/developer/next-planet/id1495155532</a></p>
<p>☕ If you like what I do, consider supporting me on Ko-fi! Every little bit means the world!<br /><a target="_blank" href="https://ko-fi.com/kslazinski">https://ko-fi.com/kslazinski</a></p>
]]></content:encoded></item><item><title><![CDATA[UI resources for iOS developers and designers]]></title><description><![CDATA[In this article, I would like to share with you a list of UI resources I am using during my work on my iOS apps. There are so many resources that can help you with UI and UX design and with the promotion of your apps. But something I prefer is to kee...]]></description><link>https://blog.next-planet.com/ui-resources-for-ios-developers-and-designers</link><guid isPermaLink="true">https://blog.next-planet.com/ui-resources-for-ios-developers-and-designers</guid><category><![CDATA[iOS]]></category><category><![CDATA[ios app development]]></category><category><![CDATA[UI]]></category><category><![CDATA[UX]]></category><category><![CDATA[app development]]></category><dc:creator><![CDATA[Kris Slazinski]]></dc:creator><pubDate>Sun, 09 Jul 2023 08:36:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1688891708740/b7c06cd9-4bc7-468c-8c5c-d51a34034d01.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this article, I would like to share with you a list of UI resources I am using during my work on <a target="_blank" href="https://apple.co/3MblwxW">my iOS apps</a>. There are so many resources that can help you with UI and UX design and with the promotion of your apps. But something I prefer is to keep my toolbox simple and use the minimum amount of tools and resources to make the job done as fast and easiest as possible.</p>
<p>That being said, if you use something else in your workflow and would like to share this with me, please leave a comment. I would love to know.</p>
<h2 id="heading-human-interface-guidelines">Human Interface Guidelines</h2>
<p>Link: <a target="_blank" href="https://developer.apple.com/design/human-interface-guidelines">https://developer.apple.com/design/human-interface-guidelines</a></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688800926096/5f1d33c7-b34d-4ba5-973c-4dca30129b11.png" alt class="image--center mx-auto" /></p>
<p>This is one of the best places to learn about UI and UX design for Apple platforms. You will learn the basics and read about best practices and design patterns. If you are just starting or if you are a more experienced designer, this should be a place where you go to improve and update your design knowledge.</p>
<p>I recommend you read the whole documentation. But if needed, you can also search for specific topics. For example, "Color" alone is a long article about best practices, inclusive color, system colors, color management (with an example of sRGB vs Display P3 color spaces) and more. Also with color palette and RGB values for different variants of each color.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688800363554/baa4f72f-ee4b-4740-90d6-576f15a8a712.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-fonts">Fonts</h2>
<h3 id="heading-fonts-for-apple-platforms">Fonts for Apple platforms</h3>
<p>Link: <a target="_blank" href="https://developer.apple.com/fonts/">https://developer.apple.com/fonts/</a></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688801928021/6b3d519d-c4b7-4b0c-a631-5dc8dff0d9c8.png" alt class="image--center mx-auto" /></p>
<p>Here you can learn more about Apple's default fonts and download font packages.</p>
<p>Maybe you already know this, but... The default system fonts on Apple platforms are <strong>San Francisco</strong> and <strong>New York</strong> (most apps from Apple use San Francisco). These fonts come with Apple's OSes and you don't need to do anything for your devices to display them properly in the apps you are using. However, if you want to design with these fonts and be able to use them in your design tools, you need to download the fonts from Apple.</p>
<p>It's all on the website I mentioned, but I want to emphasize this: SF Compact is the system font for watchOS. And SF Arabic, SF Armenian, SF Georgian and SF Hebrew are separate font packages if you need them.</p>
<p>Also, another important thing: you only need to download these fonts for design purposes. No need to embed them in your apps developed with Xcode.</p>
<h3 id="heading-google-fonts">Google Fonts</h3>
<p>Link: <a target="_blank" href="https://fonts.google.com">https://fonts.google.com</a></p>
<p>There are many websites with fonts. Google Fonts is just an example. I chose it for a few reasons:</p>
<ul>
<li><p>It's a big library of FREE fonts.</p>
</li>
<li><p>The filters are very good, so you can easily check if the font supports the language or group of languages you need.</p>
</li>
<li><p>Google Fonts are very popular in web design. Very often you will also have a website about your app, so you could potentially use the same fonts in your app and on the website.</p>
</li>
</ul>
<p>But... I would like to give you one piece of advice here. Try to use Apple's fonts first and go for other fonts only if you really need them (font as an important part of your brand, etc.). This will make your life easier. With Apple's fonts, you don't need to embed fonts in your app. You don't need to define custom font styles. And you don't need to worry about localization. With custom fonts (Google Fonts or any other fonts than San Francisco and New York) you need to take care of this by yourself. If you need custom fonts - plan ahead and choose fonts with support for all the languages you might need later.</p>
<h2 id="heading-icons">Icons</h2>
<h3 id="heading-sf-symbols">SF Symbols</h3>
<p>Link: <a target="_blank" href="https://developer.apple.com/sf-symbols/">https://developer.apple.com/sf-symbols/</a></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688799666577/05e48866-f82b-4b68-be5a-0c7aadc7b09a.png" alt class="image--center mx-auto" /></p>
<p>I recommend you start with this: official icons from Apple. Used in every OS from Apple. Very easy to use in SwiftUI. If you need some custom icons, there's an easy way to make your own SF Symbols.</p>
<p>Using SF Symbols is not only easy, but it also helps with improving the UX of your apps. If you are using the same icons as in other apps (and also in native iOS apps), users will be familiar with them. They will recognize them faster.</p>
<h3 id="heading-fontawesome">FontAwesome</h3>
<p>link: <a target="_blank" href="https://fontawesome.com">https://fontawesome.com</a></p>
<p>There are many other icon libraries, but I would like to show you FontAwesome icon library as an example. They have many icons in the catalog and many styles to choose from.</p>
<p>I really like and recommend these icons, but... I mostly use them for web design and web development. So while there are other options than the icons from Apple, personally, for my iOS apps, I like to use default SF Symbols and design my custom SF Symbols if necessary.</p>
<h2 id="heading-component-libraries">Component Libraries</h2>
<h3 id="heading-sketch-libraries">Sketch Libraries</h3>
<p>Link: <a target="_blank" href="https://developer.apple.com/design/resources/">https://developer.apple.com/design/resources/</a></p>
<p>Apple's official design resources for Sketch and a few other tools: Adobe XD, Photoshop and some resources for Apple's Keynote.</p>
<p>For your convenience, here are some direct links to a few most popular resources:</p>
<p>iOS and iPadOS: <a target="_blank" href="https://developer.apple.com/design/resources/#ios-apps">https://developer.apple.com/design/resources/#ios-apps</a></p>
<p>macOS: <a target="_blank" href="https://developer.apple.com/design/resources/#macos-apps">https://developer.apple.com/design/resources/#macos-apps</a></p>
<p>visionOS: <a target="_blank" href="https://developer.apple.com/design/resources/#visionos-apps">https://developer.apple.com/design/resources/#visionos-apps</a></p>
<p>Licensing and Trademarks: <a target="_blank" href="https://developer.apple.com/licensing-trademarks/">https://developer.apple.com/licensing-trademarks/</a></p>
<p>Wait! But what about Figma?</p>
<h3 id="heading-ios-ui-kit-in-figma">iOS UI kit in Figma</h3>
<p>Link: <a target="_blank" href="https://www.figma.com/community/file/1121065701252736567">https://www.figma.com/community/file/1121065701252736567</a></p>
<p>This is just one example. iOS 16 UI Kit for Figma by Joey Banks. One of the best and most popular iOS design kits for Figma.</p>
<p>For a few years, Figma users were relying on such libraries made by the Figma community. It is also possible to import Sketch files to Figma (including Apple's Sketch libraries), but Figma design kits were just easier and better.</p>
<p>However, we now also have...</p>
<h3 id="heading-apples-design-library-for-figma">Apple's design library for Figma</h3>
<p>Link: <a target="_blank" href="https://www.figma.com/community/file/1248375255495415511">https://www.figma.com/community/file/1248375255495415511</a></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688799441106/7d6e7a14-7518-4800-ac12-cfd53bfb03f6.png" alt class="image--center mx-auto" /></p>
<p>During WWDC 2023 Apple finally released their official iOS and iPadOS Design Library for Figma. Great news for every Figma user designing for iOS.</p>
<p>But that's not all. We also have a few other libraries from Apple:</p>
<h3 id="heading-macos-design-library">macOS Design Library</h3>
<p>Link: <a target="_blank" href="https://www.figma.com/community/file/1251588934545918753">https://www.figma.com/community/file/1251588934545918753</a></p>
<h3 id="heading-visionos-design-library">visionOS Design Library</h3>
<p>Link: <a target="_blank" href="https://www.figma.com/community/file/1253443272911187215">https://www.figma.com/community/file/1253443272911187215</a></p>
<h3 id="heading-app-icon-production-templates">App Icon Production Templates</h3>
<p>Link: <a target="_blank" href="https://www.figma.com/community/file/1250144691121363237">https://www.figma.com/community/file/1250144691121363237</a></p>
<h2 id="heading-app-screenshots">App Screenshots</h2>
<p>When we want to publish our app on the App Store, we need not only the app, metadata (app name, description, etc.) but also app screenshots.</p>
<h3 id="heading-screenshot-specifications">Screenshot specifications</h3>
<p>Link: <a target="_blank" href="https://developer.apple.com/help/app-store-connect/reference/screenshot-specifications">https://developer.apple.com/help/app-store-connect/reference/screenshot-specifications</a></p>
<p>Let's start with Apple's screenshot specifications. Very detailed, but in my opinion also a bit confusing. After you'll read the <strong>Requirement</strong> for each screenshot size, you will realize that you don't need most of the sizes. At the time of writing this article, the minimal set of screenshots you need for an iPhone app is:</p>
<ul>
<li><p>iPhone 6.5" (for example, iPhone 14 Plus)</p>
</li>
<li><p>iPhone 5.5" (for example, iPhone 8 Plus)</p>
</li>
</ul>
<h3 id="heading-apples-product-bezels">Apple's Product Bezels</h3>
<p>Link: <a target="_blank" href="https://developer.apple.com/design/resources/#product-bezels">https://developer.apple.com/design/resources/#product-bezels</a></p>
<p>Here you will find the latest product bezels from Apple. Latest iPhones, iPads, Macs, and even Apple Watches and Apple TV. With those, you can create screenshots like in the image below - app screenshots on the iPhone screen with some promo texts.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688888774992/ea4a4998-bac5-4d59-bb0e-633baf0ebf5a.png" alt class="image--center mx-auto" /></p>
<p>However, as I previously said, we still need to provide screenshots for iPhone 5.5" (for example, iPhone 8 Plus). And while it's not a requirement to use iPhone 8 Plus bezels on those screenshots, this is something I am doing. If you want to do the same, you will find out that iPhone 8 Plus is not included in the official Apple Product Bezels. But...</p>
<h3 id="heading-metas-product-bezels">Meta's Product Bezels</h3>
<p>Link: <a target="_blank" href="https://design.facebook.com/toolsandresources/devices/">https://design.facebook.com/toolsandresources/devices/</a></p>
<p>Thankfully, Meta's Design Team comes to the rescue. Their library is quite big. They have bezels of products from many brands and luckily for us, they also have Apple's iPhones, including iPhone 8 Plus.</p>
<h2 id="heading-marketing">Marketing</h2>
<p>OK. Your app is ready. Your screenshots are ready. It's time to publish your app on the App Store and tell the world about it. It's time for some marketing and with Apple's resources, promoting our app is a bit easier.</p>
<h3 id="heading-marketing-resources-and-identity-guidelines">Marketing Resources and Identity Guidelines</h3>
<p>Link: <a target="_blank" href="https://developer.apple.com/app-store/marketing/guidelines/">https://developer.apple.com/app-store/marketing/guidelines/</a></p>
<p>Here you can learn a lot about Apple's recommendations. Few nice examples of what you can and shouldn't do with your marketing images.</p>
<h3 id="heading-app-store-marketing-tools">App Store Marketing Tools</h3>
<p>Link: <a target="_blank" href="https://tools.applemediaservices.com/app-store/">https://tools.applemediaservices.com/app-store/</a></p>
<p>Find your app first and you will be directed to a page with marketing tools for your app, where you can generate a Short Link for your app, download App Icon, get "Download on the App Store" badges and even generate a QR code with a link to your app on the App Store.</p>
<h3 id="heading-promote-like-a-pro">Promote like a pro</h3>
<p>Link: <a target="_blank" href="https://tools.applemediaservices.com/apple-app-store-promote">https://tools.applemediaservices.com/apple-app-store-promote</a></p>
<p>Again, start with finding your app. You will be redirected to a page with a wizard to generate nice-looking promo images for your app. Few different texts, a few color variations and a few different sizes to choose from.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688827902741/fade1a46-6502-4927-9916-632d02fa971b.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-thank-you-for-reading">Thank you for reading!</h2>
<p>I hope that you found this article useful and maybe learned a thing or two about UI design.</p>
<p>If you want to support my work, please like, comment, share the article and most importantly...</p>
<p>📱Check out my apps on the App Store:<br /><a target="_blank" href="https://apps.apple.com/developer/next-planet/id1495155532">https://apps.apple.com/developer/next-planet/id1495155532</a></p>
<p>☕ If you like what I do, consider supporting me on Ko-fi! Every little bit means the world!<br /><a target="_blank" href="https://ko-fi.com/kslazinski">https://ko-fi.com/kslazinski</a></p>
]]></content:encoded></item><item><title><![CDATA[Digit group separators in a SwiftUI app]]></title><description><![CDATA[Recently one user of my iOS app Wins (Goals Tracker) asked me for one small improvement - my app should display numbers with digit group separators (AKA thousands separators). Small change but can improve the UX of my app.
Without digit group separat...]]></description><link>https://blog.next-planet.com/digit-group-separators-in-a-swiftui-app</link><guid isPermaLink="true">https://blog.next-planet.com/digit-group-separators-in-a-swiftui-app</guid><category><![CDATA[ios app development]]></category><category><![CDATA[SwiftUI]]></category><category><![CDATA[UX]]></category><category><![CDATA[iOS]]></category><dc:creator><![CDATA[Kris Slazinski]]></dc:creator><pubDate>Tue, 04 Jul 2023 13:14:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1688469192384/350182f1-45fd-49d4-907a-e8285f42e695.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently one user of my iOS app <a target="_blank" href="https://apple.co/3mk0NjK">Wins (Goals Tracker)</a> asked me for one small improvement - my app should display numbers with digit group separators (AKA thousands separators). Small change but can improve the UX of my app.</p>
<p>Without digit group separators, big numbers look like this:</p>
<p><strong>10000</strong></p>
<p>Thousands are not separated and the numbers are harder to read. With digit group separators the same number will look like this:</p>
<p><strong>10 000</strong><br /><strong>10.000</strong><br /><strong>10,000</strong></p>
<p>It depends on the users' locale. In iOS you set it in <strong>Settings</strong> / <strong>General</strong> / <strong>Language &amp; Region</strong> / <strong>Region</strong>. It means that depending on the user's region, the user will see <strong>10 000</strong> or <strong>10.000</strong> or <strong>10,000</strong>. Many non-English-speaking countries use periods as digit group separators. Most English-speaking countries use a comma. In some countries, it's just space. But thankfully we don't need to worry too much and remember which separator to use. We just need to specify which numbers should have the separator.</p>
<h2 id="heading-text-view">Text view</h2>
<p>I needed to format numbers in 2 different places. The first one was the Text view.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688473205628/17f84673-dfb2-46c1-987f-dc60a2404998.png" alt class="image--center mx-auto" /></p>
<p>The first thing I needed to do is to create and configure the NumberFormatter.</p>
<pre><code class="lang-swift"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">GoalView</span>: <span class="hljs-title">View</span> </span>{
    <span class="hljs-comment">// other properties</span>

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">let</span> numberFormatter: <span class="hljs-type">NumberFormatter</span> = {
        <span class="hljs-keyword">let</span> formatter = <span class="hljs-type">NumberFormatter</span>()
        formatter.numberStyle = .decimal
        formatter.usesGroupingSeparator = <span class="hljs-literal">true</span>
        <span class="hljs-keyword">return</span> formatter
    }()

    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-comment">// body of my GoalView</span>
    }
}
</code></pre>
<p>Previously, when I didn't use digit group separators, my Text in the body of the GoalView looked like this:</p>
<pre><code class="lang-swift"><span class="hljs-type">HStack</span>(spacing: <span class="hljs-number">4</span>) {
    <span class="hljs-type">Text</span>(<span class="hljs-string">"\(goal.currently)"</span>)
    <span class="hljs-type">Text</span>(<span class="hljs-string">"/"</span>)
    <span class="hljs-type">Text</span>(<span class="hljs-string">"\(goal.goal)"</span>)
    <span class="hljs-type">Text</span>(goal.currency ?? <span class="hljs-string">"USD"</span>)
}
</code></pre>
<p>To use digit group separators, my new code looks like this:</p>
<pre><code class="lang-swift"><span class="hljs-type">HStack</span>(spacing: <span class="hljs-number">4</span>) {
    <span class="hljs-type">Text</span>(<span class="hljs-string">"\(numberFormatter.string(for: goal.currently) ?? "</span><span class="hljs-number">0</span><span class="hljs-string">")"</span>)
    <span class="hljs-type">Text</span>(<span class="hljs-string">"/"</span>)
    <span class="hljs-type">Text</span>(<span class="hljs-string">"\(numberFormatter.string(for: goal.goal) ?? "</span><span class="hljs-number">0</span><span class="hljs-string">")"</span>)
    <span class="hljs-type">Text</span>(goal.currency ?? <span class="hljs-string">"USD"</span>)
}
</code></pre>
<p>We are using the <a target="_blank" href="https://developer.apple.com/documentation/foundation/formatter/1415993-string">string(for:)</a> method on numberFormatter, so we are changing the number into a String and applying the style we previously configured for NumberFormatter.</p>
<h2 id="heading-textfield">TextField</h2>
<p>I also wanted to format numbers using digit group separators when users type data in the TextField.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688473359751/994714ea-dbf2-45e3-9c6d-0e9dae861621.png" alt class="image--center mx-auto" /></p>
<p>One way of doing this is to use TextField like this:</p>
<pre><code class="lang-swift"><span class="hljs-type">TextField</span>(<span class="hljs-string">"Goal"</span>, value: $goal, format: .number)
</code></pre>
<p>But one problem with this solution is that the text is changed to the number (and formatted) only when the user presses Enter. To format the numbers on the fly, when users edit the text in the TextField, I did something else.</p>
<p>First, we need to use NumberFormatter as before:</p>
<pre><code class="lang-swift"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">AddGoalView</span>: <span class="hljs-title">View</span> </span>{
    <span class="hljs-comment">// other properties</span>

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">let</span> numberFormatter: <span class="hljs-type">NumberFormatter</span> = {
        <span class="hljs-keyword">let</span> formatter = <span class="hljs-type">NumberFormatter</span>()
        formatter.numberStyle = .decimal
        formatter.usesGroupingSeparator = <span class="hljs-literal">true</span>
        <span class="hljs-keyword">return</span> formatter
    }()

    @<span class="hljs-type">State</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> goal = <span class="hljs-number">0</span>
    @<span class="hljs-type">State</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> goalString = <span class="hljs-string">""</span>

    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-comment">// body of my GoalView</span>
    }
}
</code></pre>
<p>One important thing here - I am using 2 properties per each TextField, for example <strong>goal</strong> (Int) and <strong>goalString</strong> (String).</p>
<p>My TextFields in the body of my <strong>AddGoalView</strong> look like this:</p>
<pre><code class="lang-swift"><span class="hljs-type">TextField</span>(<span class="hljs-string">"0"</span>, text: $goalString)
    .keyboardType(.numberPad)
    .onChange(of: goalString) { <span class="hljs-number">_</span> <span class="hljs-keyword">in</span>
        goalString = goalString.<span class="hljs-built_in">filter</span> { $<span class="hljs-number">0</span>.isNumber }
        goal = <span class="hljs-type">Int</span>(goalString) ?? <span class="hljs-number">0</span>
        goalString = numberFormatter.string(<span class="hljs-keyword">for</span>: goal) ?? <span class="hljs-string">"0"</span>
    }
</code></pre>
<p>The solution is very simple. I am using .onChange modifier. When goalString in the TextField changes, my app does 3 things:</p>
<ol>
<li><p><strong>goalString</strong> is filtered and we keep only characters that represent a number</p>
</li>
<li><p><strong>goalString</strong> is converted to Int and assigned to the <strong>goal</strong> property</p>
</li>
<li><p>the value of <strong>goal</strong> is assigned to <strong>goalString</strong> as a string formatted as specified for <strong>numberFormatter</strong></p>
</li>
</ol>
<h2 id="heading-limitations">Limitations</h2>
<p>Please mind that there are some limitations here. If the user copies and pastes <strong>10,000.50</strong>, it will be converted to the Int value of <strong>1000050</strong> and this converted to <strong>1,000,050</strong>.</p>
<p>That's because of this line of code I showed you before:</p>
<pre><code class="lang-swift">goalString = goalString.<span class="hljs-built_in">filter</span> { $<span class="hljs-number">0</span>.isNumber }
</code></pre>
<p>I solved this problem in my other app <a target="_blank" href="https://apple.co/3WjbjnR">Numi</a> - in Numi you can copy and paste numbers with decimal fractions and they are correctly formatted based on the user's locale. But Numi is an app for personal finances and decimal fractions are something very common when you work with money. In Wins I only use Integers, so I didn't need this.</p>
<p>In Numi I have more complex functions triggered when users edit the text in TextFields, involving properties like <strong>formatter.minimumFractionDigits</strong>, <strong>formatter.maximumFractionDigits</strong> and <strong>formatter.roundingMode</strong>. But this is a topic for another article :)<br />Just please be aware of this and rewrite the logic in .onChange modifier based on your needs.</p>
<h2 id="heading-thank-you-for-reading">Thank you for reading!</h2>
<p>If you want to support my work, please like, comment, share the article and most importantly...</p>
<p>📱Check out my apps on the App Store:<br /><a target="_blank" href="https://apps.apple.com/developer/next-planet/id1495155532">https://apps.apple.com/developer/next-planet/id1495155532</a></p>
<p>☕ If you like what I do, consider supporting me on Ko-fi! Every little bit means the world!<br /><a target="_blank" href="https://ko-fi.com/kslazinski">https://ko-fi.com/kslazinski</a></p>
]]></content:encoded></item><item><title><![CDATA[How to migrate NSPersistentCloudKitContainer to App Groups]]></title><description><![CDATA[Recently I needed to add iOS Widgets to my app Wins. As I discovered, if you are using Core Data and want to share data between your iOS app and widgets, you need to use App Groups.
At first, I tried to migrate my database following the solution by D...]]></description><link>https://blog.next-planet.com/how-to-migrate-nspersistentcloudkitcontainer-to-app-groups</link><guid isPermaLink="true">https://blog.next-planet.com/how-to-migrate-nspersistentcloudkitcontainer-to-app-groups</guid><category><![CDATA[iOS]]></category><category><![CDATA[CoreData]]></category><category><![CDATA[CloudKit]]></category><category><![CDATA[backend]]></category><category><![CDATA[app development]]></category><dc:creator><![CDATA[Kris Slazinski]]></dc:creator><pubDate>Fri, 02 Jun 2023 15:05:03 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1685707354627/f00ca95d-9294-49d2-b92b-c4279547d587.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently I needed to add iOS Widgets to my app <a target="_blank" href="https://apple.co/3mk0NjK">Wins</a>. As I discovered, if you are using Core Data and want to share data between your iOS app and widgets, you need to use App Groups.</p>
<p>At first, I tried to migrate my database following the solution by Donny Wals. In his book <a target="_blank" href="https://practicalcoredata.com">Practical Core Data</a>, there is a separate chapter dedicated to this topic: <strong>“Chapter 6 - Sharing a Core Data store with apps and extensions”</strong>. If you work with Core Data, I highly recommend reading this chapter and the whole book. Donny is a great Core Data expert and his book helped me a lot with understanding this framework.</p>
<p>However, in his example, he is using <strong>NSPersistentContainer</strong>. And in my app, I am using <strong>NSPersistentCloudKitContainer</strong>, which means that I have Core Data with CloudKit support. When trying to migrate my database I ran into 2 problems:</p>
<ol>
<li><p>My app was freezing during migration if iCloud was turned off (for example, if the user was not signed in to iCloud).</p>
</li>
<li><p>After migration, all my data was duplicated.</p>
</li>
</ol>
<p>I spent many hours until I solved both issues. Using ChatGPT, reading Apple's documentation, and browsing through StackOverflow and Apple Developer Forums. As there wasn't one place with a complete working solution, I decided to write this article as it might help other iOS developers.</p>
<p>At the end of this article, I will show you my entire PersistenceController code. I want you to understand each part of the code, so let's have a look at each part of my PersistenceController. But first, a few words about App Groups.</p>
<h2 id="heading-app-groups">App Groups</h2>
<p>What are they and why do we need them? By default, when an app uses Core Data, the app's database is created in the Application Support directory of this app. And only this particular app has access to this folder (and database). App Groups are shared folders. When 2 or more apps have access to this folder, they all can use the same database.</p>
<p>You can read how to configure App Groups in this article from Apple:<br /><a target="_blank" href="https://developer.apple.com/documentation/xcode/configuring-app-groups">https://developer.apple.com/documentation/xcode/configuring-app-groups</a></p>
<p>Some people recommend adding App Groups support to all new projects as doing this at the beginning is much easier than migrating an existing database to the App Group folder.</p>
<p>In this article, I will show you how I migrated my database to the App Groups folder. First, I will show you my whole code for the PersistenceController. After this, I will talk about each part and explain why I did something the way I did.</p>
<h2 id="heading-full-code">Full code</h2>
<pre><code class="lang-swift"><span class="hljs-keyword">import</span> CoreData

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">PersistenceController</span> </span>{
    <span class="hljs-keyword">static</span> <span class="hljs-keyword">let</span> shared = <span class="hljs-type">PersistenceController</span>()

    <span class="hljs-comment">// Create a database for Preview Canvas.</span>
    <span class="hljs-keyword">static</span> <span class="hljs-keyword">var</span> preview: <span class="hljs-type">PersistenceController</span> = {
        <span class="hljs-keyword">let</span> result = <span class="hljs-type">PersistenceController</span>(inMemory: <span class="hljs-literal">true</span>)
        <span class="hljs-keyword">let</span> viewContext = result.container.viewContext

        <span class="hljs-keyword">for</span> <span class="hljs-number">_</span> <span class="hljs-keyword">in</span> <span class="hljs-number">0</span>..&lt;<span class="hljs-number">4</span> {
            <span class="hljs-keyword">let</span> newGoal = <span class="hljs-type">CDEGoal</span>(context: viewContext)
            newGoal.type = <span class="hljs-type">GoalType</span>.number.rawValue
            newGoal.date = <span class="hljs-type">Date</span>()
            newGoal.icon = <span class="hljs-string">"trophy"</span>
            newGoal.name = <span class="hljs-string">"Goal name"</span>
            newGoal.goal = <span class="hljs-number">1000</span>
            newGoal.currently = <span class="hljs-number">500</span>
            newGoal.status = <span class="hljs-type">GoalStatus</span>.planned.rawValue
            newGoal.color = <span class="hljs-string">"purple"</span>
            newGoal.id = <span class="hljs-type">UUID</span>()
        }

        <span class="hljs-keyword">do</span> {
            <span class="hljs-keyword">try</span> viewContext.save()
        } <span class="hljs-keyword">catch</span> {
            <span class="hljs-keyword">let</span> nsError = error <span class="hljs-keyword">as</span> <span class="hljs-type">NSError</span>
            <span class="hljs-built_in">fatalError</span>(<span class="hljs-string">"Unresolved error \(nsError), \(nsError.userInfo)"</span>)
        }
        <span class="hljs-keyword">return</span> result
    }()

    <span class="hljs-keyword">let</span> container: <span class="hljs-type">NSPersistentCloudKitContainer</span>

    <span class="hljs-keyword">var</span> oldStoreURL: <span class="hljs-type">URL</span> {
        <span class="hljs-keyword">let</span> appSupport = <span class="hljs-type">FileManager</span>.<span class="hljs-keyword">default</span>.urls(
            <span class="hljs-keyword">for</span>: .applicationSupportDirectory,
            <span class="hljs-keyword">in</span>: .userDomainMask
        ).first!
        <span class="hljs-keyword">return</span> appSupport.appendingPathComponent(<span class="hljs-string">"YourAppName.sqlite"</span>)
    }

    <span class="hljs-comment">// We define new App Group containerURL.</span>
    <span class="hljs-keyword">var</span> sharedStoreURL: <span class="hljs-type">URL</span> {
        <span class="hljs-keyword">let</span> id = <span class="hljs-string">"group.com.yourDomain.YourAppName"</span> <span class="hljs-comment">// Use App Group's id here.</span>
        <span class="hljs-keyword">let</span> containerURL = <span class="hljs-type">FileManager</span>.<span class="hljs-keyword">default</span>.containerURL(forSecurityApplicationGroupIdentifier: id)!
        <span class="hljs-keyword">return</span> containerURL.appendingPathComponent(<span class="hljs-string">"YourAppName.sqlite"</span>)
    }

    <span class="hljs-keyword">init</span>(inMemory: <span class="hljs-type">Bool</span> = <span class="hljs-literal">false</span>) {
        container = <span class="hljs-type">NSPersistentCloudKitContainer</span>(name: <span class="hljs-string">"iCloud.com.yourDomain.YourAppName"</span>)

        <span class="hljs-keyword">let</span> description = container.persistentStoreDescriptions.first!
        <span class="hljs-keyword">let</span> originalCloudKitOptions = description.cloudKitContainerOptions

        <span class="hljs-comment">// Use the App Group store if migration is not needed (if default store without App Group doesn't exist).</span>
        <span class="hljs-keyword">if</span> !<span class="hljs-type">FileManager</span>.<span class="hljs-keyword">default</span>.fileExists(atPath: oldStoreURL.path) {
            description.url = sharedStoreURL
        } <span class="hljs-keyword">else</span> {
            <span class="hljs-comment">// Disable CloudKit integration if migration is needed.</span>
            description.cloudKitContainerOptions = <span class="hljs-literal">nil</span>
        }

        description.setOption(<span class="hljs-literal">true</span> <span class="hljs-keyword">as</span> <span class="hljs-type">NSNumber</span>, forKey: <span class="hljs-type">NSPersistentHistoryTrackingKey</span>)

        <span class="hljs-keyword">if</span> inMemory {
            description.url = <span class="hljs-type">URL</span>(fileURLWithPath: <span class="hljs-string">"/dev/null"</span>)
        }

        <span class="hljs-comment">// Load persistent stores.</span>
        container.loadPersistentStores(completionHandler: { (storeDescription, error) <span class="hljs-keyword">in</span>
            <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> error = error <span class="hljs-keyword">as</span> <span class="hljs-type">NSError?</span> {
                <span class="hljs-built_in">fatalError</span>(<span class="hljs-string">"Unresolved error \(error), \(error.userInfo)"</span>)
            }
        })

        <span class="hljs-comment">// Perform the migration.</span>
        migrateStore(<span class="hljs-keyword">for</span>: container, originalCloudKitOptions: originalCloudKitOptions)

        container.viewContext.automaticallyMergesChangesFromParent = <span class="hljs-literal">true</span>
    }

<span class="hljs-comment">// Function migrateStore. Migrates data store to App Group if needed.</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">migrateStore</span><span class="hljs-params">(<span class="hljs-keyword">for</span> container: NSPersistentCloudKitContainer, originalCloudKitOptions: NSPersistentCloudKitContainerOptions?)</span></span> {
    <span class="hljs-keyword">let</span> coordinator = container.persistentStoreCoordinator
    <span class="hljs-keyword">let</span> storeDescription = container.persistentStoreDescriptions.first!

    <span class="hljs-comment">// Exit current scope if persistentStore(for:) returns nil (migration is not needed).</span>
    <span class="hljs-keyword">guard</span> coordinator.persistentStore(<span class="hljs-keyword">for</span>: oldStoreURL) != <span class="hljs-literal">nil</span> <span class="hljs-keyword">else</span> {
        <span class="hljs-built_in">print</span>(<span class="hljs-string">"Migration not needed"</span>)
        <span class="hljs-keyword">return</span>
    }

        <span class="hljs-comment">// Replace one persistent store with another.</span>
        <span class="hljs-keyword">do</span> {
            <span class="hljs-keyword">try</span> coordinator.replacePersistentStore(
                at: sharedStoreURL,
                withPersistentStoreFrom: oldStoreURL,
                type: .sqlite
            )
        } <span class="hljs-keyword">catch</span> {
            <span class="hljs-built_in">fatalError</span>(<span class="hljs-string">"Something went wrong while migrating the store: \(error)"</span>)
        }

        <span class="hljs-comment">// Delete old store.</span>
        <span class="hljs-keyword">do</span> {
            <span class="hljs-keyword">try</span> coordinator.destroyPersistentStore(at: oldStoreURL, type: .sqlite, options: <span class="hljs-literal">nil</span>)
        } <span class="hljs-keyword">catch</span> {
            <span class="hljs-built_in">fatalError</span>(<span class="hljs-string">"Something went wrong while deleting the old store: \(error)"</span>)
        }

        <span class="hljs-type">NSFileCoordinator</span>(filePresenter: <span class="hljs-literal">nil</span>).coordinate(writingItemAt: oldStoreURL.deletingLastPathComponent(), options: .forDeleting, error: <span class="hljs-literal">nil</span>, byAccessor: { url <span class="hljs-keyword">in</span>
            <span class="hljs-keyword">try</span>? <span class="hljs-type">FileManager</span>.<span class="hljs-keyword">default</span>.removeItem(at: oldStoreURL)
            <span class="hljs-keyword">try</span>? <span class="hljs-type">FileManager</span>.<span class="hljs-keyword">default</span>.removeItem(at: oldStoreURL.deletingLastPathComponent().appendingPathComponent(<span class="hljs-string">"\(container.name).sqlite-shm"</span>))
            <span class="hljs-keyword">try</span>? <span class="hljs-type">FileManager</span>.<span class="hljs-keyword">default</span>.removeItem(at: oldStoreURL.deletingLastPathComponent().appendingPathComponent(<span class="hljs-string">"\(container.name).sqlite-wal"</span>))
            <span class="hljs-keyword">try</span>? <span class="hljs-type">FileManager</span>.<span class="hljs-keyword">default</span>.removeItem(at: oldStoreURL.deletingLastPathComponent().appendingPathComponent(<span class="hljs-string">"ckAssetFiles"</span>))
        })

        <span class="hljs-comment">// Unload the store and load it again with new storeDescription to re-enable CloudKit.</span>
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> persistentStore = container.persistentStoreCoordinator.persistentStores.first {
            <span class="hljs-keyword">do</span> {
                <span class="hljs-keyword">try</span> container.persistentStoreCoordinator.remove(persistentStore)
                <span class="hljs-built_in">print</span>(<span class="hljs-string">"Persistent store unloaded"</span>)
            } <span class="hljs-keyword">catch</span> {
                <span class="hljs-built_in">print</span>(<span class="hljs-string">"Failed to unload persistent store: \(error)"</span>)
            }
        }

        <span class="hljs-comment">// Set the URL of the storeDescription to the sharedStoreURL.</span>
        storeDescription.url = sharedStoreURL
        <span class="hljs-comment">// Modify the storeDescription to re-enable CloudKit integration.</span>
        storeDescription.cloudKitContainerOptions = originalCloudKitOptions

        <span class="hljs-comment">// Load the persistent store with the updated storeDescription.</span>
        container.loadPersistentStores(completionHandler: { (storeDescription, error) <span class="hljs-keyword">in</span>
            <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> error = error <span class="hljs-keyword">as</span> <span class="hljs-type">NSError?</span> {
                <span class="hljs-built_in">fatalError</span>(<span class="hljs-string">"Unresolved error \(error), \(error.userInfo)"</span>)
            }
        })

        <span class="hljs-built_in">print</span>(<span class="hljs-string">"Migration completed"</span>)
    }
}
</code></pre>
<p>That's the whole code. Now let's look at each part and I will explain to you what's happening and why.</p>
<h2 id="heading-create-a-persistencecontroller">Create a PersistenceController</h2>
<p>You are probably familiar with the first part of the code. Especially if you (like me) start with the default Core Data stack in Xcode. When we start <strong>New Project</strong> and choose "<strong>App</strong>" and later check "<strong>Use Core Data</strong>" and "<strong>Host in iCloud</strong>", we will end up with something like this. <strong>PersistenceController</strong> struct, <strong>static let shared</strong> for our app's data and <strong>static var preview</strong> for our <strong>Preview Canvas</strong>.</p>
<p>In the <strong>for _ in</strong> block I am creating some objects for my previews. This is not important in the context of database migration, so please don't worry about this part of the code. In your app, this block will be different anyway.</p>
<pre><code class="lang-swift"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">PersistenceController</span> </span>{
    <span class="hljs-keyword">static</span> <span class="hljs-keyword">let</span> shared = <span class="hljs-type">PersistenceController</span>()

    <span class="hljs-comment">// Create a database for Preview Canvas.</span>
    <span class="hljs-keyword">static</span> <span class="hljs-keyword">var</span> preview: <span class="hljs-type">PersistenceController</span> = {
        <span class="hljs-keyword">let</span> result = <span class="hljs-type">PersistenceController</span>(inMemory: <span class="hljs-literal">true</span>)
        <span class="hljs-keyword">let</span> viewContext = result.container.viewContext

        <span class="hljs-keyword">for</span> <span class="hljs-number">_</span> <span class="hljs-keyword">in</span> <span class="hljs-number">0</span>..&lt;<span class="hljs-number">4</span> {
            <span class="hljs-keyword">let</span> newGoal = <span class="hljs-type">CDEGoal</span>(context: viewContext)
            newGoal.type = <span class="hljs-type">GoalType</span>.number.rawValue
            newGoal.date = <span class="hljs-type">Date</span>()
            newGoal.icon = <span class="hljs-string">"trophy"</span>
            newGoal.name = <span class="hljs-string">"Goal name"</span>
            newGoal.goal = <span class="hljs-number">1000</span>
            newGoal.currently = <span class="hljs-number">500</span>
            newGoal.status = <span class="hljs-type">GoalStatus</span>.planned.rawValue
            newGoal.color = <span class="hljs-string">"purple"</span>
            newGoal.id = <span class="hljs-type">UUID</span>()
        }

        <span class="hljs-keyword">do</span> {
            <span class="hljs-keyword">try</span> viewContext.save()
        } <span class="hljs-keyword">catch</span> {
            <span class="hljs-keyword">let</span> nsError = error <span class="hljs-keyword">as</span> <span class="hljs-type">NSError</span>
            <span class="hljs-built_in">fatalError</span>(<span class="hljs-string">"Unresolved error \(nsError), \(nsError.userInfo)"</span>)
        }
        <span class="hljs-keyword">return</span> result
    }()
<span class="hljs-comment">// More code...</span>
}
</code></pre>
<h2 id="heading-declare-properties">Declare properties</h2>
<p>We need to declare a few properties first. We will use the name <strong>container</strong> for our <strong>NSPersistentCloudKitContainer</strong>. We will assign a particular iCloud container to it later in the <strong>init</strong> method.</p>
<p>We also declare <strong>oldStoreURL</strong> and <strong>sharedStoreURL</strong> properties that we will use later to move our database to the proper <strong>App Group</strong> folder.</p>
<pre><code class="lang-swift"><span class="hljs-keyword">let</span> container: <span class="hljs-type">NSPersistentCloudKitContainer</span>

<span class="hljs-keyword">var</span> oldStoreURL: <span class="hljs-type">URL</span> {
    <span class="hljs-keyword">let</span> appSupport = <span class="hljs-type">FileManager</span>.<span class="hljs-keyword">default</span>.urls(
        <span class="hljs-keyword">for</span>: .applicationSupportDirectory,
        <span class="hljs-keyword">in</span>: .userDomainMask
    ).first!
    <span class="hljs-keyword">return</span> appSupport.appendingPathComponent(<span class="hljs-string">"YourAppName.sqlite"</span>)
}

<span class="hljs-comment">// Define new App Group containerURL.</span>
<span class="hljs-keyword">var</span> sharedStoreURL: <span class="hljs-type">URL</span> {
    <span class="hljs-keyword">let</span> id = <span class="hljs-string">"group.com.yourDomain.YourAppName"</span> <span class="hljs-comment">// Use App Group's id here.</span>
    <span class="hljs-keyword">let</span> containerURL = <span class="hljs-type">FileManager</span>.<span class="hljs-keyword">default</span>.containerURL(forSecurityApplicationGroupIdentifier: id)!
    <span class="hljs-keyword">return</span> containerURL.appendingPathComponent(<span class="hljs-string">"YourAppName.sqlite"</span>)
}
</code></pre>
<h2 id="heading-create-the-container">Create the container</h2>
<p>We set our container as an <strong>NSPersistentCloudKitContainer</strong> with a name similar to "<strong>iCloud.com.yourDomain.YourAppName</strong>". One note here: while Apple suggests this naming convention, you can as well use a different one, for example, "<strong>iCloud.YourAppName</strong>". The most important thing is that this name must match the name of the iCloud container selected in the <strong>Signing &amp; Capabilities</strong> tab of your app's target.</p>
<p>We also create a constant <strong>description</strong> that we will use later in the code. And a constant <strong>originalCloudKitOptions</strong> which we will pass to the <strong>migrateStore</strong> function later.</p>
<pre><code class="lang-swift"><span class="hljs-keyword">init</span>(inMemory: <span class="hljs-type">Bool</span> = <span class="hljs-literal">false</span>) {
    container = <span class="hljs-type">NSPersistentCloudKitContainer</span>(name: <span class="hljs-string">"iCloud.com.yourDomain.YourAppName"</span>)

    <span class="hljs-keyword">let</span> description = container.persistentStoreDescriptions.first!
    <span class="hljs-keyword">let</span> originalCloudKitOptions = description.cloudKitContainerOptions

<span class="hljs-comment">// Other code.</span>
}
</code></pre>
<h2 id="heading-check-if-migration-is-needed">Check if migration is needed</h2>
<p>In our code, we will check two times if the migration is needed. Here's the first time. We are checking if a database file exists at the <strong>oldStoreURL</strong>. This covers a situation when users run our app for the first time on some device. If so, the database doesn't exist yet on the device and we set the <strong>URL</strong> of our store to <strong>sharedStoreURL</strong> (App Group folder). Database migration won't be needed here at all.</p>
<pre><code class="lang-swift"><span class="hljs-keyword">init</span>(inMemory: <span class="hljs-type">Bool</span> = <span class="hljs-literal">false</span>) {
<span class="hljs-comment">// Other code.</span>

    <span class="hljs-comment">// Use the App Group store if migration is not needed (if default store without App Group doesn't exist).</span>
    <span class="hljs-keyword">if</span> !<span class="hljs-type">FileManager</span>.<span class="hljs-keyword">default</span>.fileExists(atPath: oldStoreURL.path) {
        description.url = sharedStoreURL
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-comment">// Disable CloudKit integration if migration is needed.</span>
        description.cloudKitContainerOptions = <span class="hljs-literal">nil</span>
    }

<span class="hljs-comment">// Other code.</span>
}
</code></pre>
<p>As you can see, we also have an <strong>else</strong> statement. If a database file does exist on the device at the <strong>oldStoreURL</strong>, we disable iCloud sync by setting <strong>cloudKitContainerOptions</strong> to <strong>nil</strong>.</p>
<p>As I said earlier, my app was freezing during migration if iCloud was turned off on the device. With more detailed logging turned on, I saw that my app tries to connect to iCloud again and again and the migration code is not executed. The only solution I found is to turn off iCloud sync during the migration. So here in the code we are saying: if the database file exists at the <strong>oldStoreURL</strong>, turn off iCloud sync as we will perform the migration.</p>
<h2 id="heading-load-persistent-stores-and-migrate-data">Load persistent stores and migrate data</h2>
<p>We can migrate the database only if the persistent store was loaded, so we load persistent stores first and after this, we migrate the data.</p>
<pre><code class="lang-swift"><span class="hljs-keyword">init</span>(inMemory: <span class="hljs-type">Bool</span> = <span class="hljs-literal">false</span>) {
<span class="hljs-comment">// Other code.</span>

    <span class="hljs-comment">// Load the persistent stores.</span>
    container.loadPersistentStores(completionHandler: { (storeDescription, error) <span class="hljs-keyword">in</span>
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> error = error <span class="hljs-keyword">as</span> <span class="hljs-type">NSError?</span> {
            <span class="hljs-built_in">fatalError</span>(<span class="hljs-string">"Unresolved error \(error), \(error.userInfo)"</span>)
        }
    })

    <span class="hljs-comment">// Perform the migration.</span>
    migrateStore(<span class="hljs-keyword">for</span>: container, originalCloudKitOptions: originalCloudKitOptions)

<span class="hljs-comment">// Other code.</span>
}
</code></pre>
<p>We perform the migration using the <strong>migrateStore</strong> function that we will write in a moment as part of our <strong>PersistenceController</strong>.</p>
<h2 id="heading-function-to-migrate-the-store">Function to migrate the store</h2>
<p>In our <strong>PersistenceController</strong> we create a function named <strong>migrateStore</strong> which we execute at the end of our init method.</p>
<p>We will pass two arguments to this function: <strong>container</strong> and <strong>originalCloudKitOptions</strong>. We also declare the <strong>coordinator</strong> constant equal to <strong>container.persistentStoreCoordinator</strong> and <strong>storeDescription</strong> to make the part where we edit the container's options easier to read.</p>
<pre><code class="lang-swift"><span class="hljs-comment">// Function migrateStore. Migrates data store to App Group if needed.</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">migrateStore</span><span class="hljs-params">(<span class="hljs-keyword">for</span> container: NSPersistentCloudKitContainer, originalCloudKitOptions: NSPersistentCloudKitContainerOptions?)</span></span> {
    <span class="hljs-keyword">let</span> coordinator = container.persistentStoreCoordinator
    <span class="hljs-keyword">let</span> storeDescription = container.persistentStoreDescriptions.first!

    <span class="hljs-comment">// More code.</span>
}
</code></pre>
<h2 id="heading-check-if-migration-is-needed-again">Check if migration is needed (again)</h2>
<p>We also write a guard statement to check for the second time if the migration is needed. Let's say that a user opened a previous version of our app on his device before. If <strong>persistentStore</strong> for <strong>oldStoreURL</strong> is different than <strong>nil</strong>, it means that the database at the default location exists and we need to migrate the data to the App Group folder. If it doesn't exist (which means that we already migrated our data), print "Migration not needed" and exit from the current scope (<strong>migrateStore</strong> function).</p>
<pre><code class="lang-swift"><span class="hljs-comment">// Exit current scope if persistentStore(for:) returns nil (migration is not needed).</span>
<span class="hljs-keyword">guard</span> coordinator.persistentStore(<span class="hljs-keyword">for</span>: oldStoreURL) != <span class="hljs-literal">nil</span> <span class="hljs-keyword">else</span> {
    <span class="hljs-built_in">print</span>(<span class="hljs-string">"Migration not needed"</span>)
    <span class="hljs-keyword">return</span>
}
</code></pre>
<h2 id="heading-migrate-database">Migrate database</h2>
<p>With this code, we are migrating our database. But a more correct word would be <strong>replacing</strong>. And I will explain why in the next paragraphs.</p>
<pre><code class="lang-swift"><span class="hljs-comment">// Replace one persistent store with another.</span>
<span class="hljs-keyword">do</span> {
    <span class="hljs-keyword">try</span> coordinator.replacePersistentStore(
        at: sharedStoreURL,
        withPersistentStoreFrom: oldStoreURL,
        type: .sqlite
    )
} <span class="hljs-keyword">catch</span> {
    <span class="hljs-built_in">fatalError</span>(<span class="hljs-string">"Something went wrong while migrating the store: \(error)"</span>)
}
</code></pre>
<p>We use the word "migrate" when we talk about changing one database to another (or when we change our data model). But this may lead us to one mistake. <strong>NSPersistentStoreCoordinator</strong> has 2 similar methods: <a target="_blank" href="https://developer.apple.com/documentation/coredata/nspersistentstorecoordinator/3747534-migratepersistentstore">migratePersistentStore</a> and <a target="_blank" href="https://developer.apple.com/documentation/coredata/nspersistentstorecoordinator/3747536-replacepersistentstore">replacePersistentStore</a>. There is one crucial difference between them:</p>
<p>As one anonymous Apple wrote on <a target="_blank" href="https://developer.apple.com/forums/thread/653975?answerId=621347022#621347022">Apple Developer Forums</a>:</p>
<blockquote>
<p>migratePersistentStore is (...) creating a clean copy of all the data in your store file in a new location on the file system. Use replacePersistentStore instead to move the store to a new location.</p>
</blockquote>
<p>I tested <strong>migratePersistentStore</strong> before knowing about <strong>replacePersistentStore</strong> and in the result I had duplicated data. With the <strong>replacePersistentStore</strong> method, no duplicates were created.</p>
<h2 id="heading-delete-old-database">Delete old database</h2>
<p>With the <a target="_blank" href="https://developer.apple.com/documentation/coredata/nspersistentstorecoordinator/3747536-replacepersistentstore">replacePersistentStore</a> method, we replaced the store with our new store located in the App Group. Apple's documentation doesn't tell us much about it. It just says this: "Replaces one persistent store with another.". I thought this method will move one store to another place and will literally move the database files. But it doesn't. After running the code I noticed that a new version of the database was in fact created in the App Group folder, but old files were still present in the app support folder.</p>
<p>To get rid of old files, we need to delete them by ourselves. To do this, we will use <a target="_blank" href="https://developer.apple.com/documentation/coredata/nspersistentstorecoordinator/3747532-destroypersistentstore">destroyPersistentStore</a> and <a target="_blank" href="https://developer.apple.com/documentation/foundation/filemanager/1408573-removeitem">removeItem</a> methods.</p>
<pre><code class="lang-swift"><span class="hljs-comment">// Delete old store.</span>
<span class="hljs-keyword">do</span> {
    <span class="hljs-keyword">try</span> coordinator.destroyPersistentStore(at: oldStoreURL, type: .sqlite, options: <span class="hljs-literal">nil</span>)
} <span class="hljs-keyword">catch</span> {
    <span class="hljs-built_in">fatalError</span>(<span class="hljs-string">"Something went wrong while deleting the old store: \(error)"</span>)
}

<span class="hljs-type">NSFileCoordinator</span>(filePresenter: <span class="hljs-literal">nil</span>).coordinate(writingItemAt: oldStoreURL.deletingLastPathComponent(), options: .forDeleting, error: <span class="hljs-literal">nil</span>, byAccessor: { url <span class="hljs-keyword">in</span>
    <span class="hljs-keyword">try</span>? <span class="hljs-type">FileManager</span>.<span class="hljs-keyword">default</span>.removeItem(at: oldStoreURL) <span class="hljs-comment">// Delete .sqlite file.</span>
    <span class="hljs-keyword">try</span>? <span class="hljs-type">FileManager</span>.<span class="hljs-keyword">default</span>.removeItem(at: oldStoreURL.deletingLastPathComponent().appendingPathComponent(<span class="hljs-string">"\(container.name).sqlite-shm"</span>)) <span class="hljs-comment">// Delete .sqlite-shm file.</span>
    <span class="hljs-keyword">try</span>? <span class="hljs-type">FileManager</span>.<span class="hljs-keyword">default</span>.removeItem(at: oldStoreURL.deletingLastPathComponent().appendingPathComponent(<span class="hljs-string">"\(container.name).sqlite-wal"</span>)) <span class="hljs-comment">// Delete .sqlite-wal file.</span>
    <span class="hljs-keyword">try</span>? <span class="hljs-type">FileManager</span>.<span class="hljs-keyword">default</span>.removeItem(at: oldStoreURL.deletingLastPathComponent().appendingPathComponent(<span class="hljs-string">"ckAssetFiles"</span>)) <span class="hljs-comment">// Delete ckAssetFiles.</span>
    <span class="hljs-comment">// ckAssetFiles files may be named like this: AppName_ckAssets</span>
})
</code></pre>
<p>When I checked the Simulator's folder holding my database I saw 3 files there: .sqlite, .sqlite-shm and .sqlite-wal. I didn't see any ckAssetFiles files, but this may be due to iCloud being turned off in the Simulator. I decided to leave the last FileManager.default.removeItem as it was in the example. If there will be <strong>ckAssetFiles</strong> in the database folder, they will be deleted. If they are not there, nothing will happen.</p>
<p>I also need to mention that I tried to comment out <strong>coordinator.destroyPersistentStore</strong> and my files were not deleted. I tried to leave <strong>coordinator.destroyPersistentStore</strong> and comment out <strong>NSFileCoordinator</strong> and again - my old files were not deleted. We need both parts. We need to <strong>destroyPersistentStore</strong> first and later delete each file with <strong>FileManager.default.removeItem</strong>.</p>
<p>I found this solution on <a target="_blank" href="https://stackoverflow.com/a/72585271/12315994">StackOverflow</a> and I would like to quote a user named <strong>Jordan H</strong> who posted the solution:</p>
<blockquote>
<p>In talking with an engineer in a WWDC lab, they explained it does not actually delete the database files at the provided location as the documentation seems to imply. It actually just truncates rather than deletes. If you want them gone you can manually delete the files (if you can ensure no other process or a different thread is accessing them).</p>
</blockquote>
<h2 id="heading-re-enable-cloudkit">Re-enable CloudKit</h2>
<p>We moved our database. We deleted old files. Now we can re-enable CloudKit. As we can't change <strong>cloudKitContainerOptions</strong> on a running persistence store, we need to unload our current store, change <strong>storeDescription.cloudKitContainerOptions</strong> to the value previously stored in the <strong>originalCloudKitOptions</strong> constant and load the store again with these options.</p>
<pre><code class="lang-swift"><span class="hljs-comment">// Unload the store and load it again with new storeDescription to re-enable CloudKit.</span>
<span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> persistentStore = container.persistentStoreCoordinator.persistentStores.first {
    <span class="hljs-keyword">do</span> {
        <span class="hljs-keyword">try</span> container.persistentStoreCoordinator.remove(persistentStore)
        <span class="hljs-built_in">print</span>(<span class="hljs-string">"Persistent store unloaded"</span>)
    } <span class="hljs-keyword">catch</span> {
        <span class="hljs-built_in">print</span>(<span class="hljs-string">"Failed to unload persistent store: \(error)"</span>)
    }
}

<span class="hljs-comment">// Set the URL of the storeDescription to the sharedStoreURL</span>
storeDescription.url = sharedStoreURL
<span class="hljs-comment">// Modify the storeDescription to re-enable CloudKit integration</span>
storeDescription.cloudKitContainerOptions = originalCloudKitOptions

<span class="hljs-comment">// Load the persistent store with the updated storeDescription</span>
container.loadPersistentStores(completionHandler: { (storeDescription, error) <span class="hljs-keyword">in</span>
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> error = error <span class="hljs-keyword">as</span> <span class="hljs-type">NSError?</span> {
        <span class="hljs-built_in">fatalError</span>(<span class="hljs-string">"Unresolved error \(error), \(error.userInfo)"</span>)
    }
})

<span class="hljs-built_in">print</span>(<span class="hljs-string">"Migration completed"</span>)
</code></pre>
<h2 id="heading-final-note">Final note</h2>
<p>That's it. That's how I managed to migrate <strong>NSPersistentCloudKitContainer</strong> to <strong>App Groups</strong>. I tested this solution on Simulator and real devices. I tested with iCloud sync turned on and with iCloud sync turned off on the device. I tested if the data continues to sync after migration. And everything seems to work. However, I assume that it's not the best way of solving this problem. And also I am very far from being a Core Data expert, so please take this into consideration. Please double-check everything before using my code in your projects. Please test the results and make some changes if necessary. And if you know that something could be done better, please leave a comment and let me know about this.</p>
<h2 id="heading-thank-you-for-reading">Thank you for reading!</h2>
<p>If you want to support my work, please like, comment, share the article and most importantly...</p>
<p>📱Check out my apps on the App Store:<br /><a target="_blank" href="https://apps.apple.com/developer/next-planet/id1495155532">https://apps.apple.com/developer/next-planet/id1495155532</a></p>
<p>☕ If you like what I do, consider supporting me on Ko-fi! Every little bit means the world!<br /><a target="_blank" href="https://ko-fi.com/kslazinski">https://ko-fi.com/kslazinski</a></p>
]]></content:encoded></item><item><title><![CDATA[Outlined TabItem icons in iOS]]></title><description><![CDATA[Let's see how to make outlined TabItem icons in iOS. And let's look at this problem from a few different perspectives: development, UX and UI design and accessibility.
Regular Tabs
The easiest and fastest way to add tab navigation to your SwiftUI app...]]></description><link>https://blog.next-planet.com/outlined-tabitem-icons-in-ios</link><guid isPermaLink="true">https://blog.next-planet.com/outlined-tabitem-icons-in-ios</guid><category><![CDATA[SwiftUI]]></category><category><![CDATA[Swift]]></category><category><![CDATA[UX]]></category><category><![CDATA[Accessibility]]></category><category><![CDATA[iOS]]></category><dc:creator><![CDATA[Kris Slazinski]]></dc:creator><pubDate>Mon, 06 Mar 2023 14:43:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1678070964520/f7115045-c71b-47a6-b28d-a2ab8995023a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Let's see how to make outlined TabItem icons in iOS. And let's look at this problem from a few different perspectives: development, UX and UI design and accessibility.</p>
<h2 id="heading-regular-tabs">Regular Tabs</h2>
<p>The easiest and fastest way to add tab navigation to your SwiftUI app is to use <a target="_blank" href="https://developer.apple.com/documentation/swiftui/tabview">TabView</a> and <a target="_blank" href="https://developer.apple.com/sf-symbols/">SF Symbols</a> from Apple.</p>
<p>When I'm writing this article, the current version of SF Symbols is 4. And it consists of 4,400 symbols. Many of them have 2 variants: regular (outlined) and filled. Please have a look at the <strong>pie chart</strong> example and notice differences in the icon and the name.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678108619266/a36f7323-0c8e-46e3-baee-fcd558123b6c.png" alt class="image--center mx-auto" /></p>
<p>In SwiftUI, we use these icons like this:</p>
<pre><code class="lang-swift"><span class="hljs-type">Image</span>(systemName: <span class="hljs-string">"chart.pie"</span>)
<span class="hljs-type">Image</span>(systemName: <span class="hljs-string">"chart.pie.fill"</span>)
</code></pre>
<p>Here's an example of the TabView with 4 tabs using SF Symbols icons:</p>
<pre><code class="lang-swift"><span class="hljs-type">TabView</span> {    
    <span class="hljs-type">TransactionsView</span>()
        .tabItem {
            <span class="hljs-type">Label</span> {
                <span class="hljs-type">Text</span>(<span class="hljs-string">"Transactions"</span>)
            } icon: {
                <span class="hljs-type">Image</span>(systemName: <span class="hljs-string">"arrow.left.arrow.right.square"</span>)
            }
        }
        .tag(<span class="hljs-string">"transactions"</span>)

    <span class="hljs-type">RecurringView</span>()
        .tabItem {
            <span class="hljs-type">Label</span> {
                <span class="hljs-type">Text</span>(<span class="hljs-string">"Recurring"</span>)
            } icon: {
                <span class="hljs-type">Image</span>(systemName: <span class="hljs-string">"arrow.clockwise.circle"</span>)
            }
        }
        .tag(<span class="hljs-string">"recurring"</span>)

    <span class="hljs-type">SummaryView</span>()
        .tabItem {
            <span class="hljs-type">Label</span> {
                <span class="hljs-type">Text</span>(<span class="hljs-string">"Summary"</span>)
            } icon: {
                <span class="hljs-type">Image</span>(systemName: <span class="hljs-string">"chart.pie"</span>)
            }
        }
        .tag(<span class="hljs-string">"summary"</span>)

    <span class="hljs-type">MoreView</span>()
        .tabItem {
            <span class="hljs-type">Label</span> {
                <span class="hljs-type">Text</span>(<span class="hljs-string">"More"</span>)
            } icon: {
                <span class="hljs-type">Image</span>(systemName: <span class="hljs-string">"ellipsis.circle"</span>)
            }
        }
        .tag(<span class="hljs-string">"more"</span>)
}
</code></pre>
<p>With this code we will get this tab navigation:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678111776816/7dea251d-1569-43ea-b0c0-fffebf70e5a1.png" alt class="image--center mx-auto" /></p>
<p>Did you notice something strange? In my code, I used regular variants of the icons, but in the app, they are filled. And this is a...</p>
<h2 id="heading-default-swiftui-behavior">Default SwiftUI behavior</h2>
<p>Since iOS 15 it just works this way. Apple decided to render TabItem icons as filled on iOS and outlined on iPadOS and macOS. The same code will result in a little different look on each platform.</p>
<p>Let's see how the same icons in Apple's <strong>Developer</strong> app look on iOS, iPadOS and macOS:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678106444754/3a576c97-4f94-4721-8960-4a75ced0c277.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678106999291/f67c51e8-d83d-4081-8e87-9ab8fd7656c2.png" alt class="image--center mx-auto" /></p>
<p>It's part of Apple's design and you can read more about <strong>Tab bars</strong> in <strong>Human Interface Guidelines</strong> here:<br /><a target="_blank" href="https://developer.apple.com/design/human-interface-guidelines/components/navigation-and-search/tab-bars/">https://developer.apple.com/design/human-interface-guidelines/components/navigation-and-search/tab-bars/</a></p>
<p>And also about <strong>Icons</strong> here:<br /><a target="_blank" href="https://developer.apple.com/design/human-interface-guidelines/foundations/icons">https://developer.apple.com/design/human-interface-guidelines/foundations/icons</a></p>
<h2 id="heading-outlined-tabs-in-ios">Outlined tabs in iOS</h2>
<p>But what if we want to have outlined icons in the tab navigation in iOS for some reason? The good news is that it is quite easy to do. We can add this one line of code to each Label in our TabView to tell SwiftUI not to change variants of our icons:</p>
<pre><code class="lang-swift">.environment(\.symbolVariants, .<span class="hljs-keyword">none</span>)
</code></pre>
<p>Now let's add one variable in our view:</p>
<pre><code class="lang-swift">@<span class="hljs-type">State</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> selectedTab = <span class="hljs-string">"transactions"</span>
</code></pre>
<p>And let's use a ternary operator in our code like this:</p>
<pre><code class="lang-swift"><span class="hljs-type">Image</span>(systemName: (<span class="hljs-keyword">self</span>.selectedTab == <span class="hljs-string">"summary"</span>) ? <span class="hljs-string">"chart.pie.fill"</span> : <span class="hljs-string">"chart.pie"</span>)
</code></pre>
<p>If you are not familiar with ternary operator syntax, this basically means:</p>
<p>If <strong>selectedTab</strong> is equal to <strong>"summary"</strong>, use the <strong>chart.pie.fill</strong> icon. If not, use the <strong>chart.pie</strong> icon.</p>
<p>Let's see how the whole TabView code would look like with these changes:</p>
<pre><code class="lang-swift"><span class="hljs-type">TabView</span>(selection: $selectedTab) {

    <span class="hljs-type">TransactionsView</span>()
        .tabItem {
            <span class="hljs-type">Label</span> {
                <span class="hljs-type">Text</span>(<span class="hljs-string">"Transactions"</span>, comment: <span class="hljs-string">"Tab view navigation item."</span>)
            } icon: {
                <span class="hljs-type">Image</span>(systemName: (<span class="hljs-keyword">self</span>.selectedTab == <span class="hljs-string">"transactions"</span>) ? <span class="hljs-string">"arrow.left.arrow.right.square.fill"</span> : <span class="hljs-string">"arrow.left.arrow.right.square"</span>)
            }
            .environment(\.symbolVariants, .<span class="hljs-keyword">none</span>)
        }
        .tag(<span class="hljs-string">"transactions"</span>)

    <span class="hljs-type">RecurringView</span>()
        .tabItem {
            <span class="hljs-type">Label</span> {
                <span class="hljs-type">Text</span>(<span class="hljs-string">"Recurring"</span>, comment: <span class="hljs-string">"Tab view navigation item."</span>)
            } icon: {
                <span class="hljs-type">Image</span>(systemName: (<span class="hljs-keyword">self</span>.selectedTab == <span class="hljs-string">"recurring"</span>) ? <span class="hljs-string">"arrow.clockwise.circle.fill"</span> : <span class="hljs-string">"arrow.clockwise.circle"</span>)
            }
            .environment(\.symbolVariants, .<span class="hljs-keyword">none</span>)
        }
        .tag(<span class="hljs-string">"recurring"</span>)

    <span class="hljs-type">SummaryView</span>()
        .tabItem {
            <span class="hljs-type">Label</span> {
                <span class="hljs-type">Text</span>(<span class="hljs-string">"Summary"</span>, comment: <span class="hljs-string">"Tab view navigation item."</span>)
            } icon: {
                <span class="hljs-type">Image</span>(systemName: (<span class="hljs-keyword">self</span>.selectedTab == <span class="hljs-string">"summary"</span>) ? <span class="hljs-string">"chart.pie.fill"</span> : <span class="hljs-string">"chart.pie"</span>)
            }
            .environment(\.symbolVariants, .<span class="hljs-keyword">none</span>)
        }
        .tag(<span class="hljs-string">"summary"</span>)

    <span class="hljs-type">MoreView</span>()
        .tabItem {
            <span class="hljs-type">Label</span> {
                <span class="hljs-type">Text</span>(<span class="hljs-string">"More"</span>, comment: <span class="hljs-string">"Tab view navigation item."</span>)
            } icon: {
                <span class="hljs-type">Image</span>(systemName: (<span class="hljs-keyword">self</span>.selectedTab == <span class="hljs-string">"more"</span>) ? <span class="hljs-string">"ellipsis.circle.fill"</span> : <span class="hljs-string">"ellipsis.circle"</span>)
            }
            .environment(\.symbolVariants, .<span class="hljs-keyword">none</span>)
        }
        .tag(<span class="hljs-string">"more"</span>)

}
</code></pre>
<p>This way selected tab will have a filled icon variant and other tabs will have a regular, outlined icon.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678111767766/2476575f-97f7-45fb-a3ef-a1c742c3f729.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-accessibility">Accessibility</h2>
<p>If you know anything about accessibility, at this point you may wonder: Is it better to override default iOS behavior and use the fill icon for the selected tab item and outlined variant for all the other tabs? And the answer is: Yes and No 🙃</p>
<p>In general, relying on only one thing to distinguish some items is not a good idea. In our case, this one thing would be color, if all the icons would be filled. The active tab item is blue. Other tab items are grey. Users with colorblindness might see all of them as grey. And it wouldn't be clear to them which tab is active. So it's good to mix outlined and filled icons, right? Right!</p>
<p>But... Apple does a great job with accessibility. iOS users can choose from a variety of different assistive technologies. One of the iOS features that could help you if you have colorblindness is <strong>Accessibility / Display &amp; Text Size / Button Shapes</strong>. If this setting is ON, text buttons will be underlined and some other buttons will have a highlighted shape when it's necessary.</p>
<p>Here's an example of how this feature works with TabView without any custom changes. All the tabs have filled icons, but the active tab has an additional background.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1678112463289/8339bb4d-8b37-4670-9344-050817bd2391.png" alt class="image--center mx-auto" /></p>
<p>Are we done? Almost 🙃 Because at this point we can also have a valid question: "Will all our users who would need this feature know about it?".</p>
<p>And the answer is: "I don't know" 😅</p>
<h2 id="heading-so-what-should-i-do">So? What should I do?</h2>
<p>Every project is different. Every app has a different target audience. But if you don't know about any special requirements, I would do this:</p>
<ol>
<li><p>Start as simply as possible. Use default components, don't override the default look and behavior and let iOS, iPadOS and macOS do their job.</p>
</li>
<li><p>If you'll get any user feedback suggesting that a custom tab bar would be better for some reason, make the necessary changes. And now you know how 😉</p>
</li>
</ol>
<p>Thank you for reading my article 🙂 Any likes, comments and shares are highly appreciated.</p>
<h2 id="heading-thank-you-for-reading">Thank you for reading!</h2>
<p>📱Check out my apps on the App Store:<br /><a target="_blank" href="https://apps.apple.com/developer/next-planet/id1495155532">https://apps.apple.com/developer/next-planet/id1495155532</a></p>
<p>☕ If you like what I do, consider supporting me on Ko-fi! Every little bit means the world!<br /><a target="_blank" href="https://ko-fi.com/kslazinski">https://ko-fi.com/kslazinski</a></p>
]]></content:encoded></item><item><title><![CDATA[1 button away from better UX]]></title><description><![CDATA[Sometimes adding 1 button can improve the User Experience of your app. But how to know which button? 🙃 That's where UX research comes to play.
As my friend Mateusz Jedraszczyk likes to say, User Experience without users is just some experience. Or i...]]></description><link>https://blog.next-planet.com/1-button-away-from-better-ux</link><guid isPermaLink="true">https://blog.next-planet.com/1-button-away-from-better-ux</guid><category><![CDATA[UX]]></category><category><![CDATA[user experience]]></category><category><![CDATA[iOS]]></category><category><![CDATA[ios app development]]></category><category><![CDATA[UXdesign ]]></category><dc:creator><![CDATA[Kris Slazinski]]></dc:creator><pubDate>Wed, 18 Jan 2023 04:18:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1674014903683/e998e46c-05eb-42bf-b0a6-90c5f2746a64.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Sometimes adding 1 button can improve the User Experience of your app. But how to know which button? 🙃 That's where UX research comes to play.</p>
<p>As my friend <a target="_blank" href="https://www.linkedin.com/in/ACoAABI8N9wBnOGTwCIcVnwux6QwEUSeJ7rZUDI">Mateusz Jedraszczyk</a> likes to say, User Experience without users is just some experience. Or in other words, there's no User Experience without users. UX design should always be an answer to people's needs. You can't just design new screens and user flows in isolation from users. You need to know what they need and what problems they have.</p>
<p>Two things I did to identify what changes I need to implement in my iOS app Numi:</p>
<p><strong>1. Usability Tests.</strong><br />I conducted them a few weeks ago with a few people. Remotely, using the FaceTime app.<br /><a target="_blank" href="https://apps.apple.com/app/id1636232810">Numi</a> is an app for managing personal finances, in which you can record your income and expenses. And few of the testers told me that they want to be able to add new categories while adding transactions.<br />OK, so I knew it is one of the things I need to improve.</p>
<p><strong>2. Direct Feedback from users.</strong><br />My app has an implemented email link. This means that users can contact me directly by sending me an email from the app. And a few days ago one of my users did. He described how annoying it is to close Add Transaction screen, go to Settings, add New Category, and go back to Add Transaction again. Especially now, when he's new to Numi and didn't set up all his categories yet.<br />Now I knew. The ability to add new categories on the Add Transaction screen is not just one of the features I should add. It's one of the most important features I need to add. One cool thing is that it's also not something difficult to implement :)</p>
<p>And here it is, the current and the upcoming version of my iOS app Numi.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674014578817/bfba857a-c696-4b99-a21d-6c76596756da.png" alt="Before and After examples of the Numi app screen with list of categories. After version includes Add New Category button (plus icon in the top right of the screen)." class="image--center mx-auto" /></p>
<p>The main difference is added + button. Soon users will be able to add new categories not only from Settings but also on Add Transaction and Edit Transaction screens.</p>
<p>📱Link to Numi on the App Store:<br /><a target="_blank" href="https://apps.apple.com/app/id1636232810">https://apps.apple.com/app/id1636232810</a></p>
<h2 id="heading-thank-you-for-reading">Thank you for reading!</h2>
<p>☕ If you like what I do, consider supporting me on Ko-fi! Every little bit means the world!<br /><a target="_blank" href="https://ko-fi.com/kslazinski">https://ko-fi.com/kslazinski</a></p>
]]></content:encoded></item></channel></rss>