<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>IOS on kunat.dev</title>
    <link>https://kunat.dev/tags/ios/</link>
    <description>Recent content in IOS on kunat.dev</description>
    <generator>Hugo -- 0.147.5</generator>
    <language>en-us</language>
    <copyright>2025 kunat.dev</copyright>
    <lastBuildDate>Tue, 23 Dec 2025 19:50:06 +0100</lastBuildDate>
    <atom:link href="https://kunat.dev/tags/ios/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Swift Package Manager Mirrors for Local Development</title>
      <link>https://kunat.dev/notes/spm-package-mirroring/</link>
      <pubDate>Tue, 23 Dec 2025 19:50:06 +0100</pubDate>
      <guid>https://kunat.dev/notes/spm-package-mirroring/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A dependency mirror refers to an alternate source location which exactly replicates the contents of the original source. [1]&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;I often end up in the same situation: I need to tweak an internal package that a project depends on. The usual options are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Drag and drop the repo into Xcode (Xcode replaces the remote dependency with the local copy)&lt;/li&gt;
&lt;li&gt;Change &lt;code&gt;Package.swift&lt;/code&gt; to a local path dependency&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both work, but I prefer a CLI-only workflow. Turns out SPM has a cleaner alternative: &lt;strong&gt;dependency mirroring!&lt;/strong&gt;&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>A dependency mirror refers to an alternate source location which exactly replicates the contents of the original source. [1]</p></blockquote>
<p>I often end up in the same situation: I need to tweak an internal package that a project depends on. The usual options are:</p>
<ul>
<li>Drag and drop the repo into Xcode (Xcode replaces the remote dependency with the local copy)</li>
<li>Change <code>Package.swift</code> to a local path dependency</li>
</ul>
<p>Both work, but I prefer a CLI-only workflow. Turns out SPM has a cleaner alternative: <strong>dependency mirroring!</strong></p>
<h2 id="set-a-mirror">Set a Mirror</h2>
<p>Point the remote URL to a local repo:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>swift package config set-mirror --original <span style="color:#e6db74">&#34;THE_ORIGINAL_URL&#34;</span> --mirror <span style="color:#e6db74">&#34;LOCAL_PATH&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Example</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># swift package config set-mirror --original &#34;https://github.com/apple/swift-argument-parser&#34; \</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#   --mirror &#34;/Users/user/Developer/swift-argument-parser&#34;</span>
</span></span></code></pre></div><p>To confirm:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>swift package config get-mirror --original <span style="color:#e6db74">&#34;https://github.com/apple/swift-argument-parser&#34;</span>
</span></span></code></pre></div><h2 id="important-notes">Important Notes</h2>
<ul>
<li>A mirror only changes where SPM fetches the repo from. It still resolves a version, branch, or revision.</li>
<li>If you want local edits to show up, you must commit them in the mirrored repo.</li>
<li>Use a branch or revision in <code>Package.swift</code>:</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-swift" data-lang="swift"><span style="display:flex;"><span>dependencies: [
</span></span><span style="display:flex;"><span>    .package(url: <span style="color:#e6db74">&#34;https://github.com/apple/swift-argument-parser&#34;</span>, branch: <span style="color:#e6db74">&#34;main&#34;</span>),
</span></span><span style="display:flex;"><span>]
</span></span></code></pre></div><ul>
<li>Mirrors are matched by the exact URL string. If your dependency uses the <code>.git</code> suffix, set a mirror for that URL too.</li>
</ul>
<h2 id="remove-the-mirror">Remove the Mirror</h2>
<p>Once you are done, remove it:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>swift package config unset-mirror --original https://github.com/apple/swift-argument-parser
</span></span></code></pre></div><p>If you added a <code>.git</code> mirror, remove that one as well.</p>
<h2 id="closing-thoughts">Closing Thoughts</h2>
<p>Mirrors are not a drop-in replacement for local path dependencies. You still need to commit changes, but in return you get a clean, repeatable CLI workflow that works across projects without touching Xcode.</p>
<h2 id="resources">Resources</h2>
<ul>
<li>[1] <a href="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0219-package-manager-dependency-mirroring.md" target="_blank" >swift-evolution/proposals/0219-package-manager-dependency-mirroring.md at main - swiftlang/swift-evolution</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Instant Platinum: My QR Code Shortcut</title>
      <link>https://kunat.dev/notes/instant-platinum-released/</link>
      <pubDate>Tue, 18 Nov 2025 18:55:23 +0100</pubDate>
      <guid>https://kunat.dev/notes/instant-platinum-released/</guid>
      <description>&lt;p&gt;&lt;em&gt;tl;dr &lt;a href=&#34;https://github.com/bkunat/instant-platinum&#34; target=&#34;_blank&#34; &gt;Instant Platinum&lt;/a&gt; v0.1.0 released! 🚀&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Instant Platinum is an unofficial client for Fitness Platinum. The audience is tiny: a gym franchise in Kraków and Katowice, Poland. That’s fine. This is mostly for me.&lt;/p&gt;
&lt;p&gt;I can’t stand the official app. It’s not terrible, but it’s bloated with things I don’t care about: booking classes, managing goals, shopping (??). I just want to see my entry QR code as soon as I open the app. Today it takes three taps. Not awful, but not great.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><em>tl;dr <a href="https://github.com/bkunat/instant-platinum" target="_blank" >Instant Platinum</a> v0.1.0 released! 🚀</em></p>
<p>Instant Platinum is an unofficial client for Fitness Platinum. The audience is tiny: a gym franchise in Kraków and Katowice, Poland. That’s fine. This is mostly for me.</p>
<p>I can’t stand the official app. It’s not terrible, but it’s bloated with things I don’t care about: booking classes, managing goals, shopping (??). I just want to see my entry QR code as soon as I open the app. Today it takes three taps. Not awful, but not great.</p>
<p>So I built my own thing. Enter Instant Platinum: the app figures out which club you’re near and pops up the right QR code instantly. No clicks required. Works on iOS and watchOS.</p>
<p>Open, scan, done.</p>
<p>It took two evenings to put together. I’ll probably never &ldquo;earn back&rdquo; the time I spent, but I’ll happily trade those hours for fewer moments of frustration.</p>
<p>Since the initial release, it’s morphed into a fun little hobby project. I recently shipped <a href="/notes/platinum-pulse-released/" >Platinum Pulse</a>, which I plan to plug in to show historical occupancy inside Instant Platinum. Next up: widgets and shortcuts so the code is ready before I even think about opening the app.</p>
<p>If you by any chance go to Fitness Platinum, go ahead and try it!</p>
]]></content:encoded>
    </item>
    <item>
      <title>How to Use Xcode 16&#39;s Buildable Folders with CocoaPods</title>
      <link>https://kunat.dev/notes/xcode16-folders-pods/</link>
      <pubDate>Wed, 11 Jun 2025 19:11:09 +0200</pubDate>
      <guid>https://kunat.dev/notes/xcode16-folders-pods/</guid>
      <description>&lt;p&gt;A few months ago, I decided to try Xcode&amp;rsquo;s new &lt;a href=&#34;https://dimillian.medium.com/why-you-should-use-xcode-16-buildable-folders-instead-of-groups-6f438611914d&#34; target=&#34;_blank&#34; &gt;buildable folders&lt;/a&gt; feature in my project. However, right after converting all my groups to folders, I discovered that CocoaPods had stopped working. Attempting to install project dependencies resulted in the following error:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ pod install
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;❌ ArgumentError - &lt;span style=&#34;color:#f92672&#34;&gt;[&lt;/span&gt;Xcodeproj&lt;span style=&#34;color:#f92672&#34;&gt;]&lt;/span&gt; Unable to find compatibility version string &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; object version &lt;span style=&#34;color:#e6db74&#34;&gt;`&lt;/span&gt;70&lt;span style=&#34;color:#e6db74&#34;&gt;`&lt;/span&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;At the time, I updated CocoaPods to the latest version, confirmed my project format was set to the latest Xcode version (16.3), and tried again. When the issue persisted, I assumed CocoaPods was incompatible with buildable folders and that I would need to migrate to Swift Package Manager (SPM) to use them. This conclusion seemed to be confirmed by several closed GitHub issues where the suggested workaround was to manually convert folders back to groups.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>A few months ago, I decided to try Xcode&rsquo;s new <a href="https://dimillian.medium.com/why-you-should-use-xcode-16-buildable-folders-instead-of-groups-6f438611914d" target="_blank" >buildable folders</a> feature in my project. However, right after converting all my groups to folders, I discovered that CocoaPods had stopped working. Attempting to install project dependencies resulted in the following error:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>$ pod install
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>❌ ArgumentError - <span style="color:#f92672">[</span>Xcodeproj<span style="color:#f92672">]</span> Unable to find compatibility version string <span style="color:#66d9ef">for</span> object version <span style="color:#e6db74">`</span>70<span style="color:#e6db74">`</span>.
</span></span></code></pre></div><p>At the time, I updated CocoaPods to the latest version, confirmed my project format was set to the latest Xcode version (16.3), and tried again. When the issue persisted, I assumed CocoaPods was incompatible with buildable folders and that I would need to migrate to Swift Package Manager (SPM) to use them. This conclusion seemed to be confirmed by several closed GitHub issues where the suggested workaround was to manually convert folders back to groups.</p>
<p>However, it turns out you don&rsquo;t have to wait to use Xcode&rsquo;s buildable folders with CocoaPods! The solution is surprisingly simple: set the <strong>Project Format</strong> in your project&rsquo;s settings to <strong>Xcode 16.0</strong>. Any other selection will trigger the <code>Xcodeproj</code> error.</p>
<p><img alt="hello there" loading="lazy" src="/notes/images/xcode16-folders-pods-header.jpg"></p>
<p>With that single change, you can start taking full advantage of Xcode&rsquo;s buildable folders in your CocoaPods projects!</p>
<h3 id="references">References</h3>
<ul>
<li><a href="https://github.com/CocoaPods/Xcodeproj/pull/985" target="_blank" >Xcodeproj PR #985</a></li>
<li><a href="https://github.com/CocoaPods/CocoaPods/issues/12671#issuecomment-2467142931" target="_blank" >CocoaPods Issue #12671</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Adding Swift Package Manager Support to a Legacy Objective-C Project</title>
      <link>https://kunat.dev/notes/spm-support-objc-project/</link>
      <pubDate>Tue, 03 Jun 2025 16:50:49 +0200</pubDate>
      <guid>https://kunat.dev/notes/spm-support-objc-project/</guid>
      <description>&lt;p&gt;I recently needed to add Swift Package Manager support to a legacy package that one of my projects was using. The package was originally distributed with CocoaPods. The primary reason for migrating from CocoaPods to SPM is that CocoaPods entered &lt;a href=&#34;https://blog.cocoapods.org/CocoaPods-Support-Plans/&#34; target=&#34;_blank&#34; &gt;maintenance mode&lt;/a&gt; a few months ago. Removing it will future-proof our projects and allow us to use the latest features, such as the &lt;a href=&#34;https://dimillian.medium.com/why-you-should-use-xcode-16-buildable-folders-instead-of-groups-6f438611914d&#34; target=&#34;_blank&#34; &gt;buildable folders&lt;/a&gt; introduced in Xcode 16.&lt;/p&gt;
&lt;h2 id=&#34;assumptions&#34;&gt;Assumptions&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Since this was an internal package, I did not need to maintain CocoaPods support. The goal was to replace it entirely with SPM. It is certainly possible to keep both, but that would require some additional work.&lt;/li&gt;
&lt;li&gt;The project I was working on is considered legacy; no one has touched it in about six years. Once this task is complete, I hope no one will need to update it for another half-dozen years. This allowed me to take some shortcuts, like integrating a third-party dependency directly into the codebase instead of spending time updating it to the latest version.&lt;/li&gt;
&lt;li&gt;All internal dependencies of your framework already support SPM.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With that said, let&amp;rsquo;s get started.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I recently needed to add Swift Package Manager support to a legacy package that one of my projects was using. The package was originally distributed with CocoaPods. The primary reason for migrating from CocoaPods to SPM is that CocoaPods entered <a href="https://blog.cocoapods.org/CocoaPods-Support-Plans/" target="_blank" >maintenance mode</a> a few months ago. Removing it will future-proof our projects and allow us to use the latest features, such as the <a href="https://dimillian.medium.com/why-you-should-use-xcode-16-buildable-folders-instead-of-groups-6f438611914d" target="_blank" >buildable folders</a> introduced in Xcode 16.</p>
<h2 id="assumptions">Assumptions</h2>
<ul>
<li>Since this was an internal package, I did not need to maintain CocoaPods support. The goal was to replace it entirely with SPM. It is certainly possible to keep both, but that would require some additional work.</li>
<li>The project I was working on is considered legacy; no one has touched it in about six years. Once this task is complete, I hope no one will need to update it for another half-dozen years. This allowed me to take some shortcuts, like integrating a third-party dependency directly into the codebase instead of spending time updating it to the latest version.</li>
<li>All internal dependencies of your framework already support SPM.</li>
</ul>
<p>With that said, let&rsquo;s get started.</p>
<h2 id="how">How?</h2>
<p>I&rsquo;ll assume your framework has no internal dependencies that require the same SPM treatment. If it does, start with the bottom-most dependency and work your way up.</p>
<h3 id="external-dependencies">External Dependencies</h3>
<p>The first step is to update all external dependencies to use SPM.</p>
<p>In my case, one of the dependencies was <a href="https://github.com/mxcl/PromiseKit" target="_blank" >PromiseKit</a>, which was stuck six major versions behind the latest release. Since the project was considered legacy and no further development was planned, I decided to cut some corners.</p>
<p>Instead of spending time updating PromiseKit from version 1 through 7, I opted to add SPM support to the version my project was currently using. This approach required significantly less work and proved to be the right choice.</p>
<h3 id="removing-the-old-dependency-manager">Removing the Old Dependency Manager</h3>
<p>With all dependencies migrated to SPM, you should now be able to remove the old dependency manager from your project.</p>
<h3 id="adding-a-swift-package-manifest">Adding a Swift Package Manifest</h3>
<p>Next, initialize a new Swift package in your project&rsquo;s root directory:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>swift package init
</span></span></code></pre></div><p>This will create a <code>Package.swift</code> file. Here is a basic configuration:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-swift" data-lang="swift"><span style="display:flex;"><span><span style="color:#75715e">// swift-tools-version: 6.1</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">PackageDescription</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">let</span> package = Package(
</span></span><span style="display:flex;"><span>    name: <span style="color:#e6db74">&#34;MyPackage&#34;</span>,
</span></span><span style="display:flex;"><span>    products: [
</span></span><span style="display:flex;"><span>        .library(
</span></span><span style="display:flex;"><span>            name: <span style="color:#e6db74">&#34;MyPackage&#34;</span>,
</span></span><span style="display:flex;"><span>            targets: [<span style="color:#e6db74">&#34;MyPackage&#34;</span>]),
</span></span><span style="display:flex;"><span>    ],
</span></span><span style="display:flex;"><span>    targets: [
</span></span><span style="display:flex;"><span>        .target(
</span></span><span style="display:flex;"><span>            name: <span style="color:#e6db74">&#34;MyPackage&#34;</span>),
</span></span><span style="display:flex;"><span>        .testTarget(
</span></span><span style="display:flex;"><span>            name: <span style="color:#e6db74">&#34;MyPackageTests&#34;</span>,
</span></span><span style="display:flex;"><span>            dependencies: [<span style="color:#e6db74">&#34;MyPackage&#34;</span>]
</span></span><span style="display:flex;"><span>        ),
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p>Move your project&rsquo;s source files to <code>Sources/MyPackage</code> and try to build the project. You will likely encounter one of the following errors:</p>
<pre tabindex="0"><code>SomeHeaderFile.h:19:9 &#39;AnotherHeaderFile.h&#39; file not found
</code></pre><p>or</p>
<pre tabindex="0"><code>public headers (&#34;include&#34;) directory path for &#39;MyPackage&#39; is invalid or not contained in the target
</code></pre><p>These errors indicate that the compiler cannot locate your project&rsquo;s header files. By default, SPM looks for public headers in an <code>include</code> directory.</p>
<p>Create an <code>include</code> directory inside <code>Sources/MyPackage</code> and move all of your project&rsquo;s public headers there.</p>
<p>If this structure doesn&rsquo;t suit your project, you can define a custom path for your public headers by specifying <code>publicHeadersPath</code> in your <code>Package.swift</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-swift" data-lang="swift"><span style="display:flex;"><span>    targets: [
</span></span><span style="display:flex;"><span>        .target(
</span></span><span style="display:flex;"><span>            name: <span style="color:#e6db74">&#34;MyPackage&#34;</span>,
</span></span><span style="display:flex;"><span>            publicHeadersPath: <span style="color:#e6db74">&#34;some/custom/path&#34;</span>
</span></span><span style="display:flex;"><span>        ),
</span></span><span style="display:flex;"><span>        .testTarget(
</span></span><span style="display:flex;"><span>            name: <span style="color:#e6db74">&#34;MyPackageTests&#34;</span>,
</span></span><span style="display:flex;"><span>            dependencies: [<span style="color:#e6db74">&#34;MyPackage&#34;</span>]
</span></span><span style="display:flex;"><span>        ),
</span></span><span style="display:flex;"><span>    ]
</span></span></code></pre></div><p><strong>How do I locate my project&rsquo;s public headers?</strong></p>
<p>Your project most likely had an Xcode project file (<code>.xcproj</code>) before you started adding SPM support. You can find a list of your public headers in the &ldquo;Build Phases&rdquo; tab, under the &ldquo;Headers&rdquo; section.</p>
<p><img alt="changedetection" loading="lazy" src="/notes/images/spm-objc-public-headers.jpg"></p>
<p>After correctly configuring the public header files, your project might build. If it still doesn&rsquo;t, you may have other header files that need to be discoverable. If that&rsquo;s the case, you&rsquo;ll need to add search paths for your private header files using <code>headerSearchPath</code> in your cSettings:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-swift" data-lang="swift"><span style="display:flex;"><span>.target(
</span></span><span style="display:flex;"><span>    name: <span style="color:#e6db74">&#34;MyPackage&#34;</span>,
</span></span><span style="display:flex;"><span>    cSettings: [
</span></span><span style="display:flex;"><span>        .headerSearchPath(<span style="color:#e6db74">&#34;Payments&#34;</span>),
</span></span><span style="display:flex;"><span>        .headerSearchPath(<span style="color:#e6db74">&#34;Payments/ApplePayPaymentsService&#34;</span>),
</span></span><span style="display:flex;"><span>        .headerSearchPath(<span style="color:#e6db74">&#34;Transactions/Components&#34;</span>)
</span></span><span style="display:flex;"><span>    ]),
</span></span></code></pre></div><p>It&rsquo;s up to you whether you want to group all private header files in a single directory (<code>.headerSearchPath(&quot;Path/To/My/Private/Headers&quot;)</code>) or add all existing directories one by one using their current locations as I did in the snippet above.</p>
<p>With that done, you should now be able to build your project.</p>
<h2 id="troubleshooting">Troubleshooting</h2>
<p>In my case, after migrating all dependencies to SPM and adding the package manifest, I was able to delete the Xcode project file entirely. If that is not the case for you, you might encounter additional issues.</p>
<p>One common problem is the error <code>fatal error: framework '&lt;some_framework&gt;' not found</code> when trying to import an SPM dependency into an Objective-C codebase in a project managed via an Xcode project file (<code>.xcproj</code>).</p>
<p>In this situation, you may need to experiment with your project settings. <a href="https://developer.apple.com/forums/thread/120152" target="_blank" >This thread</a> on the Apple Developer Forums is an good starting point.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Once these steps are completed, you should be able to build your project. If you add your new package as a dependency via SPM, you can import it like any other Swift package. The same rules apply.</p>
<p><strong>Swift:</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-swift" data-lang="swift"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">MyPackage</span>
</span></span></code></pre></div><p><strong>Objective-C:</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-objectivec" data-lang="objectivec"><span style="display:flex;"><span>@import MyPackage;
</span></span></code></pre></div><h2 id="resources">Resources</h2>
<p>What helped me the most was looking at examples of existing Objective-C projects with SPM support. The following repositories are worth exploring:</p>
<ul>
<li><a href="https://github.com/PSPDFKit/PDFXKit" target="_blank" >PSPDFKit/PDFXKit</a>: A drop-in replacement for Apple&rsquo;s PDFKit, powered by the PSPDFKit framework.</li>
<li><a href="https://github.com/AliSoftware/OHHTTPStubs" target="_blank" >AliSoftware/OHHTTPStubs</a>: A library to easily stub your network requests.</li>
<li><a href="https://github.com/Mantle/Mantle" target="_blank" >Mantle/Mantle</a>: Model framework for Cocoa and Cocoa Touch</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Region-Specific Language Control: Implementing Forced Localization in iOS Apps</title>
      <link>https://kunat.dev/notes/removing-unwanted-localization-files/</link>
      <pubDate>Wed, 02 Apr 2025 22:08:53 +0200</pubDate>
      <guid>https://kunat.dev/notes/removing-unwanted-localization-files/</guid>
      <description>&lt;p&gt;My current project has a single target and multiple schemes. Each scheme represents the same app for different countries/regions. It&amp;rsquo;s essentially the same application with minor adjustments made for each of the supported jurisdictions.&lt;/p&gt;
&lt;p&gt;In Xcode projects, you define supported languages at the project level (not scheme level). Because of this structure, each app variant inherits the same language settings.&lt;/p&gt;
&lt;p&gt;My task was to force each app variant to support only the language for the region it was released in. The French app should only support French, no matter what the user&amp;rsquo;s device language is set to, and so on. This decision was made because large portions of the app are web-based and only supported a single language.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>My current project has a single target and multiple schemes. Each scheme represents the same app for different countries/regions. It&rsquo;s essentially the same application with minor adjustments made for each of the supported jurisdictions.</p>
<p>In Xcode projects, you define supported languages at the project level (not scheme level). Because of this structure, each app variant inherits the same language settings.</p>
<p>My task was to force each app variant to support only the language for the region it was released in. The French app should only support French, no matter what the user&rsquo;s device language is set to, and so on. This decision was made because large portions of the app are web-based and only supported a single language.</p>
<h2 id="solution">Solution</h2>
<p>There are multiple ways to solve this problem. What follows is the approach that worked best for my particular use case, given the project&rsquo;s current structure:</p>
<p>First, create a <a href="https://nshipster.com/xcconfig/" target="_blank" >build configuration file</a> for each of the schemes and define the <code>LOCALIZATIONS_TO_KEEP</code> variable.</p>
<pre tabindex="0"><code class="language-plist" data-lang="plist">LOCALIZATIONS_TO_KEEP = fr
</code></pre><p>Then, add a new build phase called &ldquo;Remove unused localizations.&rdquo; Remember to position it after the &ldquo;Copy Bundle Resources&rdquo; build phase:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Keeping only </span><span style="color:#e6db74">${</span>LOCALIZATIONS_TO_KEEP<span style="color:#e6db74">}</span><span style="color:#e6db74">.lproj&#34;</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;Application .app path: </span><span style="color:#e6db74">${</span>CODESIGNING_FOLDER_PATH<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Only search in the main app bundle, excluding </span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Frameworks directory and other dependency folders</span>
</span></span><span style="display:flex;"><span>find <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>CODESIGNING_FOLDER_PATH<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span> -type d -name <span style="color:#e6db74">&#34;*.lproj&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -not -path <span style="color:#e6db74">&#34;*/Frameworks/*&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -not -path <span style="color:#e6db74">&#34;*/PlugIns/*&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -not -path <span style="color:#e6db74">&#34;*/Watch/*&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  -not -path <span style="color:#e6db74">&#34;*/SwiftPM/*&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  | grep -v <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>LOCALIZATIONS_TO_KEEP<span style="color:#e6db74">}</span><span style="color:#e6db74">.lproj&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span>  | xargs rm -rf
</span></span></code></pre></div><blockquote>
<p><code>CODESIGNING_FOLDER_PATH</code> is an environment variable used in Xcode build scripts that represents the path to the app bundle being built. It typically points to the directory where the compiled application and its resources are placed before code signing occurs.</p></blockquote>
<p>You&rsquo;ll want to exclude locations where third-party dependencies might be located, because you can&rsquo;t guarantee that they support the same language you want to force the app to use.</p>
<p>You can verify that the script works by navigating to ${CODESIGNING_FOLDER_PATH} in derived data and checking if the script successfully removed all other localization files.</p>
<h2 id="alternative-solutions">Alternative Solutions</h2>
<p>This issue could be solved by setting <code>CFBundleLocalizations</code> and <code>CFBundleDevelopmentRegion</code> IF my project used separate targets instead of separate schemes for different apps.</p>
<p>Alternatively, I could dynamically select Info.plist files for each of the schemes. I opted against this approach since the project already had build configurations defined for each of the schemes.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Create a Safari Extension to Extract Article Content</title>
      <link>https://kunat.dev/notes/safari-extension-reader-mode/</link>
      <pubDate>Wed, 18 Dec 2024 18:59:56 +0100</pubDate>
      <guid>https://kunat.dev/notes/safari-extension-reader-mode/</guid>
      <description>&lt;p&gt;&lt;img alt=&#34;safari-extension-reader-mode-preview&#34; loading=&#34;lazy&#34; src=&#34;https://kunat.dev/notes/images/safari-extension-reader-mode-preview.png&#34;&gt;&lt;/p&gt;
&lt;p&gt;In this article, I&amp;rsquo;ll show you how to access a web page&amp;rsquo;s content, alter it using JavaScript, and display it using your Safari extension&amp;rsquo;s UI. Specifically, I&amp;rsquo;ll demonstrate how to display an article&amp;rsquo;s content in a popover.&lt;/p&gt;
&lt;p&gt;I often use Large Language Models (LLMs) to summarize articles when I&amp;rsquo;m unsure if they&amp;rsquo;re worth reading. Without the extension we&amp;rsquo;re about to build, I&amp;rsquo;d need to manually trigger Safari&amp;rsquo;s reader mode (⌘ + ⇧ + R), copy all content, and paste it into Claude or ChatGPT. This extension is a handy tool that reduces friction!&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><img alt="safari-extension-reader-mode-preview" loading="lazy" src="/notes/images/safari-extension-reader-mode-preview.png"></p>
<p>In this article, I&rsquo;ll show you how to access a web page&rsquo;s content, alter it using JavaScript, and display it using your Safari extension&rsquo;s UI. Specifically, I&rsquo;ll demonstrate how to display an article&rsquo;s content in a popover.</p>
<p>I often use Large Language Models (LLMs) to summarize articles when I&rsquo;m unsure if they&rsquo;re worth reading. Without the extension we&rsquo;re about to build, I&rsquo;d need to manually trigger Safari&rsquo;s reader mode (⌘ + ⇧ + R), copy all content, and paste it into Claude or ChatGPT. This extension is a handy tool that reduces friction!</p>
<p>Start by creating a new project using the &ldquo;Safari App Extension&rdquo; template, or add a Safari Extension target to your project. When prompted, set <code>type</code> to Safari App Extension (the default at the time of writing).</p>
<p>After the initial setup is complete, follow <a href="https://www.polpiella.dev/safari-extensions-swiftui" target="_blank" >this guide</a> to modify the template so that you can use SwiftUI to build the UI. Follow all the instructions except for one: do not delete <code>script.js</code>. We&rsquo;ll need it to capture the web page&rsquo;s content.</p>
<p>Once that&rsquo;s done, we&rsquo;ll proceed to get the current web page&rsquo;s content, modify it to extract the article content, and display it. You can check out the complete sample project <a href="https://github.com/bkunat/SafariReaderModeSample" target="_blank" >here</a>.</p>
<ol>
<li><strong>Getting Active Safari Page</strong> (<code>getActiveSafariPage</code>)</li>
</ol>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-swift" data-lang="swift"><span style="display:flex;"><span><span style="color:#66d9ef">let</span> page = <span style="color:#66d9ef">try</span> await getActiveSafariPage()
</span></span></code></pre></div><ul>
<li>Uses Safari&rsquo;s extension API to retrieve the current window, tab, and page</li>
<li>Returns a <code>SFSafariPage</code> object representing the active browser page</li>
</ul>
<ol start="2">
<li><strong>Requesting Content</strong> (<code>requestAndReceiveContent</code>)</li>
</ol>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-swift" data-lang="swift"><span style="display:flex;"><span>page.dispatchMessageToScript(
</span></span><span style="display:flex;"><span>    withName: SafariExtensionMessage.Name.getContent.rawValue,
</span></span><span style="display:flex;"><span>    userInfo: <span style="color:#66d9ef">nil</span>)
</span></span></code></pre></div><ul>
<li>Sends a &ldquo;getContent&rdquo; message to the injected content script</li>
</ul>
<ol start="3">
<li><strong>Content Script Processing</strong> (<code>script.js</code>)</li>
</ol>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getPageContent</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Tries to extract content in this order:
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// 1. Structured article data (JSON-LD)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// 2. Article elements with specific selectors
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#75715e">// 3. Fallback to body text
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">content</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">title</span><span style="color:#f92672">:</span> document.<span style="color:#a6e22e">title</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">body</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">getReaderContent</span>().<span style="color:#a6e22e">trim</span>()
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">safari</span>.<span style="color:#a6e22e">extension</span>.<span style="color:#a6e22e">dispatchMessage</span>(<span style="color:#e6db74">&#34;pageContent&#34;</span>, { <span style="color:#a6e22e">content</span> });
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><ol start="4">
<li><strong>Message Handling</strong> (<code>SafariExtensionHandler</code>)</li>
</ol>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-swift" data-lang="swift"><span style="display:flex;"><span><span style="color:#66d9ef">case</span> .pageContent:
</span></span><span style="display:flex;"><span>    NotificationCenter.<span style="color:#66d9ef">default</span>.post(
</span></span><span style="display:flex;"><span>        name: NSNotification.Name(SafariExtensionMessage.Notification.messageReceived.rawValue),
</span></span><span style="display:flex;"><span>        object: <span style="color:#66d9ef">nil</span>,
</span></span><span style="display:flex;"><span>        userInfo: userInfo
</span></span><span style="display:flex;"><span>    )
</span></span></code></pre></div><ul>
<li>Receives the content message from the script</li>
<li>Posts it to NotificationCenter</li>
</ul>
<ol start="5">
<li><strong>Content Reception</strong> (<code>PopoverViewModel</code>)</li>
</ol>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-swift" data-lang="swift"><span style="display:flex;"><span><span style="color:#66d9ef">for</span> await notification <span style="color:#66d9ef">in</span> NotificationCenter.<span style="color:#66d9ef">default</span>
</span></span><span style="display:flex;"><span>    .notifications(named: NSNotification.Name(SafariExtensionMessage.Notification.messageReceived.rawValue))
</span></span><span style="display:flex;"><span>    .compactMap({ notification <span style="color:#66d9ef">in</span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Converts notification data to WebPageContent</span>
</span></span><span style="display:flex;"><span>    })
</span></span></code></pre></div><ul>
<li>Listens for the notification with the page content</li>
<li>Converts the received data into a <code>WebPageContent</code> struct</li>
<li>Returns the structured content to be displayed in the popover</li>
</ul>
<h2 id="resources">Resources</h2>
<ul>
<li><a href="https://www.polpiella.dev/safari-extensions-swiftui" target="_blank" >How to build a Safari extension with SwiftUI</a></li>
<li><a href="https://github.com/bkunat/SafariReaderModeSample" target="_blank" >SafariReaderModeSample | GitHub</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Xcode: Missing Package Product (local package)</title>
      <link>https://kunat.dev/notes/xcode-missing-package-product/</link>
      <pubDate>Mon, 24 Jun 2024 18:48:01 +0200</pubDate>
      <guid>https://kunat.dev/notes/xcode-missing-package-product/</guid>
      <description>&lt;p&gt;When adding a local Swift package to your Xcode project, you might encounter the following error:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Missing package product `PACKAGE_NAME`
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img loading=&#34;lazy&#34; src=&#34;https://kunat.dev/notes/images/xcode-missing-package-product.jpg&#34;&gt;&lt;/p&gt;
&lt;p&gt;The issue often occurs when the package you&amp;rsquo;re trying to add is already open in Xcode. To resolve:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Close the package in Xcode.&lt;/li&gt;
&lt;li&gt;Close your main project.&lt;/li&gt;
&lt;li&gt;Reopen your main project.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I&amp;rsquo;m not sure what might be causing this issue or why reopening the project is required (a clean build won&amp;rsquo;t do).&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>When adding a local Swift package to your Xcode project, you might encounter the following error:</p>
<pre tabindex="0"><code>Missing package product `PACKAGE_NAME`
</code></pre><p><img loading="lazy" src="/notes/images/xcode-missing-package-product.jpg"></p>
<p>The issue often occurs when the package you&rsquo;re trying to add is already open in Xcode. To resolve:</p>
<ol>
<li>Close the package in Xcode.</li>
<li>Close your main project.</li>
<li>Reopen your main project.</li>
</ol>
<p>I&rsquo;m not sure what might be causing this issue or why reopening the project is required (a clean build won&rsquo;t do).</p>
<p>To learn more about local Swift packages read <a href="../swift-local-packages" >Working with Local Swift Packages</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Open Any Xcode Project Type with a Shell Script</title>
      <link>https://kunat.dev/notes/shell-script-for-opening-xcode-projects/</link>
      <pubDate>Mon, 29 Apr 2024 19:19:18 +0200</pubDate>
      <guid>https://kunat.dev/notes/shell-script-for-opening-xcode-projects/</guid>
      <description>&lt;p&gt;In my current role, we have around 30 projects distributed across roughly 20 different repositories. Most of them are Swift packages for various features used in our apps.&lt;/p&gt;
&lt;p&gt;This setup requires me to frequently switch between multiple projects. Just a few days ago, I was working on the login flow. This involved modifying two Swift packages and one Xcode project.&lt;/p&gt;
&lt;p&gt;Remembering whether a given project is a Swift package, a workspace (&lt;code&gt;.xcworkspace&lt;/code&gt;), or a standard project (&lt;code&gt;.xcodeproj&lt;/code&gt;) is exhausting. This adds unnecessary complexity and slows down the workflow. It would be better if we had a single command to open all of them at once. Here&amp;rsquo;s a script that does just that:&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>In my current role, we have around 30 projects distributed across roughly 20 different repositories. Most of them are Swift packages for various features used in our apps.</p>
<p>This setup requires me to frequently switch between multiple projects. Just a few days ago, I was working on the login flow. This involved modifying two Swift packages and one Xcode project.</p>
<p>Remembering whether a given project is a Swift package, a workspace (<code>.xcworkspace</code>), or a standard project (<code>.xcodeproj</code>) is exhausting. This adds unnecessary complexity and slows down the workflow. It would be better if we had a single command to open all of them at once. Here&rsquo;s a script that does just that:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-shell" data-lang="shell"><span style="display:flex;"><span><span style="color:#75715e"># The osp (Open Swift project) function first attempts to open a Package.swift file in the current directory; if it doesn&#39;t exist, it then searches for and opens either a .xcworkspace or a .xcodeproj directory, or outputs an error message if none are found.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>osp<span style="color:#f92672">()</span> <span style="color:#f92672">{</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Check for Package.swift in the current directory</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[[</span> -f <span style="color:#e6db74">&#34;Package.swift&#34;</span> <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>open <span style="color:#e6db74">&#34;Package.swift&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">return</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Look for .xcworkspace directories in the current directory</span>
</span></span><span style="display:flex;"><span>local workspace<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>find . -maxdepth <span style="color:#ae81ff">1</span> -name <span style="color:#e6db74">&#34;*.xcworkspace&#34;</span> -type d | head -n 1<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[[</span> -n <span style="color:#e6db74">&#34;</span>$workspace<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>open <span style="color:#e6db74">&#34;</span>$workspace<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">return</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Look for .xcodeproj directories if no .xcworkspace directories were found</span>
</span></span><span style="display:flex;"><span>local project<span style="color:#f92672">=</span><span style="color:#66d9ef">$(</span>find . -maxdepth <span style="color:#ae81ff">1</span> -name <span style="color:#e6db74">&#34;*.xcodeproj&#34;</span> -type d | head -n 1<span style="color:#66d9ef">)</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> <span style="color:#f92672">[[</span> -n <span style="color:#e6db74">&#34;</span>$project<span style="color:#e6db74">&#34;</span> <span style="color:#f92672">]]</span>; <span style="color:#66d9ef">then</span>
</span></span><span style="display:flex;"><span>open <span style="color:#e6db74">&#34;</span>$project<span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">return</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">fi</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Output error message if neither file type was found</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;No project or workspace files found&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">}</span>
</span></span></code></pre></div><p>To use it, simply append it to your <code>.zshrc</code> file and run <code>source ~/.zshrc</code> or restart the shell.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Quitting Reels: Unveiling Instagram Deep Links</title>
      <link>https://kunat.dev/notes/instagram-dms/</link>
      <pubDate>Sun, 31 Mar 2024 21:41:11 +0200</pubDate>
      <guid>https://kunat.dev/notes/instagram-dms/</guid>
      <description>&lt;p&gt;I’ve noticed that I’ve been wasting a lot of time watching Instagram Reels lately. This would not be a problem if I actually enjoyed the time spent doing it. However, this was not the case. I&amp;rsquo;m not only angry at myself after the fact, but I also usually feel miserable while mindlessly scrolling. It’s just that sometimes I lack the discipline to avoid it altogether.&lt;/p&gt;
&lt;p&gt;Ideally, I’d completely delete it from my phone, but I still need it for texting. It’s borderline impossible to convince some of my friends to switch to a different platform. Also, some of Instagram&amp;rsquo;s messaging features are just nice to have.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I’ve noticed that I’ve been wasting a lot of time watching Instagram Reels lately. This would not be a problem if I actually enjoyed the time spent doing it. However, this was not the case. I&rsquo;m not only angry at myself after the fact, but I also usually feel miserable while mindlessly scrolling. It’s just that sometimes I lack the discipline to avoid it altogether.</p>
<p>Ideally, I’d completely delete it from my phone, but I still need it for texting. It’s borderline impossible to convince some of my friends to switch to a different platform. Also, some of Instagram&rsquo;s messaging features are just nice to have.</p>
<p>Learning from experience, the second-best option in this type of situation is to add friction to the process. Having Reels one click away each time I open the app is way too tempting. Since I’m only ever using it to text people, it would be nice to have it open directly on the Messages screen.</p>
<p>How do I get a complete list of deep links the Instagram iOS app supports?</p>
<p>My initial plan was to jailbreak an iPhone, export the Instagram app, and then use a disassembler like Hopper to inspect the binary. On paper, the binary should contain all the supported deep links. It was only a matter of finding them. And learning how to jailbreak devices. And learning how to work with a disassembler. Seems like a lot of work for a simple shortcut, doesn’t it?</p>
<p>“Why don’t you use <code>/.well-known/apple-app-site-association</code>?” a friend of mine suggested after I shared what I was working on. Even though I was well aware of it, this didn’t come to my mind when looking for solutions.</p>
<blockquote>
<p>The <code>/.well-known/apple-app-site-association</code> is a file used in iOS app development to configure Universal Links. Universal Links allow iOS apps to link directly to content within the app using standard HTTPS URLs, providing a seamless user experience by opening the app directly instead of a web page when the app is installed.</p></blockquote>
<p>This was the perfect tool for the job. After inspecting the file, finding the right deep link was trivial. Long story short, here’s the final shortcut:</p>
<p><a href="https://www.icloud.com/shortcuts/1b15c8091e0744878c3535c1aae01457" target="_blank" >Instagram DMs | iCloud Shortcuts</a></p>
<p>Lesson learned: When something seems like over-engineering, it probably is.</p>
<p>I hope this helps some of you to reduce the number of times you start mindlessly scrolling because you couldn’t resist watching just a few reels before checking your messages!</p>
<h2 id="resources">Resources</h2>
<ul>
<li><a href="https://www.instagram.com/.well-known/apple-app-site-association" target="_blank" >https://www.instagram.com/.well-known/apple-app-site-association</a></li>
<li><a href="https://developer.apple.com/documentation/xcode/supporting-associated-domains" target="_blank" >Supporting associated domains | Apple Developer Documentation</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Working with Local Swift Packages</title>
      <link>https://kunat.dev/notes/swift-local-packages/</link>
      <pubDate>Tue, 27 Feb 2024 21:49:35 +0100</pubDate>
      <guid>https://kunat.dev/notes/swift-local-packages/</guid>
      <description>&lt;p&gt;Imagine you&amp;rsquo;re working on Project A, which relies on Package B, a remote Swift package. To incorporate modifications in B and assess their impact on A, the standard approach requires committing and pushing the changes in B, updating B&amp;rsquo;s version in A, and then rebuilding A to observe the outcomes. This method is slow and cumbersome. There must be a better way!&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Project A
  |
  └──&amp;gt; Swift Package B
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;To speed up and improve the workflow, you can convert the remote dependency B to a local one. This method works whether Project A is a Swift package or an Xcode project (xcodeproj or xcworkspace).&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Imagine you&rsquo;re working on Project A, which relies on Package B, a remote Swift package. To incorporate modifications in B and assess their impact on A, the standard approach requires committing and pushing the changes in B, updating B&rsquo;s version in A, and then rebuilding A to observe the outcomes. This method is slow and cumbersome. There must be a better way!</p>
<pre tabindex="0"><code>Project A
  |
  └──&gt; Swift Package B
</code></pre><p>To speed up and improve the workflow, you can convert the remote dependency B to a local one. This method works whether Project A is a Swift package or an Xcode project (xcodeproj or xcworkspace).</p>
<h2 id="xcode-project">Xcode Project</h2>
<p>In this case, Project A has an Xcode project file.</p>
<ol>
<li>Remove the remote dependency from your project.</li>
<li>Re-add it as a local package.</li>
</ol>
<p><img alt="hello there" loading="lazy" src="/notes/images/local_packages_1.png"></p>
<blockquote>
<p>This might seem excessive, as Xcode also supports just dragging and dropping the package into your project, but I’ve found this solution to be flaky. Explicitly removing it works every time, most of the time.</p></blockquote>
<h2 id="swift-package">Swift Package</h2>
<p>If Project A is a Swift package, you&rsquo;ll need to convert B&rsquo;s dependency declaration:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-swift" data-lang="swift"><span style="display:flex;"><span>dependencies: [
</span></span><span style="display:flex;"><span>    .package(url: <span style="color:#e6db74">&#34;https://github.com/TinyCorp/B&#34;</span>, exact: <span style="color:#e6db74">&#34;0.52.3&#34;</span>),
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// ...</span>
</span></span></code></pre></div><p>&hellip; into this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-swift" data-lang="swift"><span style="display:flex;"><span>dependencies: [
</span></span><span style="display:flex;"><span>    .package(path: <span style="color:#e6db74">&#34;path/to/B&#34;</span>),
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// ...</span>
</span></span></code></pre></div><p>Example: given the following file structure, <code>path</code> is <code>../B</code>:</p>
<pre tabindex="0"><code>.
├── A
│   └── Package.swift (we&#39;re here)
└── B
    └── Package.swift
</code></pre><hr>
<p>With this setup, you’re now able to edit dependency B directly inside of A. I was genuinely surprised by how well Xcode handles this workflow.</p>
<h2 id="notes">Notes</h2>
<blockquote>
<p>Make sure the Swift package that you plan to add to your project is not opened in another Xcode window. In some cases, Xcode might fail with a cryptic error when trying to add the local package.</p></blockquote>
]]></content:encoded>
    </item>
    <item>
      <title>Automating Adding SPM Plugins</title>
      <link>https://kunat.dev/notes/spm-plugin-update/</link>
      <pubDate>Mon, 18 Dec 2023 18:43:35 +0100</pubDate>
      <guid>https://kunat.dev/notes/spm-plugin-update/</guid>
      <description>&lt;p&gt;&lt;em&gt;How to automate adding plugins to all of your existing (and future) targets in a Swift package&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;TL;DR&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-swift&#34; data-lang=&#34;swift&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;package.targets = package.targets.map { target &lt;span style=&#34;color:#66d9ef&#34;&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; plugins = target.plugins ?? []
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    plugins.append(.plugin(name: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;SwiftLintPlugin&amp;#34;&lt;/span&gt;, package: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;SwiftLint&amp;#34;&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    target.plugins = plugins
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; target
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;the-problem&#34;&gt;The Problem&lt;/h2&gt;
&lt;p&gt;Last month, I was tasked with adding SwiftFormat and SwiftLint to one of the projects my team maintains. The project is medium-sized and uses Swift Package Manager (SPM) for modularization. It has a separate SPM package for each module. For example:&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><em>How to automate adding plugins to all of your existing (and future) targets in a Swift package</em></p>
<p>TL;DR</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-swift" data-lang="swift"><span style="display:flex;"><span>package.targets = package.targets.map { target <span style="color:#66d9ef">in</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> plugins = target.plugins ?? []
</span></span><span style="display:flex;"><span>    plugins.append(.plugin(name: <span style="color:#e6db74">&#34;SwiftLintPlugin&#34;</span>, package: <span style="color:#e6db74">&#34;SwiftLint&#34;</span>))
</span></span><span style="display:flex;"><span>    target.plugins = plugins
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> target
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="the-problem">The Problem</h2>
<p>Last month, I was tasked with adding SwiftFormat and SwiftLint to one of the projects my team maintains. The project is medium-sized and uses Swift Package Manager (SPM) for modularization. It has a separate SPM package for each module. For example:</p>
<ul>
<li>FeatureModules // Directory
<ul>
<li>Login // Swift Package</li>
<li>MyAccount // Swift Package</li>
<li>Dashboard // Swift Package</li>
</ul>
</li>
<li>Core // Directory
<ul>
<li>DomainModels // Swift Package</li>
<li>Networking // Swift Package</li>
</ul>
</li>
</ul>
<p>Each module typically consists of three targets (e.g., Login, LoginTests, LoginTestUtils). The third target&rsquo;s sole purpose is to avoid duplicating testing doubles. You can read more about it <a href="/notes/duplicate-testing-doubles/" >here</a>.</p>
<p>Adding SwiftFormat to the main project and all SPM packages was pretty straightforward. I decided to integrate it at the CI level to ensure all new code changes are properly formatted. Locally, I left this choice up to the maintainers. They can use it through a post-commit git hook or as an Xcode Extension. Depending on your needs, both solutions work great.</p>
<p>Integrating SwiftLint into the base project was also a breeze. What became problematic was integrating it into all the Swift packages. As I mentioned before, we were using a Swift package for each module. As per the documentation:</p>
<blockquote>
<p>Due to limitations with Swift Package Manager plugins, this is only recommended for projects that have a SwiftLint configuration in their root directory, as there is currently no way to pass any additional options to the SwiftLint executable.</p></blockquote>
<p>This limitation would require us to duplicate our SwiftLint config for each package, making future updates to the SwiftLint config error-prone and time-consuming. The same goes for updating the plugin itself.</p>
<h2 id="looking-for-a-solution">Looking For a Solution</h2>
<p>I had two problems to solve:</p>
<ol>
<li>Automating the updating of the config file(s).</li>
<li>Automating the addition of the SwiftLint plugin to all new Swift packages in the future.</li>
</ol>
<p>The first problem is easily solvable with a simple bash script. It would still require writing documentation, but let’s say I’m okay with the trade-off.</p>
<p>The second problem got me looking at Sourcery. However, this solution seemed like overkill from the beginning. Introducing Sourcery to a project just because of SPM limitations seemed like a bad idea.</p>
<p>Back to square one!</p>
<h2 id="solution">Solution</h2>
<p>I started experimenting with the project structure. Instead of creating a new Swift package for each module, I decided to create a single package that worked as a container for all existing modules. Having all the modules defined in a single <code>Package.swift</code> file makes automating the SwiftLint plugin trivial.</p>
<p><img alt="hello there" loading="lazy" src="/notes/images/spm-plugin-update-update-project-structure.png"></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-swift" data-lang="swift"><span style="display:flex;"><span><span style="color:#75715e">// Inject base plugins into each target</span>
</span></span><span style="display:flex;"><span>package.targets = package.targets.map { target <span style="color:#66d9ef">in</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> plugins = target.plugins ?? []
</span></span><span style="display:flex;"><span>    plugins.append(.plugin(name: <span style="color:#e6db74">&#34;SwiftLintPlugin&#34;</span>, package: <span style="color:#e6db74">&#34;SwiftLint&#34;</span>))
</span></span><span style="display:flex;"><span>    target.plugins = plugins
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> target
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This approach not only solved all of my problems but also required very little work. It involved merging all package definitions into a single one and moving some files around.</p>
<p>This solution won’t work for larger projects. Since my app had only a handful of modules, it worked wonderfully. If you’re working on a mid-sized project with tens of packages, you might consider grouping your modules into a handful of packages like FeatureModules, Models, etc.</p>
<p>I hope this issue will have been resolved long before we hit the wall due to the growing number of modules!</p>
<h2 id="resources">Resources</h2>
<ul>
<li><a href="https://www.swiftystack.com/" target="_blank" >Swifty Stack</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>GitLab Code Coverage for Pure Swift Packages</title>
      <link>https://kunat.dev/notes/gitlab-code-coverage/</link>
      <pubDate>Fri, 28 Jul 2023 21:53:46 +0200</pubDate>
      <guid>https://kunat.dev/notes/gitlab-code-coverage/</guid>
      <description>&lt;p&gt;&lt;em&gt;How to use GitLab&amp;rsquo;s built-in test coverage report tool for pure Swift packages?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Some test coverage report generation tools do not support “pure” Swift packages. One example is &lt;a href=&#34;https://github.com/SlatherOrg/slather/issues/466&#34; target=&#34;_blank&#34; &gt;Slather&lt;/a&gt;. By pure, I’m referring to packages that contain only the &lt;code&gt;Package.swift&lt;/code&gt; file, without any &lt;code&gt;.xcodeproj&lt;/code&gt; or &lt;code&gt;.xcworkspace&lt;/code&gt; files.&lt;/p&gt;
&lt;p&gt;One solution to this problem was to generate a project file for a Swift package on the fly using Fastlane. This solution has one limitation: it&amp;rsquo;s not possible to generate a project file for a Swift package that contains any resources. When you try doing so, you’ll see &lt;code&gt;Type &#39;Bundle&#39; has no member “module”&lt;/code&gt; error when building.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><em>How to use GitLab&rsquo;s built-in test coverage report tool for pure Swift packages?</em></p>
<p>Some test coverage report generation tools do not support “pure” Swift packages. One example is <a href="https://github.com/SlatherOrg/slather/issues/466" target="_blank" >Slather</a>. By pure, I’m referring to packages that contain only the <code>Package.swift</code> file, without any <code>.xcodeproj</code> or <code>.xcworkspace</code> files.</p>
<p>One solution to this problem was to generate a project file for a Swift package on the fly using Fastlane. This solution has one limitation: it&rsquo;s not possible to generate a project file for a Swift package that contains any resources. When you try doing so, you’ll see <code>Type 'Bundle' has no member “module”</code> error when building.</p>
<h2 id="solution">Solution</h2>
<p>What worked was using Fastlane with <code>xcov</code>. Here’s a snippet:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ruby" data-lang="ruby"><span style="display:flex;"><span>scan(
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">package_path</span>: <span style="color:#e6db74">&#34;.&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#75715e"># Schemes generated by Xcode have a &#34;-Package&#34; suffix</span>
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">scheme</span>: <span style="color:#e6db74">&#34;MyPackage-Package&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">clean</span>: <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">device</span>: <span style="color:#e6db74">&#34;iPhone 14&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">code_coverage</span>: <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">result_bundle</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>xcov(
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">is_swift_package</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p>Here’s how the <code>xcov</code> output looks like:</p>
<pre tabindex="0"><code>+----------------------+--------+
|[32mxcov Coverage Report[0m    |
+----------------------+--------+
| MyPackage            | 13.75% |
| MyPackageTestUtils   | 0.00%  |
| MyPackageTests       | 22.12% |
| Average Coverage     | 11.96% |
+----------------------+--------+
</code></pre><p>The final piece of the puzzle is displaying the test coverage on the MR page like so:</p>
<p><img alt="hello there" loading="lazy" src="/notes/images/gitlab-code-coverage-pipeline-passed.png"></p>
<p>In order to do that, we’ll need a regex to parse output from <code>xcov</code>. In this case we’re only interested in getting the percentage right beside “MyPackgae”. Here’s a snippet from the <code>.gitlab-ci.yml</code> file:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yml" data-lang="yml"><span style="display:flex;"><span><span style="color:#f92672">unit_test</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">stage</span>: <span style="color:#ae81ff">tests</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">script</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">bundle exec fastlane unit_test</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">coverage</span>: <span style="color:#e6db74">&#39;/\|\s+MyPackage\s+\|\s+\d+\.\d+\%\s+\|/&#39;</span>
</span></span></code></pre></div><h2 id="resources">Resources</h2>
<ul>
<li><a href="https://github.com/fastlane/fastlane/discussions/17362" target="_blank" >Support for SPM package projects in fastlane scan</a></li>
<li><a href="https://augmentedcode.io/2021/04/12/code-coverage-for-swift-packages-with-fastlane/" target="_blank" >Code coverage for Swift Packages with Fastlane</a></li>
<li><a href="https://github.com/fastlane/fastlane/discussions/17362" target="_blank" >Support for SPM package projects in fastlane scan</a></li>
<li><a href="https://docs.gitlab.com/ee/ci/testing/code_coverage.html" target="_blank" >Code coverage | GitLab Docs</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Xcode Development Assets with XcodeGen</title>
      <link>https://kunat.dev/notes/development-assets-xcodegen/</link>
      <pubDate>Mon, 10 Apr 2023 17:05:57 +0200</pubDate>
      <guid>https://kunat.dev/notes/development-assets-xcodegen/</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Development Assets in Xcode that allow developers to provide test data to use within SwiftUI previews and other code during development. Assets marked for development will only be included in debug builds and removed once you create an archive of your app. This solution is preferred to having larger assets in the production binary size. Developers can add different types of data as Development Assets, such as JSON files for mocked network requests, images to use in SwiftUI previews, Core Data sample databases, and &lt;strong&gt;Swift files representing mocked or sample data&lt;/strong&gt;. Swift files are added differently because they use the dead code stripper to avoid the compiler from failing during archiving.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<blockquote>
<p>Development Assets in Xcode that allow developers to provide test data to use within SwiftUI previews and other code during development. Assets marked for development will only be included in debug builds and removed once you create an archive of your app. This solution is preferred to having larger assets in the production binary size. Developers can add different types of data as Development Assets, such as JSON files for mocked network requests, images to use in SwiftUI previews, Core Data sample databases, and <strong>Swift files representing mocked or sample data</strong>. Swift files are added differently because they use the dead code stripper to avoid the compiler from failing during archiving.</p></blockquote>
<p>To define development assets through the XcodeGen config file, use <code>DEVELOPMENT_ASSET_PATHS</code>:</p>
<pre tabindex="0"><code>name: MySampleApp
options:
  bundleId: com.example.MySampleApp
  deploymentTarget: 14.0
  organizationName: Example, Inc.
  developmentLanguage: swift
  useTabs: true
  indentWidth: 4
  tabWidth: 4
  swiftVersion: 5.5
  carthageBuildPath: Carthage/Build/iOS

targets:
  - name: MySampleApp
    type: application
    platform: iOS
    deploymentTarget: 14.0
    sources: 
      - path: MySampleApp
    settings:
      ENABLE_BITCODE: NO
      SWIFT_OPTIMIZATION_LEVEL: -Onone
      SWIFT_ACTIVE_COMPILATION_CONDITIONS: DEBUG
      DEVELOPMENT_ASSET_PATHS: 
         Dashboard/PreviewAssets
         List/Views/PreviewAssets
// ...
</code></pre><h2 id="resources">Resources</h2>
<ul>
<li><a href="https://www.avanderlee.com/xcode/development-assets-preview-catalog/" target="_blank" >Development Assets in Xcode to enrich SwiftUI Previews</a></li>
<li><a href="https://useyourloaf.com/blog/swiftui-preview-data/" target="_blank" >SwiftUI Preview Data</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>How to Tackle Duplicate Test Doubles Problem in Modular Architecture</title>
      <link>https://kunat.dev/notes/duplicate-testing-doubles/</link>
      <pubDate>Thu, 30 Mar 2023 22:09:54 +0200</pubDate>
      <guid>https://kunat.dev/notes/duplicate-testing-doubles/</guid>
      <description>&lt;p&gt;After SPM asset support has been added to Xcode 12, there is no reason not to use modularization in your iOS projects. It brings value no matter the project and team size.&lt;/p&gt;
&lt;p&gt;However, after some time, you might encounter problems like increased complexity, difficulty with integration testing, or code duplication. Today we’ll focus on the last one, specifically, testing doubles duplication. Let’s jump in!&lt;/p&gt;
&lt;h2 id=&#34;introduction&#34;&gt;Introduction&lt;/h2&gt;
&lt;p&gt;When saying “module” I’m always referring to a group of related targets. One example would be &lt;code&gt;DomainModels&lt;/code&gt; and &lt;code&gt;DomainModelsTests&lt;/code&gt;. The &lt;code&gt;DomainModels&lt;/code&gt; target stores the implementation, while &lt;code&gt;DomainModelsTests&lt;/code&gt; has unit tests code.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>After SPM asset support has been added to Xcode 12, there is no reason not to use modularization in your iOS projects. It brings value no matter the project and team size.</p>
<p>However, after some time, you might encounter problems like increased complexity, difficulty with integration testing, or code duplication. Today we’ll focus on the last one, specifically, testing doubles duplication. Let’s jump in!</p>
<h2 id="introduction">Introduction</h2>
<p>When saying “module” I’m always referring to a group of related targets. One example would be <code>DomainModels</code> and <code>DomainModelsTests</code>. The <code>DomainModels</code> target stores the implementation, while <code>DomainModelsTests</code> has unit tests code.</p>
<p>In the sample project for this article, we have the <code>DomainModels</code> package with a single module: <code>DomainModels</code>. Apart from that, we have the <code>FeatureModules</code> package with multiple modules. High-level structure of this setup looks like this:</p>
<pre tabindex="0"><code>├── DomainModels
│   ├── DomainModels
│   └── DomainModelsTests
└── FeatureModules
    ├── Dashboard
    ├── DashboardTests
    ├── AccountOverview
    └── AccountOverviewTests
</code></pre><p>All modules in the <code>FeatureModules</code> package depend on the <code>DomainModels</code> module. Because of that, both <code>DashboardTests</code> and <code>AccountOverviewTests</code> will likely have some duplicate testing doubles. This is not ideal.</p>
<p>There are many possible solutions to that problem. The one that I like the most is creating a separate target in the module just for storing all of its testing doubles. Let’s implement it!</p>
<h2 id="solution">Solution</h2>
<p>The solution is fairly simple. We’ll just add the third target to the <code>DomainModels</code> module: <code>DomainModelsFakes</code> and move all the testing doubles from feature modules there.</p>
<blockquote>
<p>Although the <code>TestingDoubles</code> postfix may be more appropriate for these modules, I prefer the term <code>Fakes</code> as it conveys the same meaning and is shorter. This choice becomes more practical when considering that there will likely be a significant number of these modules.</p></blockquote>
<p>With this new addition, the <code>DomainModels</code> module consists of three targets:</p>
<ul>
<li><code>DomainModels</code></li>
<li><code>DomainModelsTests</code></li>
<li><code>DomainModelsFakes</code></li>
</ul>
<p>Here’s the package definition:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-swift" data-lang="swift"><span style="display:flex;"><span><span style="color:#75715e">// swift-tools-version: 5.7</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// The swift-tools-version declares the minimum version of Swift required to build this package.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">PackageDescription</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">let</span> package = Package(
</span></span><span style="display:flex;"><span>    name: <span style="color:#e6db74">&#34;DomainModels&#34;</span>,
</span></span><span style="display:flex;"><span>    platforms: [.iOS(.v16), .macOS(.v13)],
</span></span><span style="display:flex;"><span>    products: [
</span></span><span style="display:flex;"><span>        .library(
</span></span><span style="display:flex;"><span>            name: <span style="color:#e6db74">&#34;DomainModels&#34;</span>,
</span></span><span style="display:flex;"><span>            targets: [<span style="color:#e6db74">&#34;DomainModels&#34;</span>, <span style="color:#e6db74">&#34;DomainModelsFakes&#34;</span>]),
</span></span><span style="display:flex;"><span>    ],
</span></span><span style="display:flex;"><span>    dependencies: [],
</span></span><span style="display:flex;"><span>    targets: [
</span></span><span style="display:flex;"><span>        .target(
</span></span><span style="display:flex;"><span>            name: <span style="color:#e6db74">&#34;DomainModels&#34;</span>,
</span></span><span style="display:flex;"><span>            dependencies: []),
</span></span><span style="display:flex;"><span>        .target(
</span></span><span style="display:flex;"><span>            name: <span style="color:#e6db74">&#34;DomainModelsFakes&#34;</span>,
</span></span><span style="display:flex;"><span>            dependencies: [<span style="color:#e6db74">&#34;DomainModels&#34;</span>]),
</span></span><span style="display:flex;"><span>        .testTarget(
</span></span><span style="display:flex;"><span>            name: <span style="color:#e6db74">&#34;DomainModelsTests&#34;</span>,
</span></span><span style="display:flex;"><span>            dependencies: [<span style="color:#e6db74">&#34;DomainModels&#34;</span>, <span style="color:#e6db74">&#34;DomainModelsFakes&#34;</span>]),
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p>The new target <code>DomainModelsFakes</code> depends on the <code>DomainModels</code> target. We also need to add it as a dependency to the test target to use the testing doubles.</p>
<p>With those changes in place, we can now add <code>DomainModelsFakes</code> as a dependency to feature modules testing targets:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-swift" data-lang="swift"><span style="display:flex;"><span><span style="color:#75715e">// swift-tools-version: 5.7</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// The swift-tools-version declares the minimum version of Swift required to build this package.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">PackageDescription</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">let</span> package = Package(
</span></span><span style="display:flex;"><span>    name: <span style="color:#e6db74">&#34;FeatureModules&#34;</span>,
</span></span><span style="display:flex;"><span>    platforms: [.iOS(.v16), .macOS(.v13)],
</span></span><span style="display:flex;"><span>    products: [
</span></span><span style="display:flex;"><span>        .library(
</span></span><span style="display:flex;"><span>            name: <span style="color:#e6db74">&#34;FeatureModules&#34;</span>,
</span></span><span style="display:flex;"><span>            targets: [<span style="color:#e6db74">&#34;Dashboard&#34;</span>, <span style="color:#e6db74">&#34;AccountOverview&#34;</span>]),
</span></span><span style="display:flex;"><span>    ],
</span></span><span style="display:flex;"><span>    dependencies: [
</span></span><span style="display:flex;"><span>        .package(path: <span style="color:#e6db74">&#34;../DomainModels&#34;</span>),
</span></span><span style="display:flex;"><span>    ],
</span></span><span style="display:flex;"><span>    targets: [
</span></span><span style="display:flex;"><span>        .target(
</span></span><span style="display:flex;"><span>            name: <span style="color:#e6db74">&#34;Dashboard&#34;</span>,
</span></span><span style="display:flex;"><span>            dependencies: [
</span></span><span style="display:flex;"><span>                .product(name: <span style="color:#e6db74">&#34;DomainModels&#34;</span>, package: <span style="color:#e6db74">&#34;DomainModels&#34;</span>),
</span></span><span style="display:flex;"><span>            ]),
</span></span><span style="display:flex;"><span>        .testTarget(
</span></span><span style="display:flex;"><span>            name: <span style="color:#e6db74">&#34;DashboardTests&#34;</span>,
</span></span><span style="display:flex;"><span>            dependencies: [
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;Dashboard&#34;</span>,
</span></span><span style="display:flex;"><span>                .product(name: <span style="color:#e6db74">&#34;DomainModelsFakes&#34;</span>, package: <span style="color:#e6db74">&#34;DomainModels&#34;</span>),
</span></span><span style="display:flex;"><span>          ]),
</span></span><span style="display:flex;"><span>        .target(
</span></span><span style="display:flex;"><span>            name: <span style="color:#e6db74">&#34;AccountOverview&#34;</span>,
</span></span><span style="display:flex;"><span>            dependencies: [
</span></span><span style="display:flex;"><span>                .product(name: <span style="color:#e6db74">&#34;DomainModels&#34;</span>, package: <span style="color:#e6db74">&#34;DomainModels&#34;</span>),
</span></span><span style="display:flex;"><span>            ]),
</span></span><span style="display:flex;"><span>        .testTarget(
</span></span><span style="display:flex;"><span>            name: <span style="color:#e6db74">&#34;AccountOverviewTests&#34;</span>,
</span></span><span style="display:flex;"><span>            dependencies: [
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;AccountOverview&#34;</span>,
</span></span><span style="display:flex;"><span>                .product(name: <span style="color:#e6db74">&#34;DomainModelsFakes&#34;</span>, package: <span style="color:#e6db74">&#34;DomainModels&#34;</span>),
</span></span><span style="display:flex;"><span>          ]),
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><h2 id="conclusion">Conclusion</h2>
<p>As our project grows, we may encounter problems such as code duplication, which can lead to increased complexity and difficulty with integration testing. In this article, we focused on the issue of testing doubles duplication and provided a solution to address it by creating a separate target in the module to store all testing doubles. By implementing this solution, we can reduce code duplication and improve the maintainability of our codebase. Do let me know if there are any other solutions worth trying out!</p>
<h2 id="resources">Resources</h2>
<ul>
<li><a href="https://www.youtube.com/watch?v=SXN1eWO5YtI" target="_blank" >“Fewer, Smarter, Faster: Scaling Testing @Spotify” by Vivian Santos &amp; Sami Bouchebaba</a> This talk is the where I’ve found this idea. See the video around 12:00 minute mark.</li>
<li><a href="https://martinfowler.com/bliki/TestDouble.html" target="_blank" >TestDouble</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>How to Connect Internal Dependencies in Swift Package Manager</title>
      <link>https://kunat.dev/notes/spm-internal-dependencies/</link>
      <pubDate>Thu, 16 Mar 2023 14:30:23 +0100</pubDate>
      <guid>https://kunat.dev/notes/spm-internal-dependencies/</guid>
      <description>&lt;p&gt;Swift Package Manager (SPM) is a powerful tool for modularizing your code and managing dependencies in your projects. If you’re working on a project that uses SPM for modularization, you may need to add internal dependencies to connect different parts of your codebase. In this article, we’ll go over how to add internal dependencies in SPM and configure your &lt;code&gt;Package.swift&lt;/code&gt; file to properly connect them.&lt;/p&gt;
&lt;h2 id=&#34;assumptions&#34;&gt;Assumptions&lt;/h2&gt;
&lt;p&gt;Before we get started, let’s make a few assumptions about your project and its structure:&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Swift Package Manager (SPM) is a powerful tool for modularizing your code and managing dependencies in your projects. If you’re working on a project that uses SPM for modularization, you may need to add internal dependencies to connect different parts of your codebase. In this article, we’ll go over how to add internal dependencies in SPM and configure your <code>Package.swift</code> file to properly connect them.</p>
<h2 id="assumptions">Assumptions</h2>
<p>Before we get started, let’s make a few assumptions about your project and its structure:</p>
<ul>
<li>You’re working on a project where modularization is done using SPM.</li>
<li>You have an SPM package that depends on other packages.</li>
</ul>
<p>Here’s how my project structure looks like:</p>
<p><img alt="Project structure" loading="lazy" src="/notes/images/timewise-project-structure.png"></p>
<h2 id="connecting-internal-dependencies">Connecting Internal Dependencies</h2>
<p>To connect the internal dependencies in your Package.swift file, you need to specify the dependencies for your package and the dependencies for your targets. Here&rsquo;s an example <code>Package.swift</code> file that configures the <code>FeatureModules</code> package with its internal dependencies:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-swift" data-lang="swift"><span style="display:flex;"><span><span style="color:#75715e">// swift-tools-version: 5.7</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// The swift-tools-version declares the minimum version of Swift required to build this package.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">PackageDescription</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">let</span> package = Package(
</span></span><span style="display:flex;"><span>    name: <span style="color:#e6db74">&#34;FeatureModules&#34;</span>,
</span></span><span style="display:flex;"><span>    platforms: [.iOS(.v16), .macOS(.v13)],
</span></span><span style="display:flex;"><span>    products: [
</span></span><span style="display:flex;"><span>        .library(
</span></span><span style="display:flex;"><span>            name: <span style="color:#e6db74">&#34;FeatureModules&#34;</span>,
</span></span><span style="display:flex;"><span>            targets: [<span style="color:#e6db74">&#34;Dashboard&#34;</span>]),
</span></span><span style="display:flex;"><span>    ],
</span></span><span style="display:flex;"><span>    dependencies: [
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Dependencies declare other packages that this package depends on.</span>
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// .package(url: /* package url */, from: &#34;1.0.0&#34;),</span>
</span></span><span style="display:flex;"><span>        .package(path: <span style="color:#e6db74">&#34;../DomainModels&#34;</span>),
</span></span><span style="display:flex;"><span>        .package(path: <span style="color:#e6db74">&#34;../Resources&#34;</span>),
</span></span><span style="display:flex;"><span>        .package(path: <span style="color:#e6db74">&#34;../Shared&#34;</span>),
</span></span><span style="display:flex;"><span>        .package(path: <span style="color:#e6db74">&#34;../Storage&#34;</span>),
</span></span><span style="display:flex;"><span>    ],
</span></span><span style="display:flex;"><span>    targets: [
</span></span><span style="display:flex;"><span>        .target(
</span></span><span style="display:flex;"><span>            name: <span style="color:#e6db74">&#34;Dashboard&#34;</span>,
</span></span><span style="display:flex;"><span>            dependencies: [
</span></span><span style="display:flex;"><span>                .product(name: <span style="color:#e6db74">&#34;DomainModels&#34;</span>, package: <span style="color:#e6db74">&#34;DomainModels&#34;</span>),
</span></span><span style="display:flex;"><span>                .product(name: <span style="color:#e6db74">&#34;Resources&#34;</span>, package: <span style="color:#e6db74">&#34;Resources&#34;</span>),
</span></span><span style="display:flex;"><span>                .product(name: <span style="color:#e6db74">&#34;Shared&#34;</span>, package: <span style="color:#e6db74">&#34;Shared&#34;</span>),
</span></span><span style="display:flex;"><span>            ]),
</span></span><span style="display:flex;"><span>        .testTarget(
</span></span><span style="display:flex;"><span>            name: <span style="color:#e6db74">&#34;DashboardTests&#34;</span>,
</span></span><span style="display:flex;"><span>            dependencies: [
</span></span><span style="display:flex;"><span>                <span style="color:#e6db74">&#34;Dashboard&#34;</span>,
</span></span><span style="display:flex;"><span>                .product(name: <span style="color:#e6db74">&#34;Storage&#34;</span>, package: <span style="color:#e6db74">&#34;Storage&#34;</span>),
</span></span><span style="display:flex;"><span>          ]),
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><h2 id="conclusion">Conclusion</h2>
<p>Connecting internal dependencies in Swift Package Manager is a crucial step when working on modularized projects. By properly configuring the <code>Package.swift</code> file and specifying the dependencies, you can ensure that your code compiles and runs without any issues. Remember to use the .product declaration when specifying dependencies in your target, and to make sure that the package paths are correctly set in the <code>Package.swift</code> file. By following these steps, you can maintain a well-organized project structure and increase code reusability across your codebase.</p>
<h2 id="resources">Resources</h2>
<ul>
<li><a href="https://www.pointfree.co/episodes/ep171-modularization-part-1" target="_blank" >Episode #171: Modularization: Part 1 | Point-Free</a></li>
<li><a href="https://www.pointfree.co/episodes/ep172-modularization-part-2" target="_blank" >Episode #172: Modularization: Part 2 | Point-Free</a></li>
<li><a href="https://betterprogramming.pub/modularize-an-ios-app-with-spm-c3f51f03bb0b" target="_blank" >Modularize an iOS App With SPM | Better Programming</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Handling Edit Menus Without the First Responder</title>
      <link>https://kunat.dev/notes/edit-menu-no-first-responder/</link>
      <pubDate>Tue, 31 Jan 2023 21:12:56 +0100</pubDate>
      <guid>https://kunat.dev/notes/edit-menu-no-first-responder/</guid>
      <description>&lt;h2 id=&#34;context&#34;&gt;Context&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m currently developing a chat-based app that requires the implementation of coping messages. Essentially, when a user long-presses a message bubble, an edit menu will appear with the option to &amp;ldquo;Copy.&amp;rdquo; This feature is a common element across most chat applications.&lt;/p&gt;
&lt;p&gt;For the purpose of this article, let&amp;rsquo;s assume that all message bubbles are constructed using a subclass of UITextView called MessageTextView. To make the &amp;ldquo;Copy&amp;rdquo; option appear on a long-press gesture, we&amp;rsquo;ll first need to add the appropriate gesture recognizer:&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h2 id="context">Context</h2>
<p>I&rsquo;m currently developing a chat-based app that requires the implementation of coping messages. Essentially, when a user long-presses a message bubble, an edit menu will appear with the option to &ldquo;Copy.&rdquo; This feature is a common element across most chat applications.</p>
<p>For the purpose of this article, let&rsquo;s assume that all message bubbles are constructed using a subclass of UITextView called MessageTextView. To make the &ldquo;Copy&rdquo; option appear on a long-press gesture, we&rsquo;ll first need to add the appropriate gesture recognizer:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-swift" data-lang="swift"><span style="display:flex;"><span><span style="color:#66d9ef">let</span> textView = MessageTextView()
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">let</span> longPressGesture = UILongPressGestureRecognizer(
</span></span><span style="display:flex;"><span>    target: <span style="color:#66d9ef">self</span>, 
</span></span><span style="display:flex;"><span>    action: <span style="color:#66d9ef">#selector</span>(handleLongPressGesture(recognizer:)))
</span></span><span style="display:flex;"><span>textView.addGestureRecognizer(longPressGesture)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">@objc</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">handleLongPressGesture</span>(recognizer: UIGestureRecognizer) {}
</span></span></code></pre></div><p>Next, we&rsquo;ll move on to the second step, which involves implementing the handleLongPressGesture method:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-swift" data-lang="swift"><span style="display:flex;"><span><span style="color:#75715e">// 1</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">guard</span> recognizer.state == .began,
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">let</span> recognizerView = recognizer.view,
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">let</span> superview = recognizerView.superview <span style="color:#66d9ef">else</span> { <span style="color:#66d9ef">return</span> }
</span></span><span style="display:flex;"><span><span style="color:#75715e">// 2</span>
</span></span><span style="display:flex;"><span>textView?.becomeFirstResponder()
</span></span><span style="display:flex;"><span><span style="color:#75715e">// 3</span>
</span></span><span style="display:flex;"><span>UIMenuController.shared.showMenu(from: superview, rect: recognizerView.frame)
</span></span></code></pre></div><p>Let&rsquo;s now break down the steps necessary to implement this feature:</p>
<ol>
<li>First, we need to obtain the recognizerView and its parent view from the recognizer. We&rsquo;ll need both of these to properly position the edit menu on the screen.</li>
<li>Next, we&rsquo;ll make the edit menu the first responder to ensure that it appears on the screen.</li>
<li>Finally, we&rsquo;ll show the menu.</li>
</ol>
<p>Note that at this point, all available options will be displayed, so we still need to filter out any unnecessary edit menu options. To do this, we&rsquo;ll need to override two methods in our MessageTextView:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-swift" data-lang="swift"><span style="display:flex;"><span><span style="color:#66d9ef">override</span> <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">func</span> <span style="color:#a6e22e">canPerformAction</span>(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">_</span> action: Selector, 
</span></span><span style="display:flex;"><span>    withSender sender: Any?) -&gt; Bool {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// 1</span>
</span></span><span style="display:flex;"><span>    action == <span style="color:#66d9ef">#selector</span>(copy(<span style="color:#66d9ef">_</span>:))
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">@objc</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">override</span> <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">func</span> <span style="color:#a6e22e">copy</span>(<span style="color:#66d9ef">_</span> sender: Any?) {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// 2</span>
</span></span><span style="display:flex;"><span>    UIPasteboard.general.string = text
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><ol>
<li>We filter out all other options, except for &ldquo;copy&rdquo;.</li>
<li>As per Apple&rsquo;s <a href="https://developer.apple.com/documentation/uikit/uiresponderstandardeditactions/2354191-copy#" target="_blank" >documentation</a>:</li>
</ol>
<blockquote>
<p>UIKit calls this method when the user selects the Copy command from an editing menu. Your implementation should write the selected content to the pasteboard without removing the selection from your interface.</p></blockquote>
<p>At first glance, it may seem like we&rsquo;ve completed the implementation of this feature. However, there&rsquo;s one more detail that we need to address. Currently, if the keyboard is visible on the screen, the edit menu will be displayed in the wrong location. This occurs because we&rsquo;re calling <code>showMenu(from:rect:)</code> while the keyboard is still visible. Once the keyboard hides, the layout of the conversation view (i.e. the screen where all the messages are displayed) will likely change.</p>
<h2 id="handling-keyboard-events">Handling keyboard events</h2>
<p>One solution to present the edit menu without hiding the keyboard is by using the <code>UIResponder.next</code> property, as described in <a href="https://betterprogramming.pub/uimenucontroller-and-manipulating-the-responder-chain-c06fad73c64b" target="_blank" >this article</a>. However, in my case, this is not feasible due to the modular architecture of the app I&rsquo;m working on. The message composer (the view where the user composes a message) and conversation view (the view where all the message bubbles are displayed) are separate modules, and iOS doesn&rsquo;t provide an easy way to obtain a reference to the current first responder.</p>
<p>Therefore, I found that the best solution for this scenario is to listen for the <code>UIResponder.keyboardDidHideNotification</code> and update the edit menu accordingly. To implement this in <code>MessageTextView</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-swift" data-lang="swift"><span style="display:flex;"><span><span style="color:#75715e">// Text view subclass</span>
</span></span><span style="display:flex;"><span>NotificationCenter.<span style="color:#66d9ef">default</span>.addObserver(
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">self</span>,
</span></span><span style="display:flex;"><span>    selector: <span style="color:#66d9ef">#selector</span>(handleKeyboardDidHideNotification(notification:)),
</span></span><span style="display:flex;"><span>    name: UIResponder.keyboardDidHideNotification,
</span></span><span style="display:flex;"><span>    object: <span style="color:#66d9ef">nil</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">@objc</span>
</span></span><span style="display:flex;"><span>fileprivate <span style="color:#66d9ef">func</span> <span style="color:#a6e22e">handleKeyboardDidHideNotification</span>(notification: Notification) {
</span></span><span style="display:flex;"><span>     UIMenuController.shared.update()
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="conclusion">Conclusion</h2>
<p>That’s it! The edit menu will update its position when keyboard hides.</p>
<h3 id="notes">Notes</h3>
<ul>
<li><code>UIMenuController</code> has been deprecated in iOS 16. See <a href="https://developer.apple.com/documentation/uikit/uieditmenuinteraction" target="_blank" >UIEditMenuInteraction</a>.</li>
<li>Remember to check out <a href="https://betterprogramming.pub/uimenucontroller-and-manipulating-the-responder-chain-c06fad73c64b" target="_blank" >Using the UIMenuController and Manipulating the Responder Chain</a></li>
</ul>
]]></content:encoded>
    </item>
  </channel>
</rss>
