<?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[Inner Machinations of a Starfish]]></title><description><![CDATA[Learnings from successes (and struggles) with SwiftUI ❤️

🧽 [https://cleanshare.app](https://cleanshare.app)]]></description><link>https://ptrkstr.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Tue, 13 Oct 2026 18:21:52 GMT</lastBuildDate><atom:link href="https://ptrkstr.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[String.count vs NSString.length]]></title><description><![CDATA[I recently came across a bug in Lyrcs that caused an incorrect rhyme highlight to be applied.

Mat and cataa do not rhyme, yet Lyrcs is showing they do. The cause is a simple one but easy to miss; String.count and NSString.length do not always return...]]></description><link>https://ptrkstr.hashnode.dev/stringcount-vs-nsstringlength</link><guid isPermaLink="true">https://ptrkstr.hashnode.dev/stringcount-vs-nsstringlength</guid><category><![CDATA[iOS]]></category><category><![CDATA[macOS]]></category><category><![CDATA[Swift]]></category><dc:creator><![CDATA[Patrick]]></dc:creator><pubDate>Mon, 27 May 2024 17:03:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1716712600866/bef95f30-dc2e-4f3d-9b28-3d254470d0e1.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I recently came across a bug in <a target="_blank" href="https://lyrcs.app/">Lyrcs</a> that caused an incorrect rhyme highlight to be applied.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716713374883/b7a9280e-109f-475c-9316-d4501cb1fb34.png" alt class="image--center mx-auto" /></p>
<p><code>Mat</code> and <code>cataa</code> do not rhyme, yet <a target="_blank" href="https://lyrcs.app/">Lyrcs</a> is showing they do. The cause is a simple one but easy to miss; <code>String.count</code> and <code>NSString.length</code> do not always return the same value especially when emojis are involved. Lyrcs was performing the following:</p>
<pre><code class="lang-swift"><span class="hljs-keyword">let</span> string: <span class="hljs-type">String</span> = ...
<span class="hljs-keyword">let</span> range = <span class="hljs-type">NSRange</span>(location: <span class="hljs-number">0</span>, length: string.<span class="hljs-built_in">count</span>)
(string <span class="hljs-keyword">as</span> <span class="hljs-type">NSString</span>).enumerateSubstrings(<span class="hljs-keyword">in</span>: range, options: .byWords) { substring, range, <span class="hljs-number">_</span>, <span class="hljs-number">_</span> <span class="hljs-keyword">in</span>
    ...
}
</code></pre>
<p>This meant that the <code>range</code> was a few characters short of the whole range of <code>string</code>.</p>
<p>An excellent resource to learn more about this is <a target="_blank" href="https://docs.swift.org/swift-book/documentation/the-swift-programming-language/stringsandcharacters/#Counting-Characters">Counting Characters</a> from the Swift programming language book:</p>
<blockquote>
<p>The count of the characters returned by the <code>count</code> property isn’t always the same as the <code>length</code> property of an <code>NSString</code> that contains the same characters. The length of an <code>NSString</code> is based on the number of 16-bit code units within the string’s UTF-16 representation and not the number of Unicode extended grapheme clusters within the string.</p>
</blockquote>
<p>There are a couple of ways to solve this:</p>
<ol>
<li><p>Use a Swift range created from <code>string</code></p>
<pre><code class="lang-swift"> <span class="hljs-keyword">let</span> string: <span class="hljs-type">String</span> = ...
 <span class="hljs-keyword">let</span> range = string.startIndex..&lt;string.endIndex
 string.enumerateSubstrings(<span class="hljs-keyword">in</span>: range, options: .byWords) { substring, range, <span class="hljs-number">_</span>, <span class="hljs-number">_</span> <span class="hljs-keyword">in</span>
     ...
 }
</code></pre>
</li>
<li><p>Use <code>NSString.length</code></p>
<pre><code class="lang-swift"> <span class="hljs-keyword">let</span> string: <span class="hljs-type">String</span> = ...
 <span class="hljs-keyword">let</span> range = <span class="hljs-type">NSRange</span>(location: <span class="hljs-number">0</span>, length: (string <span class="hljs-keyword">as</span> <span class="hljs-type">NSString</span>).length
 (string <span class="hljs-keyword">as</span> <span class="hljs-type">NSString</span>).enumerateSubstrings(<span class="hljs-keyword">in</span>: range, options: .byWords) { substring, range, <span class="hljs-number">_</span>, <span class="hljs-number">_</span> <span class="hljs-keyword">in</span>
     ...
 }
</code></pre>
</li>
<li><p>Use <code>Swift.utf16.count</code></p>
<pre><code class="lang-swift"> <span class="hljs-keyword">let</span> string: <span class="hljs-type">String</span> = ...
 <span class="hljs-keyword">let</span> range = <span class="hljs-type">NSRange</span>(location: <span class="hljs-number">0</span>, length: string.utf16.<span class="hljs-built_in">count</span>)
 (string <span class="hljs-keyword">as</span> <span class="hljs-type">NSString</span>).enumerateSubstrings(<span class="hljs-keyword">in</span>: range, options: .byWords) { substring, range, <span class="hljs-number">_</span>, <span class="hljs-number">_</span> <span class="hljs-keyword">in</span>
     ...
 }
</code></pre>
</li>
</ol>
<p>The pure Swift approach would be the first one but there are times where you're dealing with Objective C APIs and it may be easier to resort to the trusty <code>as NSString</code> cast. You'll be pleased to know this has since been fixed in <a target="_blank" href="https://lyrcs.app/">Lyrcs</a> 3.5.1.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716714305565/d40e9205-6630-4a4f-bdca-0f7952b02932.png" alt class="image--center mx-auto" /></p>
]]></content:encoded></item><item><title><![CDATA[Presenting a Sheet from a Popover Will Disable the Keyboard in iOS (SwiftUI Bug)]]></title><description><![CDATA[This is occurring as of Xcode 15.3 (15E204a) on simulator and device

There is a situation in Lyrcs where:

UITextView has keyboard focus

Popover appears from a button tap

Sheet appears from the popover


When the user dismisses the sheet and popov...]]></description><link>https://ptrkstr.hashnode.dev/swiftui-keyboard-popover-sheet-bug</link><guid isPermaLink="true">https://ptrkstr.hashnode.dev/swiftui-keyboard-popover-sheet-bug</guid><category><![CDATA[iOS]]></category><category><![CDATA[SwiftUI]]></category><dc:creator><![CDATA[Patrick]]></dc:creator><pubDate>Sat, 18 May 2024 08:09:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1716019684048/341d0774-eed2-4dd6-a84f-0116bdcee957.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>This is occurring as of Xcode 15.3 (15E204a) on simulator and device</p>
</blockquote>
<p>There is a situation in <a target="_blank" href="https://lyrcs.app">Lyrcs</a> where:</p>
<ol>
<li><p><code>UITextView</code> has keyboard focus</p>
</li>
<li><p>Popover appears from a button tap</p>
</li>
<li><p>Sheet appears from the popover</p>
</li>
</ol>
<p>When the user dismisses the sheet and popover, <code>UITextView</code> will no longer have keyboard focus but the user can <strong>no longer</strong> make the keyboard appear by tapping on the <code>UITextView</code> . If the user holds down on the text input then the caret appears but still no keyboard. Even when using iOS simulator, characters typed with the macOS keyboard do not appear in the text input.</p>
<p>This occurs with any keyboard focus element; <code>UITextField</code>, <code>TextField</code>, <code>TextEditor</code> etc.</p>
<p>This is demonstrated with the following sample code:</p>
<pre><code class="lang-swift"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ContentView</span>: <span class="hljs-title">View</span> </span>{
    @<span class="hljs-type">State</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> text = <span class="hljs-string">""</span>
    @<span class="hljs-type">State</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> isPopoverPresented: <span class="hljs-type">Bool</span> = <span class="hljs-literal">false</span>
    @<span class="hljs-type">State</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> isSheetPresented: <span class="hljs-type">Bool</span> = <span class="hljs-literal">false</span>

    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">VStack</span> {
            <span class="hljs-type">Button</span>(<span class="hljs-string">"Show Popover"</span>) {
                isPopoverPresented = <span class="hljs-literal">true</span>
            }
            .popover(isPresented: $isPopoverPresented, content: {
                <span class="hljs-type">Button</span>(<span class="hljs-string">"Show Sheet"</span>) {
                    isSheetPresented = <span class="hljs-literal">true</span>
                }
                    .presentationCompactAdaptation(.popover)
                    .sheet(isPresented: $isSheetPresented, content: {
                        <span class="hljs-type">Text</span>(<span class="hljs-string">"I am a sheet"</span>)
                    })
            })
            <span class="hljs-type">TextField</span>(<span class="hljs-string">"Enter text"</span>, text: $text)
        }
    }
}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716018869522/b0a61ac1-5de4-4fee-a9ca-8986acbe8043.gif" alt class="image--center mx-auto" /></p>
<p>Until this is fixed, <a target="_blank" href="https://lyrcs.app">Lyrcs</a> uses the following workaround (placed on the <code>VStack</code>) which dismisses the keyboard when the popover is presented:</p>
<pre><code class="lang-swift"><span class="hljs-class"><span class="hljs-keyword">extension</span> <span class="hljs-title">View</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">workaround_dismissKeyboardWhenPopoverAppears</span><span class="hljs-params">(isPopoverPresented: Bool)</span></span> -&gt; some <span class="hljs-type">View</span> {
        onChange(of: isPopoverPresented) { previous, new <span class="hljs-keyword">in</span>
            <span class="hljs-keyword">if</span> new, new != previous {
                <span class="hljs-type">UIApplication</span>.shared.sendAction(#selector(<span class="hljs-type">UIResponder</span>.resignFirstResponder),
                                                    to: <span class="hljs-literal">nil</span>, from: <span class="hljs-literal">nil</span>, <span class="hljs-keyword">for</span>: <span class="hljs-literal">nil</span>)
            }
        }
    }
}
</code></pre>
<p>This has been submitted to Apple (FB13802395).</p>
]]></content:encoded></item><item><title><![CDATA[Add a PRO Badge to macOS Menu Buttons in SwiftUI]]></title><description><![CDATA[You've just added a print feature to your macOS app after reading my other article Apply FocusedValue for Active macOS Scenes Only in SwiftUI.
You've decided to only offer it to users that have paid for a subscription called PRO (which isn't the case...]]></description><link>https://ptrkstr.hashnode.dev/add-a-pro-badge-to-macos-menu-buttons-in-swiftui</link><guid isPermaLink="true">https://ptrkstr.hashnode.dev/add-a-pro-badge-to-macos-menu-buttons-in-swiftui</guid><category><![CDATA[SwiftUI]]></category><category><![CDATA[macOS]]></category><category><![CDATA[Swift]]></category><dc:creator><![CDATA[Patrick]]></dc:creator><pubDate>Sat, 11 May 2024 20:00:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1714896586798/614fcedc-dccd-40d0-a99c-b24b36e1dfd2.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You've just added a print feature to your macOS app after reading my other article <a target="_blank" href="https://blog.ptrkstr.com/apply-focusedvalue-for-active-macos-scenes-only">Apply FocusedValue for Active macOS Scenes Only in SwiftUI</a>.</p>
<p>You've decided to only offer it to users that have paid for a subscription called PRO (which isn't the case with <a target="_blank" href="https://lyrcs.app">Lyrcs</a>, printing is FREE 😉).</p>
<p>Can you guess how easy it is?</p>
<pre><code class="lang-swift"><span class="hljs-type">DocumentGroup</span>(newDocument: <span class="hljs-type">AppDocument</span>()) { file <span class="hljs-keyword">in</span>
    <span class="hljs-type">ContentView</span>(document: file.$document)
}
.commands {
    <span class="hljs-type">CommandGroup</span>(replacing: .printItem) {
        <span class="hljs-type">Button</span>(<span class="hljs-string">"Print..."</span>) {
            <span class="hljs-built_in">print</span>?()
        }
        .keyboardShortcut(<span class="hljs-string">"p"</span>, modifiers: [.command])
        .disabled(<span class="hljs-built_in">print</span> == <span class="hljs-literal">nil</span>)
        .badge(<span class="hljs-string">"PRO"</span>) <span class="hljs-comment">// &lt;--- All you need</span>
    }
}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1714908697084/98d0d329-f716-4492-8f63-c785abe5308b.png" alt class="image--center mx-auto" /></p>
<p>Easy! You may start to see some PRO badges in <a target="_blank" href="https://lyrcs.app">Lyrcs</a> (but not for printing ❤️).</p>
]]></content:encoded></item><item><title><![CDATA[Apply FocusedValue for Active macOS Scenes Only in SwiftUI]]></title><description><![CDATA[You have just created the world's best Lyric writing app for macOS.
You get your first feature request; printing. Easy enough because you've heard you can add commands to your Scene:
struct ExampleApp: App {
    var body: some Scene {
        Documen...]]></description><link>https://ptrkstr.hashnode.dev/apply-focusedvalue-for-active-macos-scenes-only</link><guid isPermaLink="true">https://ptrkstr.hashnode.dev/apply-focusedvalue-for-active-macos-scenes-only</guid><category><![CDATA[SwiftUI]]></category><category><![CDATA[Swift]]></category><category><![CDATA[macOS]]></category><dc:creator><![CDATA[Patrick]]></dc:creator><pubDate>Sun, 05 May 2024 07:33:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1714894225181/9670923c-f902-4095-ac05-f362e2d27192.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You have just created the <a target="_blank" href="https://lyrcs.app">world's best Lyric writing app for macOS</a>.</p>
<p>You get your first feature request; <strong>printing</strong>. Easy enough because you've heard you can add <code>commands</code> to your <code>Scene</code>:</p>
<pre><code class="lang-swift"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ExampleApp</span>: <span class="hljs-title">App</span> </span>{
    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">Scene</span> {
        <span class="hljs-type">DocumentGroup</span>(newDocument: <span class="hljs-type">AppDocument</span>()) { file <span class="hljs-keyword">in</span>
            <span class="hljs-type">ContentView</span>(document: file.$document)
        }
        .commands {
            <span class="hljs-type">CommandGroup</span>(replacing: .printItem) {
                <span class="hljs-type">Button</span>(<span class="hljs-string">"Print..."</span>) {
                    <span class="hljs-comment">// ...</span>
                }
                .keyboardShortcut(<span class="hljs-string">"p"</span>, modifiers: [.command])
            }
        }
    }
}
</code></pre>
<p>You even throw in a cheeky shortcut because you care about making the lives of your users easier.</p>
<p>But wait a second, how does the app know which document to print given a user can have multiple documents opened at the same time.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1714893473833/83437a9c-8cd0-4d82-bbaf-11c861a6cdd2.jpeg" alt class="image--center mx-auto" /></p>
<p>Please welcome <code>focusedSceneValue(...)</code> to the stage 👏</p>
<p>Let's get started:</p>
<ol>
<li>Create a Focused Value</li>
</ol>
<pre><code class="lang-swift"><span class="hljs-class"><span class="hljs-keyword">extension</span> <span class="hljs-title">FocusedValues</span> </span>{
    <span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">PrintValueKey</span>: <span class="hljs-title">FocusedValueKey</span> </span>{
        <span class="hljs-keyword">typealias</span> <span class="hljs-type">Value</span> = () -&gt; <span class="hljs-type">Void</span>
    }

    <span class="hljs-keyword">var</span> <span class="hljs-built_in">print</span>: <span class="hljs-type">PrintValueKey</span>.<span class="hljs-type">Value?</span> {
        <span class="hljs-keyword">get</span> { <span class="hljs-keyword">self</span>[<span class="hljs-type">PrintValueKey</span>.<span class="hljs-keyword">self</span>] }
        <span class="hljs-keyword">set</span> { <span class="hljs-keyword">self</span>[<span class="hljs-type">PrintValueKey</span>.<span class="hljs-keyword">self</span>] = newValue }
    }
}
</code></pre>
<ol start="2">
<li>Add the Focused Value to your App</li>
</ol>
<pre><code class="lang-swift"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ExampleApp</span>: <span class="hljs-title">App</span> </span>{

    @<span class="hljs-type">FocusedValue</span>(\.<span class="hljs-built_in">print</span>) <span class="hljs-comment">// &lt;--- New</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> <span class="hljs-built_in">print</span>

    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">Scene</span> {
        <span class="hljs-type">DocumentGroup</span>(newDocument: <span class="hljs-type">AppDocument</span>()) { file <span class="hljs-keyword">in</span>
            <span class="hljs-type">ContentView</span>(document: file.$document)
        }
        .commands {
            <span class="hljs-type">CommandGroup</span>(replacing: .printItem) {
                <span class="hljs-type">Button</span>(<span class="hljs-string">"Print..."</span>) {
                    <span class="hljs-built_in">print</span>?() <span class="hljs-comment">// &lt;--- New</span>
                }
                .keyboardShortcut(<span class="hljs-string">"p"</span>, modifiers: [.command])
                .disabled(<span class="hljs-built_in">print</span> == <span class="hljs-literal">nil</span>) <span class="hljs-comment">// &lt;--- If no Scene is active, print is disabled.</span>
            }
        }
    }
}
</code></pre>
<ol start="3">
<li>Update your <code>ContentView</code> to print the document.</li>
</ol>
<pre><code class="lang-swift"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ContentView</span>: <span class="hljs-title">View</span> </span>{

    @<span class="hljs-type">Binding</span>
    <span class="hljs-keyword">var</span> document: <span class="hljs-type">AppDocument</span>

    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">TextEditor</span>(text: $document.text)
            .focusedSceneValue(\.<span class="hljs-built_in">print</span>) {
                <span class="hljs-built_in">print</span>(document)
            }
    }
}
</code></pre>
<p>Try it out and you too can offer an incredible printing experience like <a target="_blank" href="https://lyrcs.app">Lyrcs</a> ❤️</p>
]]></content:encoded></item><item><title><![CDATA[Show a SwiftUI Popover on iOS]]></title><description><![CDATA[I was recently experimenting with adding a popover inside Clean Share (app for removing tracking identifiers).
A quick search showed I needed to reach for one of these functions:
func popover<Content>(
    isPresented: Binding<Bool>, 
    attachmentA...]]></description><link>https://ptrkstr.hashnode.dev/show-a-swiftui-popover-on-ios</link><guid isPermaLink="true">https://ptrkstr.hashnode.dev/show-a-swiftui-popover-on-ios</guid><category><![CDATA[Swift]]></category><category><![CDATA[SwiftUI]]></category><category><![CDATA[iOS]]></category><dc:creator><![CDATA[Patrick]]></dc:creator><pubDate>Sat, 04 May 2024 09:21:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1714814255326/24ae7cc5-3f7c-4ca1-ae17-7895d1699d25.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I was recently experimenting with adding a popover inside <a target="_blank" href="https://cleanshare.app/">Clean Share</a> (app for removing tracking identifiers).</p>
<p>A quick search showed I needed to reach for one of these functions:</p>
<pre><code class="lang-swift"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">popover</span>&lt;Content&gt;<span class="hljs-params">(
    isPresented: Binding&lt;Bool&gt;, 
    attachmentAnchor: PopoverAttachmentAnchor = .rect<span class="hljs-params">(.bounds)</span></span></span>, 
    arrowEdge: <span class="hljs-type">Edge</span> = .top, 
    @<span class="hljs-type">ViewBuilder</span> content: @escaping () -&gt; <span class="hljs-type">Content</span>
) -&gt; some <span class="hljs-type">View</span> <span class="hljs-keyword">where</span> <span class="hljs-type">Content</span> : <span class="hljs-type">View</span>
</code></pre>
<pre><code class="lang-swift"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">popover</span>&lt;Item, Content&gt;<span class="hljs-params">(
    item: Binding&lt;Item?&gt;, 
    attachmentAnchor: PopoverAttachmentAnchor = .rect<span class="hljs-params">(.bounds)</span></span></span>, 
    arrowEdge: <span class="hljs-type">Edge</span> = .top, 
    @<span class="hljs-type">ViewBuilder</span> content: @escaping (<span class="hljs-type">Item</span>) -&gt; <span class="hljs-type">Content</span>
) -&gt; some <span class="hljs-type">View</span> <span class="hljs-keyword">where</span> <span class="hljs-type">Item</span> : <span class="hljs-type">Identifiable</span>, <span class="hljs-type">Content</span> : <span class="hljs-type">View</span>
</code></pre>
<p>This was my first attempt:</p>
<pre><code class="lang-swift"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ContentView</span>: <span class="hljs-title">View</span> </span>{
    @<span class="hljs-type">State</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> isPresented: <span class="hljs-type">Bool</span> = <span class="hljs-literal">false</span>
    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">Button</span>(<span class="hljs-string">"Show Popover"</span>) {
            isPresented = <span class="hljs-literal">true</span>
        }
        .popover(isPresented: $isPresented, content: {
            <span class="hljs-type">Text</span>(<span class="hljs-string">"Hello"</span>)
        })
    }
}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1714814478558/3cd036e8-92a8-4478-92d1-feafdf4c76e1.gif" alt class="image--center mx-auto" /></p>
<p>A modal isn't what I was after 😬</p>
<p>All is not lost though, in the iOS 16.4 SDK, we were blessed with:</p>
<pre><code class="lang-swift">   <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">presentationCompactAdaptation</span><span class="hljs-params">(<span class="hljs-number">_</span> adaptation: PresentationAdaptation)</span></span> -&gt; some <span class="hljs-type">View</span>
</code></pre>
<p>By specifying <code>.popover</code> INSIDE the popover content we can achieve exactly what we're after.</p>
<pre><code class="lang-swift">.popover(isPresented: $isPresented, content: {
    <span class="hljs-type">Text</span>(<span class="hljs-string">"Hello"</span>)
        .presentationCompactAdaptation(.popover)
})
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1714813425893/68bbb7ba-45a4-4afb-94c8-9e9bb773429b.gif" alt class="image--center mx-auto" /></p>
<p>Happy popover-ing and look forward to seeing a popover or two in <a target="_blank" href="https://cleanshare.app/">Clean Share</a> in the future!</p>
]]></content:encoded></item><item><title><![CDATA[Localize a SwiftUI app]]></title><description><![CDATA[App localization is crucial when tailoring an app to different regions and locales. Localization boosts user engagement, increases accessibility, and can significantly expand an app’s market reach.
I had concerns that localizing Clean Share (app for ...]]></description><link>https://ptrkstr.hashnode.dev/localize-a-swiftui-app</link><guid isPermaLink="true">https://ptrkstr.hashnode.dev/localize-a-swiftui-app</guid><category><![CDATA[iOS]]></category><category><![CDATA[SwiftUI]]></category><dc:creator><![CDATA[Patrick]]></dc:creator><pubDate>Sat, 13 Apr 2024 08:21:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1712997146358/c1ab9229-fa57-4a05-812f-8b0e2e2a1cee.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>App localization is crucial when tailoring an app to different regions and locales. Localization boosts user engagement, increases accessibility, and can significantly expand an app’s market reach.</p>
<p>I had concerns that localizing <a target="_blank" href="https://cleanshare.app/">Clean Share</a> (app for removing tracking identifiers) to German would be a painful process but found that SwiftUI and advancements in Xcode makes this straightforward. Follow along to see how easy it is 🎉</p>
<h2 id="heading-prepare-strings">Prepare Strings</h2>
<p>This can be done in two ways.</p>
<h3 id="heading-component-initializer">Component Initializer</h3>
<p>Many SwiftUI components come with an initializer that takes a <code>LocalizedStringKey</code>. This allows you to use a string literal.</p>
<pre><code class="lang-swift"><span class="hljs-type">Text</span>(<span class="hljs-string">"Hello"</span>)
<span class="hljs-type">Toggle</span>(<span class="hljs-string">"Is On"</span>, isOn: ...)
<span class="hljs-type">Button</span>(<span class="hljs-string">"Help"</span>, ...)
</code></pre>
<h3 id="heading-string-initializer">String Initializer</h3>
<p>If you have a string that is passed around, you can use the localized initializer.</p>
<pre><code class="lang-swift"><span class="hljs-type">String</span>(localized: <span class="hljs-string">"Buy a book"</span>)
</code></pre>
<p>It is also recommended to give a comment to provide context and assist localizers.</p>
<pre><code class="lang-swift"><span class="hljs-type">Text</span>(<span class="hljs-string">"Hello"</span>, comment: <span class="hljs-string">"The title of the home screen."</span>)
<span class="hljs-type">String</span>(localized: <span class="hljs-string">"Hello"</span>, comment: <span class="hljs-string">"The title of the home screen."</span>)
</code></pre>
<h2 id="heading-string-catalog">String Catalog</h2>
<p>An Xcode string catalog is a file format used to manage localized strings. It allows developers to store all the translations for different languages in a structured format, making it easier to manage and update text in multi-language applications.</p>
<p>Create a String Catalog via <code>File &gt; New &gt; File &gt; String Catalog</code></p>
<p><img src="https://docs-assets.developer.apple.com/published/6f463d0afd212879f7b1fa591e5311e2/localizing-and-varying-text-with-a-string-catalog-1-add-a-string-catalog@2x.png" alt="An Xcode screenshot showing the new file template with a string catalog file selected and highlighted." /></p>
<p>Add a new language using the bottom button.</p>
<p><img src="https://docs-assets.developer.apple.com/published/d9b2dd71764863073381f716e731724f/localizing-and-varying-text-with-a-string-catalog-2-add-language@2x.png" alt="An Xcode screenshot of the string catalog editor with a callout for the Add button near the bottom of the Language pane." /></p>
<p>Build your project to see the list of strings you prepared in the earlier step.</p>
<p><img src="https://docs-assets.developer.apple.com/published/90c67fb32bc0afc57298f407f3c2b963/localizing-and-varying-text-with-a-string-catalog-2a-add-text@2x.png" alt="An Xcode screenshot of the string catalog editor showing a newly added language setting with a 0 percent indicator beside it." /></p>
<p>Edit the strings for the new language using the editor.</p>
<p><img src="https://docs-assets.developer.apple.com/published/29cc3c4fe487c6f2f40766b359841eba/localizing-and-varying-text-with-a-string-catalog-3-add-translations@2x.png" alt="An Xcode screenshot showing the Project navigator on the left with a string catalog selected. The string catalog editor on the right contains English strings with Swedish translations." /></p>
<h2 id="heading-plurals">Plurals</h2>
<p>Languages have different rules for pluralizing nouns based on their specific grammatical structures. For example, English uses "1 Book" for singular and "X Books" for plural, while other languages may vary.</p>
<p>To support this, use string interpolation:</p>
<pre><code class="lang-swift"><span class="hljs-type">Text</span>(<span class="hljs-string">"\(bookCount) Books"</span>)
</code></pre>
<p>After building again, select the <code>String catalog &gt; Right Click Row &gt; Vary By Plural</code></p>
<p><img src="https://docs-assets.developer.apple.com/published/479cd9130764b79f80fe677fa5448e1f/localizing-and-varying-text-with-a-string-catalog-4-add-pluralization@2x.png" alt="An Xcode screenshot of the string catalog editor showing one of the localized strings selected with a contextual menu. The Vary by Plural option is selected." /></p>
<h2 id="heading-vary-by-device">Vary by Device</h2>
<p>It is also possible to vary strings by device. For example on smaller devices you may want to display less content or specify a particular interaction (tap vs click). See the screenshot above for the button location.</p>
<h2 id="heading-testing-translations">Testing Translations</h2>
<p>You can test your translations in two ways:</p>
<h3 id="heading-scheme">Scheme</h3>
<p><code>Product &gt; Scheme &gt; Edit Scheme &gt; Run &gt; Options &gt; App Language</code></p>
<p><img src="https://docs-assets.developer.apple.com/published/6161a4b408dc8e1233459fd277c55ad1/localizing-and-varying-text-with-a-string-catalog-6-testing@2x.png" alt="An Xcode screenshot showing an open scheme editor with the Swedish language selected in the App Language drop-down." /></p>
<h3 id="heading-settings">Settings</h3>
<p>Open the Settings app on device and either change the device language or for the particular app.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Localizing apps has never been easier with the addition of String catalogs and SwiftUI. Combine this with translations through an LLM and you can perforn localization very quickly. This allowed me to localize <a target="_blank" href="https://cleanshare.app/">Clean Share</a> to German in less than a day. See how it works for you ❤️</p>
<p><em>Further reading in</em> <a target="_blank" href="https://developer.apple.com/documentation/xcode/localizing-and-varying-text-with-a-string-catalog"><em>Apple's documentation</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Guide to Naming SwiftUI Components]]></title><description><![CDATA[Whilst making Clean Share (app for removing tracking identifiers) I came across many instances where I was appending View to the end of my SwiftUI component names. Have you also gotten into the habit of ending your component names with View? Follow t...]]></description><link>https://ptrkstr.hashnode.dev/swiftui-component-naming</link><guid isPermaLink="true">https://ptrkstr.hashnode.dev/swiftui-component-naming</guid><category><![CDATA[SwiftUI]]></category><category><![CDATA[iOS]]></category><dc:creator><![CDATA[Patrick]]></dc:creator><pubDate>Thu, 14 Mar 2024 21:29:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1710451381670/d1885b00-acf6-4483-ac61-1c877576b9c2.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Whilst making <a target="_blank" href="https://cleanshare.app">Clean Share</a> (app for removing tracking identifiers) I came across many instances where I was appending <code>View</code> to the end of my SwiftUI component names. Have you also gotten into the habit of ending your component names with <code>View</code>? Follow these steps to give your components better descriptive names.</p>
<ol>
<li><p>Look at the root view in your <code>body</code> i.e. <code>Image</code>, <code>Text</code>, <code>Label</code>, <code>Button</code>, <code>List</code> etc.</p>
</li>
<li><p>Replace your component's <code>View</code> suffix with that root view name.</p>
</li>
</ol>
<p>Some examples below could be achieved by using <code>…Style</code>, however these examples are purposely kept brief and the idea still applies as your components grow beyond the capability of <code>…Style</code>.</p>
<h2 id="heading-examples">Examples</h2>
<p>The following design is taken as inspiration.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1710450560336/f036c188-d537-4f8c-a1f3-4539c40cf08f.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-swift"><span class="hljs-comment">// ✅ TransactionCategoryImage instead of...</span>
<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">TransactionCategoryView</span>: <span class="hljs-title">View</span> </span>{
    <span class="hljs-keyword">let</span> image: <span class="hljs-type">String</span>
    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">Image</span>(image)
            .resizable()
            .frame(width: <span class="hljs-number">20</span>, height: <span class="hljs-number">20</span>)
    }
}
</code></pre>
<pre><code class="lang-swift"><span class="hljs-comment">// ✅ TransactionDescriptionText instead of...</span>
<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">TransactionDescriptionView</span>: <span class="hljs-title">View</span> </span>{
    <span class="hljs-keyword">let</span> description: <span class="hljs-type">String</span>
    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">Text</span>(description)
            .font(.caption2)
            .foregroundStyle(.primary)
    }
}
</code></pre>
<pre><code class="lang-swift"><span class="hljs-comment">// ✅ TransactionCostLabel instead of...</span>
<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">TransactionCostView</span>: <span class="hljs-title">View</span> </span>{
    <span class="hljs-keyword">let</span> amount: <span class="hljs-type">Double</span>
    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">Label</span>(<span class="hljs-type">String</span>(describing: amount), systemImage: <span class="hljs-string">"dollarsign"</span>)
            .font(.caption)
            .foregroundStyle(.secondary)
    }
}
</code></pre>
<pre><code class="lang-swift"><span class="hljs-comment">// ✅ TransactionButton instead of...</span>
<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">TransactionView</span>: <span class="hljs-title">View</span> </span>{
    <span class="hljs-keyword">let</span> transaction: <span class="hljs-type">Transaction</span>
    <span class="hljs-keyword">let</span> action: () -&gt; <span class="hljs-type">Void</span>
    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">Button</span>(action: action) {
            <span class="hljs-type">HStack</span> {
                <span class="hljs-type">TransactionCategoryImage</span>(image: transaction.image)
                <span class="hljs-type">TransactionDescriptionText</span>(description: transaction.description)
                <span class="hljs-type">TransactionCostLabel</span>(amount: transaction.amount)
            }
        }
    }
}
</code></pre>
<pre><code class="lang-swift"><span class="hljs-comment">// ✅ TransactionsList instead of...</span>
<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">TransactionsView</span>: <span class="hljs-title">View</span> </span>{
    <span class="hljs-keyword">let</span> transactions: [<span class="hljs-type">Transaction</span>]
    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">List</span>(transactions) { transaction <span class="hljs-keyword">in</span>
            <span class="hljs-type">TransactionButton</span>(transaction: transaction) { }
        }
        .listStyle(.plain)
    }
}
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>If you're finding too many instances of components named with <code>View</code> in your code just like I did in <a target="_blank" href="https://cleanshare.app">Clean Share</a>, this tip may be for you.</p>
]]></content:encoded></item></channel></rss>