A+ programming moments https://aplus.rs/ Recent content on A+ programming moments Hugo -- gohugo.io en-us Thu, 13 Nov 2025 21:39:00 +0000 How to use custom variable fonts in UIKit https://aplus.rs/2025/using-variable-custom-font-4-italic-condensed-black/ Thu, 13 Nov 2025 21:39:00 +0000 https://aplus.rs/2025/using-variable-custom-font-4-italic-condensed-black/ <p>If you go with the solution 1 to the <a href="https://aplus.rs/2025/custom-width-weight-italic-uifont/">previously discussed issue</a>, I strongly suggest you embed <a href="https://fonts.google.com/knowledge/introducing_type/introducing_variable_fonts">variable fonts</a> inside your app. Taking a look at Google&rsquo;s <a href="https://fonts.google.com/specimen/Roboto">Roboto font family</a>, it has 54 static fonts which takes up ~8MB. There are just two variable font files though, each less than 500kB.</p> <p>These two files contains condensed and standard <a href="https://fonts.google.com/knowledge/glossary/width">widths</a>, 9 different <a href="https://fonts.google.com/knowledge/glossary/weight">weights</a> in both normal and italic <a href="https://fonts.google.com/knowledge/glossary/style">styles</a> thus producing 54 different font variants. That is bloody awesome, if you ask me.</p> <p>But how do you use them in iOS app?</p> <p>First of course, you check the <a href="https://fonts.google.com/specimen/Roboto/license">usage license</a> and only then dive into technical details:</p> <blockquote> <p>You can use them in your products &amp; projects – print or digital, commercial or otherwise.</p></blockquote> <p>Now <a href="https://developer.apple.com/documentation/uikit/adding-a-custom-font-to-your-app">add the two files into your project</a> and get ready for some coding.</p> <h2 id="debug-availability-of-font-features">Debug availability of font features</h2> <p>To figure out what fonts are available to your and how exactly they are called inside CoreText / UIKit, call this simple method anywhere in your app:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">public</span> <span class="kd">extension</span> <span class="nc">UIFont</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">static</span> <span class="kd">func</span> <span class="nf">listAvailableFonts</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="n">family</span> <span class="k">in</span> <span class="n">UIFont</span><span class="p">.</span><span class="n">familyNames</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="bp">print</span><span class="p">(</span><span class="s">&#34;</span><span class="si">\(</span><span class="n">family</span><span class="si">)</span><span class="s">&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="n">name</span> <span class="k">in</span> <span class="n">UIFont</span><span class="p">.</span><span class="n">fontNames</span><span class="p">(</span><span class="n">forFamilyName</span><span class="p">:</span> <span class="n">family</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="bp">print</span><span class="p">(</span><span class="s">&#34;== </span><span class="si">\(</span><span class="n">name</span><span class="si">)</span><span class="s">&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span></code></pre></div><p>You’ll see that family name is <code>Roboto</code> and specific <code>fontName</code> values are:</p> <pre tabindex="0"><code> Roboto // font-family == Roboto-Regular == Roboto-Italic == Roboto-Thin == Roboto-ThinItalic == Roboto-ExtraLight == Roboto-ExtraLightItalic ... </code></pre><p>Now, we need to figure out what OpenType’s <a href="https://learn.microsoft.com/en-us/typography/opentype/spec/dvaraxisreg">axis tags</a> are present for these fonts. <a href="https://learn.microsoft.com/en-us/typography/opentype/spec/">OpenType docs</a> are incredibly complex; it took an entire day just to get my head around all this and I feel like I know just few percents of it all.</p> <p>This is how you can write out axis tag config for all 54 font variants, using <code>familyName: &quot;Roboto&quot;</code> argument:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">public</span> <span class="kd">extension</span> <span class="nc">UIFont</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">static</span> <span class="kd">func</span> <span class="nf">printVariationAxes</span><span class="p">(</span><span class="n">familyName</span> <span class="n">family</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span> <span class="n">size</span><span class="p">:</span> <span class="n">CGFloat</span> <span class="p">=</span> <span class="mi">18</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="n">name</span> <span class="k">in</span> <span class="n">UIFont</span><span class="p">.</span><span class="n">fontNames</span><span class="p">(</span><span class="n">forFamilyName</span><span class="p">:</span> <span class="n">family</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">printVariationAxes</span><span class="p">(</span><span class="n">fontName</span><span class="p">:</span> <span class="n">name</span><span class="p">,</span> <span class="n">size</span><span class="p">:</span> <span class="n">size</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">/// Print all available axes in the given font</span> </span></span><span class="line"><span class="cl"> <span class="kd">static</span> <span class="kd">func</span> <span class="nf">printVariationAxes</span><span class="p">(</span><span class="n">fontName</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span> <span class="n">size</span><span class="p">:</span> <span class="n">CGFloat</span> <span class="p">=</span> <span class="mi">18</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">descriptor</span> <span class="p">=</span> <span class="n">UIFontDescriptor</span><span class="p">(</span><span class="n">name</span><span class="p">:</span> <span class="n">fontName</span><span class="p">,</span> <span class="n">size</span><span class="p">:</span> <span class="n">size</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">font</span> <span class="p">=</span> <span class="n">UIFont</span><span class="p">(</span><span class="n">descriptor</span><span class="p">:</span> <span class="n">descriptor</span><span class="p">,</span> <span class="n">size</span><span class="p">:</span> <span class="n">size</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="kd">let</span> <span class="nv">axes</span> <span class="p">=</span> <span class="n">CTFontCopyVariationAxes</span><span class="p">(</span><span class="n">font</span> <span class="k">as</span> <span class="n">CTFont</span><span class="p">)</span> <span class="k">as</span><span class="p">?</span> <span class="p">[[</span><span class="nb">String</span><span class="p">:</span> <span class="nb">Any</span><span class="p">]]</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="bp">print</span><span class="p">(</span><span class="s">&#34;</span><span class="se">\n</span><span class="s">=== Variation Axes for </span><span class="si">\(</span><span class="n">fontName</span><span class="si">)</span><span class="s"> ===&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="n">axis</span> <span class="k">in</span> <span class="n">axes</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="bp">print</span><span class="p">(</span><span class="s">&#34;Axis: </span><span class="si">\(</span><span class="n">axis</span><span class="si">)</span><span class="s">&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="kd">let</span> <span class="nv">identifier</span> <span class="p">=</span> <span class="n">axis</span><span class="p">[</span><span class="n">kCTFontVariationAxisIdentifierKey</span> <span class="k">as</span> <span class="nb">String</span><span class="p">]</span> <span class="k">as</span><span class="p">?</span> <span class="nb">Int</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="bp">print</span><span class="p">(</span><span class="s">&#34; Identifier (decimal): </span><span class="si">\(</span><span class="n">identifier</span><span class="si">)</span><span class="s">&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="bp">print</span><span class="p">(</span><span class="s">&#34; Identifier (hex): 0x</span><span class="si">\(</span><span class="nb">String</span><span class="si">(</span><span class="n">identifier</span><span class="p">,</span> <span class="n">radix</span><span class="p">:</span> <span class="mi">16</span><span class="si">))</span><span class="s">&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="kd">let</span> <span class="nv">name</span> <span class="p">=</span> <span class="n">axis</span><span class="p">[</span><span class="n">kCTFontVariationAxisNameKey</span> <span class="k">as</span> <span class="nb">String</span><span class="p">]</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="bp">print</span><span class="p">(</span><span class="s">&#34; Name: </span><span class="si">\(</span><span class="n">name</span><span class="si">)</span><span class="s">&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="kd">let</span> <span class="nv">minValue</span> <span class="p">=</span> <span class="n">axis</span><span class="p">[</span><span class="n">kCTFontVariationAxisMinimumValueKey</span> <span class="k">as</span> <span class="nb">String</span><span class="p">]</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="bp">print</span><span class="p">(</span><span class="s">&#34; Min: </span><span class="si">\(</span><span class="n">minValue</span><span class="si">)</span><span class="s">&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="kd">let</span> <span class="nv">maxValue</span> <span class="p">=</span> <span class="n">axis</span><span class="p">[</span><span class="n">kCTFontVariationAxisMaximumValueKey</span> <span class="k">as</span> <span class="nb">String</span><span class="p">]</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="bp">print</span><span class="p">(</span><span class="s">&#34; Max: </span><span class="si">\(</span><span class="n">maxValue</span><span class="si">)</span><span class="s">&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="kd">let</span> <span class="nv">defaultValue</span> <span class="p">=</span> <span class="n">axis</span><span class="p">[</span><span class="n">kCTFontVariationAxisDefaultValueKey</span> <span class="k">as</span> <span class="nb">String</span><span class="p">]</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="bp">print</span><span class="p">(</span><span class="s">&#34; Default: </span><span class="si">\(</span><span class="n">defaultValue</span><span class="si">)</span><span class="s">&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="bp">print</span><span class="p">(</span><span class="s">&#34;&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="bp">print</span><span class="p">(</span><span class="s">&#34;No variation axes found (not a variable font)&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Here’s an example output for one font:</p> <pre tabindex="0"><code> === Variation Axes for Roboto-ThinItalic === Axis: [&#34;NSCTVariationAxisIdentifier&#34;: 2003265652, &#34;NSCTVariationAxisDefaultValue&#34;: 400, &#34;NSCTVariationAxisName&#34;: Weight, &#34;NSCTVariationAxisMaximumValue&#34;: 900, &#34;NSCTVariationAxisMinimumValue&#34;: 100] Identifier (decimal): 2003265652 Identifier (hex): 0x77676874 Name: Weight Min: 100 Max: 900 Default: 400 Axis: [&#34;NSCTVariationAxisName&#34;: Width, &#34;NSCTVariationAxisIdentifier&#34;: 2003072104, &#34;NSCTVariationAxisDefaultValue&#34;: 100, &#34;NSCTVariationAxisMaximumValue&#34;: 100, &#34;NSCTVariationAxisMinimumValue&#34;: 75] Identifier (decimal): 2003072104 Identifier (hex): 0x77647468 Name: Width Min: 75 Max: 100 Default: 100 </code></pre><p>So it has weights from 100 to 900 and widths from 75 to 100.</p> <h2 id="couple-of-useful-extensions">Couple of useful extensions</h2> <p>I used Claude Code to summarize all 54 of these and produce much of the ensuing enums I will write about. Right of the bat, we will need CoreText framework:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">import</span> <span class="nc">UIKit</span> </span></span><span class="line"><span class="cl"><span class="kd">import</span> <span class="nc">CoreText</span> </span></span></code></pre></div><h3 id="axis-tags">Axis tags</h3> <p>OpenType <a href="https://docs.microsoft.com/en-us/typography/opentype/spec/dvaraxistag_registry">variable font axis tags</a> that we need are listed below:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">enum</span> <span class="nc">OpenTypeAxisTag</span><span class="p">:</span> <span class="nb">Int</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">/// Width axis (&#39;wdth&#39;) - Controls the width of glyphs</span> </span></span><span class="line"><span class="cl"> <span class="c1">/// Range varies by font; Roboto supports 75-100</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">width</span> <span class="p">=</span> <span class="mh">0x77647468</span> <span class="c1">// &#39;wdth&#39;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">/// Weight axis (&#39;wght&#39;) - Controls the weight/boldness of glyphs</span> </span></span><span class="line"><span class="cl"> <span class="c1">/// Range varies by font; Roboto supports 100-900</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">weight</span> <span class="p">=</span> <span class="mh">0x77676874</span> <span class="c1">// &#39;wght&#39;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">/// Italic axis (&#39;ital&#39;) - Binary italic on/off (0 or 1)</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">italic</span> <span class="p">=</span> <span class="mh">0x6974616C</span> <span class="c1">// &#39;ital&#39;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">/// Slant axis (&#39;slnt&#39;) - Oblique slant angle in degrees</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">slant</span> <span class="p">=</span> <span class="mh">0x736C6E74</span> <span class="c1">// &#39;slnt&#39;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">/// Optical size axis (&#39;opsz&#39;) - Optimizes rendering for different sizes</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">opticalSize</span> <span class="p">=</span> <span class="mh">0x6F70737A</span> <span class="c1">// &#39;opsz&#39;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">/// Grade axis (&#39;GRAD&#39;) - Adjusts weight while preserving width</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">grade</span> <span class="p">=</span> <span class="mh">0x47524144</span> <span class="c1">// &#39;GRAD&#39;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">nsNumber</span><span class="p">:</span> <span class="n">NSNumber</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">NSNumber</span><span class="p">(</span><span class="n">value</span><span class="p">:</span> <span class="kc">self</span><span class="p">.</span><span class="n">rawValue</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>We will later reference these values to force CTFont match-up to appropriate font variant. I included more than the three I need. Digging-out these specific values is super convoluted thus keeping them here for possible future use.</p> <h3 id="roboto-declarations">Roboto declarations</h3> <p>Roboto has condensed and standard widths, per axis tag printout from before.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">extension</span> <span class="nc">UIFont</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">enum</span> <span class="nc">Roboto</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">enum</span> <span class="nc">Width</span><span class="p">:</span> <span class="n">CGFloat</span><span class="p">,</span> <span class="n">CaseIterable</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">/// Condensed width (75) - Minimum width supported by Roboto</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">condensed</span> <span class="p">=</span> <span class="mi">75</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">/// Normal width (100) - Maximum width supported by Roboto (default)</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">normal</span> <span class="p">=</span> <span class="mi">100</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">value</span><span class="p">:</span> <span class="n">CGFloat</span> <span class="p">{</span> <span class="n">rawValue</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>We should ideally use <code>UIFont.Width</code> in iOS16+ and these correspond to <code>.condensed</code> and <code>.standard</code> values. Because my app at hand still supports iOS 15+ I opted for this enum.</p> <p>Other font families might have more widths; per OpenType spec, allowed numerical values are between <code>50</code> and <code>200</code>. I’m not 100% sure these are correct values but here’s a possible map:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="p">@</span><span class="n">available</span><span class="p">(</span><span class="n">iOS</span> <span class="mi">16</span><span class="p">,</span> <span class="o">*</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="kd">private</span> <span class="kd">extension</span> <span class="nc">UIFont</span><span class="p">.</span><span class="n">Width</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">numericValue</span><span class="p">:</span> <span class="n">CGFloat</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">switch</span> <span class="kc">self</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="p">.</span><span class="n">compressed</span><span class="p">:</span> <span class="k">return</span> <span class="mi">50</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="p">.</span><span class="n">condensed</span><span class="p">:</span> <span class="k">return</span> <span class="mi">75</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="p">.</span><span class="n">standard</span><span class="p">:</span> <span class="k">return</span> <span class="mi">100</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="p">.</span><span class="n">expanded</span><span class="p">:</span> <span class="k">return</span> <span class="mi">150</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">default</span><span class="p">:</span> </span></span><span class="line"><span class="cl"> <span class="c1">// Handle unknown widths by returning standard.</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="mi">100</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>We don’t need separate enum for 9 weights as they all correspond to <code>UIFont.Weight</code> values although Google uses slightly different font names:</p> <pre tabindex="0"><code> - Thin ⇥ `.ultraLight` - ExtraLight ⇥ `.thin` - Light ⇥ `.light` - Regular ⇥ `.regular` - Medium ⇥ `.medium` - Semibold ⇥ `.semibold` - Bold ⇥ `.bold` - ExtraBold ⇥ `.heavy` - Black ⇥ `.black` </code></pre><p>After adding few more helpful declarations:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">extension</span> <span class="nc">UIFont</span><span class="p">.</span><span class="n">Weight</span><span class="p">:</span> <span class="p">@</span><span class="n">retroactive</span> <span class="n">CaseIterable</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">public</span> <span class="kd">static</span> <span class="kd">var</span> <span class="nv">allCases</span><span class="p">:</span> <span class="p">[</span><span class="n">UIFont</span><span class="p">.</span><span class="n">Weight</span><span class="p">]</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="p">[.</span><span class="n">ultraLight</span><span class="p">,</span> <span class="p">.</span><span class="n">thin</span><span class="p">,</span> <span class="p">.</span><span class="n">light</span><span class="p">,</span> <span class="p">.</span><span class="n">regular</span><span class="p">,</span> <span class="p">.</span><span class="n">medium</span><span class="p">,</span> <span class="p">.</span><span class="n">semibold</span><span class="p">,</span> <span class="p">.</span><span class="n">bold</span><span class="p">,</span> <span class="p">.</span><span class="n">heavy</span><span class="p">,</span> <span class="p">.</span><span class="n">black</span><span class="p">]</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">private</span> <span class="kd">extension</span> <span class="nc">UIFont</span><span class="p">.</span><span class="n">Weight</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">numericValue</span><span class="p">:</span> <span class="n">CGFloat</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">switch</span> <span class="kc">self</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="p">.</span><span class="n">ultraLight</span><span class="p">:</span> <span class="k">return</span> <span class="mi">100</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="p">.</span><span class="n">thin</span><span class="p">:</span> <span class="k">return</span> <span class="mi">200</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="p">.</span><span class="n">light</span><span class="p">:</span> <span class="k">return</span> <span class="mi">300</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="p">.</span><span class="n">regular</span><span class="p">:</span> <span class="k">return</span> <span class="mi">400</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="p">.</span><span class="n">medium</span><span class="p">:</span> <span class="k">return</span> <span class="mi">500</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="p">.</span><span class="n">semibold</span><span class="p">:</span> <span class="k">return</span> <span class="mi">600</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="p">.</span><span class="n">bold</span><span class="p">:</span> <span class="k">return</span> <span class="mi">700</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="p">.</span><span class="n">heavy</span><span class="p">:</span> <span class="k">return</span> <span class="mi">800</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="p">.</span><span class="n">black</span><span class="p">:</span> <span class="k">return</span> <span class="mi">900</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">default</span><span class="p">:</span> </span></span><span class="line"><span class="cl"> <span class="c1">// Handle unknown weights by returning regular.</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="mi">400</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>we are ready to build actual font instances.</p> <h3 id="roboto">Roboto</h3> <p>Ideally one would use <code>UIFontDescriptor</code> APIs to build the font attributes dictionary and create the font but for the life of me I couldn’t figure out how to make it work.</p> <p>The approach below, using CoreText APIs did work.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">extension</span> <span class="nc">UIFont</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">static</span> <span class="kd">func</span> <span class="nf">roboto</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">size</span><span class="p">:</span> <span class="n">CGFloat</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">width</span><span class="p">:</span> <span class="n">Roboto</span><span class="p">.</span><span class="n">Width</span> <span class="p">=</span> <span class="p">.</span><span class="n">normal</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">weight</span><span class="p">:</span> <span class="n">UIFont</span><span class="p">.</span><span class="n">Weight</span> <span class="p">=</span> <span class="p">.</span><span class="n">regular</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">italic</span><span class="p">:</span> <span class="nb">Bool</span> <span class="p">=</span> <span class="kc">false</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> <span class="p">-&gt;</span> <span class="n">UIFont</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// Use the registered font names</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">baseFontName</span> <span class="p">=</span> <span class="n">italic</span> <span class="p">?</span> <span class="s">&#34;Roboto-Italic&#34;</span> <span class="p">:</span> <span class="s">&#34;Roboto&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// Create base font</span> </span></span><span class="line"><span class="cl"> <span class="k">guard</span> <span class="kd">let</span> <span class="nv">baseFont</span> <span class="p">=</span> <span class="n">UIFont</span><span class="p">(</span><span class="n">name</span><span class="p">:</span> <span class="n">baseFontName</span><span class="p">,</span> <span class="n">size</span><span class="p">:</span> <span class="n">size</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="bp">print</span><span class="p">(</span><span class="s">&#34;Warning: Roboto font not found&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">UIFont</span><span class="p">.</span><span class="n">systemFont</span><span class="p">(</span><span class="n">ofSize</span><span class="p">:</span> <span class="n">size</span><span class="p">,</span> <span class="n">weight</span><span class="p">:</span> <span class="n">weight</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// Create variation dictionary using enum</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">variations</span><span class="p">:</span> <span class="p">[</span><span class="n">NSNumber</span><span class="p">:</span> <span class="n">NSNumber</span><span class="p">]</span> <span class="p">=</span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="n">OpenTypeAxisTag</span><span class="p">.</span><span class="n">width</span><span class="p">.</span><span class="n">nsNumber</span><span class="p">:</span> <span class="n">NSNumber</span><span class="p">(</span><span class="n">value</span><span class="p">:</span> <span class="nb">Float</span><span class="p">(</span><span class="n">width</span><span class="p">.</span><span class="n">value</span><span class="p">)),</span> </span></span><span class="line"><span class="cl"> <span class="n">OpenTypeAxisTag</span><span class="p">.</span><span class="n">weight</span><span class="p">.</span><span class="n">nsNumber</span><span class="p">:</span> <span class="n">NSNumber</span><span class="p">(</span><span class="n">value</span><span class="p">:</span> <span class="nb">Float</span><span class="p">(</span><span class="n">weight</span><span class="p">.</span><span class="n">numericValue</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="p">]</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// Create new font with variations using CoreText</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">variedFont</span> <span class="p">=</span> <span class="n">CTFontCreateCopyWithAttributes</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">baseFont</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">size</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="kc">nil</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">CTFontDescriptorCreateWithAttributes</span><span class="p">([</span> </span></span><span class="line"><span class="cl"> <span class="n">kCTFontVariationAttribute</span><span class="p">:</span> <span class="n">variations</span> </span></span><span class="line"><span class="cl"> <span class="p">]</span> <span class="k">as</span> <span class="n">CFDictionary</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> <span class="k">as</span> <span class="n">UIFont</span> <span class="c1">// CTFont is toll-free bridged with UIFont</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">variedFont</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>And now it’s so simple to create whatever combination you want:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">font1</span> <span class="p">=</span> <span class="n">UIFont</span><span class="p">.</span><span class="n">roboto</span><span class="p">(</span><span class="n">size</span><span class="p">:</span> <span class="mi">18</span><span class="p">,</span> <span class="n">width</span><span class="p">:</span> <span class="p">.</span><span class="n">condensed</span><span class="p">,</span> <span class="n">weight</span><span class="p">:</span> <span class="p">.</span><span class="n">black</span><span class="p">,</span> <span class="n">italic</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">condensed</span> <span class="p">=</span> <span class="n">UIFont</span><span class="p">.</span><span class="n">roboto</span><span class="p">(</span><span class="n">size</span><span class="p">:</span> <span class="mi">16</span><span class="p">,</span> <span class="n">width</span><span class="p">:</span> <span class="p">.</span><span class="n">condensed</span><span class="p">,</span> <span class="n">weight</span><span class="p">:</span> <span class="p">.</span><span class="n">regular</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">normal</span> <span class="p">=</span> <span class="n">UIFont</span><span class="p">.</span><span class="n">roboto</span><span class="p">(</span><span class="n">size</span><span class="p">:</span> <span class="mi">16</span><span class="p">,</span> <span class="n">width</span><span class="p">:</span> <span class="p">.</span><span class="n">normal</span><span class="p">,</span> <span class="n">weight</span><span class="p">:</span> <span class="p">.</span><span class="n">regular</span><span class="p">)</span> </span></span></code></pre></div><p>You can use this debug method to build all possible <code>UIFont</code> instances:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">extension</span> <span class="nc">UIFont</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">enum</span> <span class="nc">Roboto</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">static</span> <span class="kd">func</span> <span class="nf">generateAllFonts</span><span class="p">()</span> <span class="p">-&gt;</span> <span class="p">[</span><span class="n">UIFont</span><span class="p">]</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">arr</span><span class="p">:</span> <span class="p">[</span><span class="n">UIFont</span><span class="p">]</span> <span class="p">=</span> <span class="p">[]</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="n">width</span> <span class="k">in</span> <span class="n">Width</span><span class="p">.</span><span class="n">allCases</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="n">weight</span> <span class="k">in</span> <span class="n">UIFont</span><span class="p">.</span><span class="n">Weight</span><span class="p">.</span><span class="n">allCases</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">arr</span><span class="p">.</span><span class="n">append</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">UIFont</span><span class="p">.</span><span class="n">roboto</span><span class="p">(</span><span class="n">size</span><span class="p">:</span> <span class="mi">18</span><span class="p">,</span> <span class="n">width</span><span class="p">:</span> <span class="n">width</span><span class="p">,</span> <span class="n">weight</span><span class="p">:</span> <span class="n">weight</span><span class="p">,</span> <span class="n">italic</span><span class="p">:</span> <span class="kc">false</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="n">width</span> <span class="k">in</span> <span class="n">Width</span><span class="p">.</span><span class="n">allCases</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="n">weight</span> <span class="k">in</span> <span class="n">UIFont</span><span class="p">.</span><span class="n">Weight</span><span class="p">.</span><span class="n">allCases</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">arr</span><span class="p">.</span><span class="n">append</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">UIFont</span><span class="p">.</span><span class="n">roboto</span><span class="p">(</span><span class="n">size</span><span class="p">:</span> <span class="mi">18</span><span class="p">,</span> <span class="n">width</span><span class="p">:</span> <span class="n">width</span><span class="p">,</span> <span class="n">weight</span><span class="p">:</span> <span class="n">weight</span><span class="p">,</span> <span class="n">italic</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">arr</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>and then call it from any breakpoint to print out fontDescriptor for generated fonts:</p> <pre tabindex="0"><code>(lldb) po UIFont.Roboto.generateAllFonts().map { $0.fontDescriptor } </code></pre><p>Very simple way to validate your desired combination of weight+width+italic actually selected proper font variant.</p> <hr> <p>This was far more complex than I expected it to be but it is what it is.</p> The Mystery of black italic condensed UIFont https://aplus.rs/2025/custom-width-weight-italic-uifont/ Wed, 12 Nov 2025 17:39:00 +0000 https://aplus.rs/2025/custom-width-weight-italic-uifont/ <p>Today I encountered frustrating issue while working on custom font styling in an iOS app. I needed to create <em>italic</em> system fonts with custom <em>weight</em> and <em>width</em> variants - something that should be straightforward with UIKit&rsquo;s font descriptor API.<br> Spoiler alert: it wasn&rsquo;t.</p> <h2 id="the-initial-setup">The Initial Setup</h2> <p>I started with what seemed like a reasonable approach - an extension to create italic fonts with custom weight and width:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">private</span> <span class="kd">extension</span> <span class="nc">UIFont</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="p">@</span><span class="n">available</span><span class="p">(</span><span class="n">iOS</span> <span class="mf">16.0</span><span class="p">,</span> <span class="o">*</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">static</span> <span class="kd">func</span> <span class="nf">prepareItalicAttributes</span><span class="p">(</span><span class="k">for</span> <span class="n">weight</span><span class="p">:</span> <span class="n">Weight</span><span class="p">,</span> <span class="n">width</span><span class="p">:</span> <span class="n">Width</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="p">[</span><span class="n">UIFontDescriptor</span><span class="p">.</span><span class="n">AttributeName</span><span class="p">:</span> <span class="nb">Any</span><span class="p">]</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="n">UIFontDescriptor</span><span class="p">.</span><span class="n">AttributeName</span><span class="p">.</span><span class="n">traits</span><span class="p">:</span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="n">UIFontDescriptor</span><span class="p">.</span><span class="n">TraitKey</span><span class="p">.</span><span class="n">weight</span><span class="p">:</span> <span class="n">weight</span><span class="p">.</span><span class="n">rawValue</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">UIFontDescriptor</span><span class="p">.</span><span class="n">TraitKey</span><span class="p">.</span><span class="n">width</span><span class="p">:</span> <span class="n">width</span><span class="p">.</span><span class="n">rawValue</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">UIFontDescriptor</span><span class="p">.</span><span class="n">TraitKey</span><span class="p">.</span><span class="n">symbolic</span><span class="p">:</span> <span class="n">UIFontDescriptor</span><span class="p">.</span><span class="n">SymbolicTraits</span><span class="p">.</span><span class="n">traitItalic</span><span class="p">.</span><span class="n">rawValue</span> </span></span><span class="line"><span class="cl"> <span class="p">]</span> </span></span><span class="line"><span class="cl"> <span class="p">]</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="p">@</span><span class="n">available</span><span class="p">(</span><span class="n">iOS</span> <span class="mf">16.0</span><span class="p">,</span> <span class="o">*</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">static</span> <span class="kd">func</span> <span class="nf">italicSystemFont</span><span class="p">(</span><span class="n">ofSize</span> <span class="n">size</span><span class="p">:</span> <span class="n">CGFloat</span><span class="p">,</span> <span class="n">weight</span><span class="p">:</span> <span class="n">Weight</span><span class="p">,</span> <span class="n">width</span><span class="p">:</span> <span class="n">Width</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="n">UIFont</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">attributes</span> <span class="p">=</span> <span class="n">prepareItalicAttributes</span><span class="p">(</span><span class="k">for</span><span class="p">:</span> <span class="n">weight</span><span class="p">,</span> <span class="n">width</span><span class="p">:</span> <span class="n">width</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">desc</span> <span class="p">=</span> <span class="n">UIFont</span><span class="p">.</span><span class="n">systemFont</span><span class="p">(</span><span class="n">ofSize</span><span class="p">:</span> <span class="n">size</span><span class="p">).</span><span class="n">fontDescriptor</span><span class="p">.</span><span class="n">addingAttributes</span><span class="p">(</span><span class="n">attributes</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">UIFont</span><span class="p">(</span><span class="n">descriptor</span><span class="p">:</span> <span class="n">desc</span><span class="p">,</span> <span class="n">size</span><span class="p">:</span> <span class="n">size</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>This code failed to produce an italic condensed font. If I removed the <code>.width</code> trait from the dictionary, it would produce a condensed font. Something was clearly conflicting so I tried mixing things up hoping it would work.</p> <h2 id="attempt-1-apply-symbolic-traits-separately">Attempt 1: Apply Symbolic Traits Separately</h2> <p>Maybe setting <code>symbolic</code> as a raw value might be conflicting with the <code>.width</code> trait. I tried using <code>withSymbolicTraits(_:)</code> as a separate step:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="p">@</span><span class="n">available</span><span class="p">(</span><span class="n">iOS</span> <span class="mf">16.0</span><span class="p">,</span> <span class="o">*</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="kd">static</span> <span class="kd">func</span> <span class="nf">italicSystemFont</span><span class="p">(</span><span class="n">ofSize</span> <span class="n">size</span><span class="p">:</span> <span class="n">CGFloat</span><span class="p">,</span> <span class="n">weight</span><span class="p">:</span> <span class="n">Weight</span><span class="p">,</span> <span class="n">width</span><span class="p">:</span> <span class="n">Width</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="n">UIFont</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">attributes</span> <span class="p">=</span> <span class="n">prepareItalicAttributes</span><span class="p">(</span><span class="k">for</span><span class="p">:</span> <span class="n">weight</span><span class="p">,</span> <span class="n">width</span><span class="p">:</span> <span class="n">width</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">desc</span> <span class="p">=</span> <span class="n">UIFont</span><span class="p">.</span><span class="n">systemFont</span><span class="p">(</span><span class="n">ofSize</span><span class="p">:</span> <span class="n">size</span><span class="p">).</span><span class="n">fontDescriptor</span><span class="p">.</span><span class="n">addingAttributes</span><span class="p">(</span><span class="n">attributes</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// Apply italic trait separately</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="kd">let</span> <span class="nv">italicDesc</span> <span class="p">=</span> <span class="n">desc</span><span class="p">.</span><span class="n">withSymbolicTraits</span><span class="p">(.</span><span class="n">traitItalic</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">desc</span> <span class="p">=</span> <span class="n">italicDesc</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">UIFont</span><span class="p">(</span><span class="n">descriptor</span><span class="p">:</span> <span class="n">desc</span><span class="p">,</span> <span class="n">size</span><span class="p">:</span> <span class="n">size</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>When I inspected the descriptors of the generated font:</p> <pre tabindex="0"><code>// Before withSymbolicTraits UICTFontDescriptor &lt;0x6000027074e0&gt; = { NSCTFontFeatureSettingsAttribute = ( ... ); NSCTFontTraitsAttribute = { NSCTFontProportionTrait = 0; NSCTFontWeightTrait = &#34;0.2300000041723251&#34;; }; NSCTFontUIUsageAttribute = CTFontRegularUsage; NSFontSizeAttribute = 10; } // After withSymbolicTraits UICTFontDescriptor &lt;0x600002628ea0&gt; = { NSCTFontFeatureSettingsAttribute = ( ... ); NSCTFontUIUsageAttribute = CTFontObliqueUsage; NSFontNameAttribute = &#34;.AppleSystemUIFontItalic&#34;; // ⚠️ Concrete font name! NSFontSizeAttribute = 10; } </code></pre><p>The <code>withSymbolicTraits</code> method was discarding my custom weight and width traits entirely and replacing them with a concrete font name.</p> <h2 id="attempt-2-reverse-the-order">Attempt 2: Reverse the Order</h2> <p>Maybe if I applied the italic trait first, then added the weight and width back, it would work?</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="p">@</span><span class="n">available</span><span class="p">(</span><span class="n">iOS</span> <span class="mf">16.0</span><span class="p">,</span> <span class="o">*</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="kd">static</span> <span class="kd">func</span> <span class="nf">italicSystemFont</span><span class="p">(</span><span class="n">ofSize</span> <span class="n">size</span><span class="p">:</span> <span class="n">CGFloat</span><span class="p">,</span> <span class="n">weight</span><span class="p">:</span> <span class="n">Weight</span><span class="p">,</span> <span class="n">width</span><span class="p">:</span> <span class="n">Width</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="n">UIFont</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">desc</span> <span class="p">=</span> <span class="n">UIFont</span><span class="p">.</span><span class="n">systemFont</span><span class="p">(</span><span class="n">ofSize</span><span class="p">:</span> <span class="n">size</span><span class="p">).</span><span class="n">fontDescriptor</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// Apply italic trait first</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="kd">let</span> <span class="nv">italicDesc</span> <span class="p">=</span> <span class="n">desc</span><span class="p">.</span><span class="n">withSymbolicTraits</span><span class="p">(.</span><span class="n">traitItalic</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">desc</span> <span class="p">=</span> <span class="n">italicDesc</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// Now reapply weight, width, and feature settings</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">attributes</span><span class="p">:</span> <span class="p">[</span><span class="n">UIFontDescriptor</span><span class="p">.</span><span class="n">AttributeName</span><span class="p">:</span> <span class="nb">Any</span><span class="p">]</span> <span class="p">=</span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">traits</span><span class="p">:</span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="n">UIFontDescriptor</span><span class="p">.</span><span class="n">TraitKey</span><span class="p">.</span><span class="n">weight</span><span class="p">:</span> <span class="n">weight</span><span class="p">.</span><span class="n">rawValue</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">UIFontDescriptor</span><span class="p">.</span><span class="n">TraitKey</span><span class="p">.</span><span class="n">width</span><span class="p">:</span> <span class="n">width</span><span class="p">.</span><span class="n">rawValue</span> </span></span><span class="line"><span class="cl"> <span class="p">],</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">featureSettings</span><span class="p">:</span> <span class="p">[</span> <span class="p">...</span> <span class="p">]</span> </span></span><span class="line"><span class="cl"> <span class="p">]</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">desc</span> <span class="p">=</span> <span class="n">desc</span><span class="p">.</span><span class="n">addingAttributes</span><span class="p">(</span><span class="n">attributes</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">UIFont</span><span class="p">(</span><span class="n">descriptor</span><span class="p">:</span> <span class="n">desc</span><span class="p">,</span> <span class="n">size</span><span class="p">:</span> <span class="n">size</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Nope. The italic was now being ignored. Looking at the final font descriptor:</p> <pre tabindex="0"><code>UICTFontDescriptor &lt;0x60000264bc60&gt; = { NSCTFontFeatureSettingsAttribute = ( ... ); NSCTFontUIUsageAttribute = CTFontMediumUsage; // Not italic anymore! NSFontSizeAttribute = 10; } </code></pre><p>The weight/width traits were overriding the italic characteristic.</p> <h2 id="attempt-3-remove-concrete-font-names">Attempt 3: Remove Concrete Font Names</h2> <p>What if I explicitly removed the concrete font name that <code>withSymbolicTraits</code> was adding and used a slant trait instead?</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="p">@</span><span class="n">available</span><span class="p">(</span><span class="n">iOS</span> <span class="mf">16.0</span><span class="p">,</span> <span class="o">*</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="kd">static</span> <span class="kd">func</span> <span class="nf">italicSystemFont</span><span class="p">(</span><span class="n">ofSize</span> <span class="n">size</span><span class="p">:</span> <span class="n">CGFloat</span><span class="p">,</span> <span class="n">weight</span><span class="p">:</span> <span class="n">Weight</span><span class="p">,</span> <span class="n">width</span><span class="p">:</span> <span class="n">Width</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="n">UIFont</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">desc</span> <span class="p">=</span> <span class="n">UIFont</span><span class="p">.</span><span class="n">systemFont</span><span class="p">(</span><span class="n">ofSize</span><span class="p">:</span> <span class="n">size</span><span class="p">).</span><span class="n">fontDescriptor</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="kd">let</span> <span class="nv">italicDesc</span> <span class="p">=</span> <span class="n">desc</span><span class="p">.</span><span class="n">withSymbolicTraits</span><span class="p">(.</span><span class="n">traitItalic</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">desc</span> <span class="p">=</span> <span class="n">italicDesc</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">attributes</span> <span class="p">=</span> <span class="n">desc</span><span class="p">.</span><span class="n">fontAttributes</span> </span></span><span class="line"><span class="cl"> <span class="n">attributes</span><span class="p">.</span><span class="n">removeValue</span><span class="p">(</span><span class="n">forKey</span><span class="p">:</span> <span class="p">.</span><span class="n">name</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">attributes</span><span class="p">[.</span><span class="n">traits</span><span class="p">]</span> <span class="p">=</span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="n">UIFontDescriptor</span><span class="p">.</span><span class="n">TraitKey</span><span class="p">.</span><span class="n">weight</span><span class="p">:</span> <span class="n">weight</span><span class="p">.</span><span class="n">rawValue</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">UIFontDescriptor</span><span class="p">.</span><span class="n">TraitKey</span><span class="p">.</span><span class="n">width</span><span class="p">:</span> <span class="n">width</span><span class="p">.</span><span class="n">rawValue</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">UIFontDescriptor</span><span class="p">.</span><span class="n">TraitKey</span><span class="p">.</span><span class="n">slant</span><span class="p">:</span> <span class="mf">0.07</span> <span class="c1">// Explicit slant</span> </span></span><span class="line"><span class="cl"> <span class="p">]</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">desc</span> <span class="p">=</span> <span class="n">UIFontDescriptor</span><span class="p">(</span><span class="n">fontAttributes</span><span class="p">:</span> <span class="n">attributes</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">UIFont</span><span class="p">(</span><span class="n">descriptor</span><span class="p">:</span> <span class="n">desc</span><span class="p">,</span> <span class="n">size</span><span class="p">:</span> <span class="n">size</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>This created a descriptor with all the right traits:</p> <pre tabindex="0"><code>UICTFontDescriptor &lt;0x6000026484e0&gt; = { NSCTFontFeatureSettingsAttribute = ( ... ); NSCTFontTraitsAttribute = { NSCTFontProportionTrait = &#34;-0.2000000029802322&#34;; NSCTFontSlantTrait = &#34;0.07000000000000001&#34;; NSCTFontWeightTrait = &#34;0.4000000059604645&#34;; }; NSCTFontUIUsageAttribute = CTFontRegularUsage; NSFontSizeAttribute = 32; } </code></pre><p>Perfect, right? But when I created the UIFont from this descriptor:</p> <pre tabindex="0"><code>UICTFontDescriptor &lt;0x6000026486c0&gt; = { NSCTFontFeatureSettingsAttribute = ( ... ); NSCTFontTraitsAttribute = { NSCTFontProportionTrait = &#34;-0.2000000029802322&#34;; // Width survived }; NSCTFontUIUsageAttribute = CTFontBoldItalicUsage; // iOS picked a concrete variant NSFontSizeAttribute = 32; } </code></pre><p>iOS resolved descriptor to the closest concrete system font variant (<code>CTFontBoldItalicUsage</code>) and discarded custom weight and slant.</p> <h2 id="attempt-4-direct-font-names">Attempt 4: Direct Font Names</h2> <p>Maybe I could bypass the system font resolution entirely by using the actual San Francisco Pro font names:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">fontName</span><span class="p">:</span> <span class="nb">String</span> </span></span><span class="line"><span class="cl"><span class="k">switch</span> <span class="n">width</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"><span class="k">case</span> <span class="p">.</span><span class="n">compressed</span><span class="p">:</span> <span class="n">fontName</span> <span class="p">=</span> <span class="s">&#34;.SFCompact-RegularItalic&#34;</span> </span></span><span class="line"><span class="cl"><span class="k">case</span> <span class="p">.</span><span class="n">condensed</span><span class="p">:</span> <span class="n">fontName</span> <span class="p">=</span> <span class="s">&#34;.SFCompact-RegularItalic&#34;</span> </span></span><span class="line"><span class="cl"><span class="k">case</span> <span class="p">.</span><span class="n">standard</span><span class="p">:</span> <span class="n">fontName</span> <span class="p">=</span> <span class="s">&#34;.SFUI-RegularItalic&#34;</span> </span></span><span class="line"><span class="cl"><span class="k">case</span> <span class="p">.</span><span class="n">expanded</span><span class="p">:</span> <span class="n">fontName</span> <span class="p">=</span> <span class="s">&#34;.SFExpanded-RegularItalic&#34;</span> </span></span><span class="line"><span class="cl"><span class="k">default</span><span class="p">:</span> <span class="n">fontName</span> <span class="p">=</span> <span class="s">&#34;.SFUI-RegularItalic&#34;</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">var</span> <span class="nv">desc</span> <span class="p">=</span> <span class="n">UIFontDescriptor</span><span class="p">(</span><span class="n">fontAttributes</span><span class="p">:</span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">name</span><span class="p">:</span> <span class="n">fontName</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">traits</span><span class="p">:</span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="n">UIFontDescriptor</span><span class="p">.</span><span class="n">TraitKey</span><span class="p">.</span><span class="n">weight</span><span class="p">:</span> <span class="n">weight</span><span class="p">.</span><span class="n">rawValue</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">],</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">featureSettings</span><span class="p">:</span> <span class="p">[</span> <span class="p">...</span> <span class="p">]</span> </span></span><span class="line"><span class="cl"><span class="p">])</span> </span></span></code></pre></div><p>The result?</p> <pre tabindex="0"><code>(lldb) po UIFont(descriptor: desc, size: size).fontDescriptor UICTFontDescriptor &lt;0x60000261b300&gt; = { NSFontNameAttribute = TimesNewRomanPSMT; NSFontSizeAttribute = 18; } </code></pre><p>Times New Roman, WTH…😵‍💫? Let me check actual font names:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">static</span> <span class="kd">func</span> <span class="nf">listAvailableFonts</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="n">family</span> <span class="k">in</span> <span class="n">UIFont</span><span class="p">.</span><span class="n">familyNames</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="bp">print</span><span class="p">(</span><span class="s">&#34;</span><span class="si">\(</span><span class="n">family</span><span class="si">)</span><span class="s">&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="n">names</span> <span class="k">in</span> <span class="n">UIFont</span><span class="p">.</span><span class="n">fontNames</span><span class="p">(</span><span class="n">forFamilyName</span><span class="p">:</span> <span class="n">family</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="bp">print</span><span class="p">(</span><span class="s">&#34;== </span><span class="si">\(</span><span class="n">names</span><span class="si">)</span><span class="s">&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>San Francisco fonts were nowhere to be found so this was apparently futile to even attempt. That dot at the start of <code>.AppleSystemUIFontItalic</code> should’ve been hint enough.</p> <h2 id="meh">Meh.</h2> <p>It indeed looked like I have encountered a fundamental limitation in UIKit&rsquo;s font system where you cannot reliably combine custom weight, custom width, and italic traits simultaneously using <code>UIFontDescriptor</code>..?</p> <p>On <a href="https://developer.apple.com/fonts/">Apple Fonts page</a> things are a bit vague. There’s…</p> <blockquote> <p>Nine weights, <strong>including italics</strong> — and features like small caps, fractions, inferior and superior numerals, indices, arrows, and more — offer breadth and depth for precision typesetting.</p></blockquote> <p>and also:</p> <blockquote> <p>SF Pro features nine weights, variable optical sizes for optimal legibility, <strong>four widths</strong>, and includes a rounded variant.</p></blockquote> <p>but those are two separate paragraphs. 🤷🏻‍♂️ So I opened FontBook app and filtered to <em>SF Pro</em> which yielded 45 fonts. There are all the required <em>italic + width</em> variants and there are <em>width + weight</em> variants but not a single “italic + width + weight” variant.</p> <p>It seems the only way to achieve this would be to either:</p> <ol> <li>Ship custom font files with all the weight/width/italic combinations just for this part of the app which would look ridiculous.</li> <li>Accept the limitations and use only the combinations that iOS provides out of the box</li> <li>Figure out a way to artificially italicize fonts but <code>.slant</code> already failed to do that. Not sure is there another way…</li> </ol> <hr> <p><em>I&rsquo;d love to hear ideas as I’m a bit out of them, for now.</em></p> Debugging an Xcodebuild hang, when the Swift compiler gets stuck https://aplus.rs/2025/debugging-xcodebuild-hang-swift-compiler-stuck/ Mon, 03 Nov 2025 09:42:00 +0000 https://aplus.rs/2025/debugging-xcodebuild-hang-swift-compiler-stuck/ <p>Building a complex iOS project with the Release scheme would hang indefinitely - no errors, no messages, just stuck. The Debug config worked fine but any Release configuration scheme would never complete. The build would appear to be running, but never finish.</p> <h2 id="the-investigation">The Investigation</h2> <p>Initial suspicion: circular dependencies between few local Swift packages that were shared among many other local packages (app has over 20 local packages and about 20+ more remote ones).</p> <h3 id="step-1-initial-database-lock-error">Step 1: Initial Database Lock Error</h3> <p>Running <code>xcodebuild</code> with verbose output:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">xcodebuild build -project MeridianBet.xcodeproj -scheme XX <span class="se">\ </span></span></span><span class="line"><span class="cl"><span class="se"></span> -destination <span class="s1">&#39;generic/platform=iOS&#39;</span> -verbose 2&gt;<span class="p">&amp;</span><span class="m">1</span> <span class="p">|</span> tee build.log </span></span></code></pre></div><p>revealed the first issue — build database was locked:</p> <pre tabindex="0"><code>error: unable to attach DB: database is locked Possibly there are two concurrent builds running in the same filesystem location. </code></pre><p>This was very unnecessary self-sabotage step as I had a build running in Xcode which I completely forgot and started another build in command line. Unrelated to the problem but thought to still include it as it&rsquo;s super easy to run into this.</p> <p>Thus here just kill all running builds and do Clean derived data.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">rm -rf ~/Library/Developer/Xcode/DerivedData/MeridianBet-* </span></span></code></pre></div><h3 id="step-2-the-real-hang">Step 2: The Real Hang</h3> <p>After cleaning, the build started but got stuck during package compilation. The process was running but making no progress.</p> <p><em>Check process status:</em></p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Check xcodebuild CPU usage</span> </span></span><span class="line"><span class="cl">top -l <span class="m">1</span> -pid &lt;xcodebuild_pid&gt; -stats pid,cpu,time,command </span></span></code></pre></div><p><strong>Result</strong>: xcodebuild at 0.0% CPU - not doing work, but not crashed either.</p> <p><strong>Find the hung compiler:</strong></p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pgrep -fl swift </span></span></code></pre></div><p><strong>Result</strong>: Found <code>swift-frontend</code> process compiling XXData package, also at 0.0% CPU, running for 8+ minutes.</p> <h3 id="step-3-sampling-the-hung-process">Step 3: Sampling the Hung Process</h3> <p>Used macOS <code>sample</code> command to see what the compiler was doing:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sample &lt;swift-frontend-pid&gt; <span class="m">1</span> -file /tmp/swift_sample.txt </span></span><span class="line"><span class="cl">grep -A <span class="m">30</span> <span class="s2">&#34;Call graph:&#34;</span> /tmp/swift_sample.txt </span></span></code></pre></div><p>And there it was — the call stack showed an <strong>infinite recursion</strong>:</p> <pre tabindex="0"><code>swift::GenericSpecializer::run() -&gt; swift::trySpecializeApplyOfGeneric() -&gt; swift::ReabstractionInfo::prepareAndCheck() -&gt; swift::TypeMatcher::visitNominalType() -&gt; Traversal::doIt(swift::Type) -&gt; Traversal::visitBoundGenericType() -&gt; Traversal::doIt(swift::Type) [RECURSING INFINITELY] </code></pre><h2 id="the-root-cause">The Root Cause</h2> <p>The Swift compiler&rsquo;s <strong>generic specializer</strong> was stuck in an infinite loop when trying to optimize complex generic types in the XXData package with <code>-O -whole-module-optimization</code> enabled.</p> <p>This was <strong>not</strong> any of the stuff I suspected:</p> <ul> <li>a circular package dependency</li> <li>a deadlock between processes</li> <li>a missing file or corrupted cache</li> </ul> <p>It was:</p> <ul> <li>Swift compiler bug with complex generic type traversal</li> <li>Triggered only by <code>whole-module</code> optimization in Release builds</li> <li>Specific to the XXData package (likely from auto-generated CoreData models)</li> </ul> <p>The funny thing is — I did check each local package on their own. Each one built just fine. But I was using simple auto-generated schema you get when you open Package.swift in Xcode and that did not reveal the issue.</p> <h2 id="the-solution">The Solution</h2> <p>Disable whole-module optimization for the problematic package by modifying <code>XXData/Package.swift</code>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">targets</span><span class="p">:</span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">target</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">name</span><span class="p">:</span> <span class="s">&#34;XXData&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">dependencies</span><span class="p">:</span> <span class="p">[...],</span> </span></span><span class="line"><span class="cl"> <span class="n">swiftSettings</span><span class="p">:</span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="c1">// Disable whole-module optimization to prevent compiler hang</span> </span></span><span class="line"><span class="cl"> <span class="c1">// during generic specialization in Release builds</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">unsafeFlags</span><span class="p">([</span><span class="s">&#34;-Onone&#34;</span><span class="p">],</span> <span class="p">.</span><span class="n">when</span><span class="p">(</span><span class="n">configuration</span><span class="p">:</span> <span class="p">.</span><span class="n">release</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="p">]</span> </span></span><span class="line"><span class="cl"> <span class="p">),</span> </span></span><span class="line"><span class="cl"><span class="p">]</span> </span></span></code></pre></div><p>DEBUG builds don&rsquo;t enable <em>whole-module optimization</em> by default, so they never triggered the compiler bug. Release builds (<code>-O -whole-module-optimization</code>) attempted aggressive generic specialization, causing the infinite loop.</p> <p>Here’s compact summary of key steps as some of these shell tools are quite new for me.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># 1. Clean derived data when build seems stuck</span> </span></span><span class="line"><span class="cl">rm -rf ~/Library/Developer/Xcode/DerivedData/MeridianBet-* </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># 2. Run build with verbose output in background</span> </span></span><span class="line"><span class="cl">xcodebuild build -project Project.xcodeproj -scheme Scheme <span class="se">\ </span></span></span><span class="line"><span class="cl"><span class="se"></span> -destination <span class="s1">&#39;generic/platform=iOS&#39;</span> -verbose 2&gt;<span class="p">&amp;</span><span class="m">1</span> <span class="p">|</span> tee build.log <span class="p">&amp;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># 3. Monitor xcodebuild process</span> </span></span><span class="line"><span class="cl">top -l <span class="m">1</span> -pid &lt;pid&gt; -stats pid,cpu,time,command </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># 4. Find hung Swift compiler</span> </span></span><span class="line"><span class="cl">pgrep -fl swift </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># 5. Sample the hung process to see call stack</span> </span></span><span class="line"><span class="cl">sample &lt;pid&gt; <span class="m">1</span> -file /tmp/sample.txt </span></span><span class="line"><span class="cl">grep -A <span class="m">30</span> <span class="s2">&#34;Call graph:&#34;</span> /tmp/sample.txt </span></span></code></pre></div><h2 id="lessons-learned">Lessons Learned</h2> <ol> <li><strong>A hang at 0% CPU is different from a hang at 100% CPU</strong> - the former suggests waiting on something (or infinite recursion with tail call optimization), the latter suggests an infinite computation loop.</li> <li><strong>Verbose build output catches database/file lock issues</strong> that silent builds hide.</li> <li><strong>The <code>sample</code> command is invaluable</strong> for diagnosing hung processes on macOS.</li> <li><strong>Compiler bugs can manifest as build hangs</strong> - not all hangs are your code&rsquo;s fault.</li> <li><strong>Per-package compiler flags</strong> are useful for working around compiler issues without affecting the entire project</li> </ol> <p>I have to commend Claude Code (Sonnet 4.5) as invaluable helper for all of this analysis and investigation. Of all the various tasks one can delegate to LLM tools, those are where it truly excels.</p> AI sceptic in LLM adventure land https://aplus.rs/2025/ai-sceptic-in-llm-adventure-land/ Sun, 15 Jun 2025 08:00:00 +0000 https://aplus.rs/2025/ai-sceptic-in-llm-adventure-land/ <p>Ever since ChatGPT and its gen-AI ilk arrived, I have been very vocal and adamant that these are bullshit generators: they try to guess what you want and will continually hallucinate things until you say it’s OK. Depending on the <em>body of existing knowledge</em> they were trained on, they’ve becoming ever more successful in that endeavour. In the span of just few years and after countless energy spent on their development and training, those tools have become <a href="https://www.designboom.com/technology/why-internet-freaking-out-chatgpt-studio-ghibli-style-ai-images-copyright-artificial-intelligence-03-28-2025/">quite capable</a> delivering amazing, <a href="https://petapixel.com/2025/05/22/10-insane-videos-from-googles-veo-3-ai-that-will-blow-your-mind/">believable</a> results in certain areas.</p> <p>I have deeply ingrained disdain of someone’s creativity being blatantly ripped off thus <a href="https://duckduckgo.com/?q=ai+studio+ghibli+generator&amp;ia=web">countless</a> Studio Ghibli-like image generators make me sick. What I want people to do — what I consider useful and pragmatic use of time and engineering prowess — is building tools that <em>help me create</em> something new, tools that help me <em>automate boring &amp; tedious</em>. To paraphrase an old tweet (I sadly long lost the link to it):</p> <blockquote> <p>I don’t want AI that copies existing work of art. I want the AI that clean my dishes and iron my clothes so I can spend my time imagining new work of art.</p></blockquote> <p><em>I am an app developer.</em> In my line of work, knowledge how to craft good Swift code and how to use iOS frameworks is (sometimes, not always) the tedious part of the job. Learning, testing, trying, reading documentation, trying, testing, reading more docs…rinse and repeat.<br> The final output, the app experience presented to people, that is the creation.</p> <p>In the last several months, I have seen quite a few effusive posts from peers I respect about various AI/LLM tools in day to day work. People I personally know and those I know from Mastodon and general Apple-related blogs, conferences and podcasts. I don’t know will I ever be ready to <a href="https://steipete.me/posts/2025/claude-code-is-my-computer">live dangerously as Peter did</a> but his <a href="https://steipete.me/posts/2025/migrating-700-tests-to-swift-testing">gushing posts</a> about usage of Claude Code and Gemini tipped me over to try it out.</p> <p>I prepared. I read suggestions and recommendations from <a href="https://steipete.me/posts/2025/the-future-of-vibe-coding">Peter</a>, <a href="https://spiess.dev/blog/how-i-use-claude-code">Philipp</a>, <a href="https://blog.nilenso.com/blog/2025/05/29/ai-assisted-coding/">Atharva</a>, <a href="https://cocoaphony.micro.blog/2025/05/26/my-ai-distractions-hopefully-a.html">Rob</a>, <a href="https://fly.io/blog/youre-all-nuts/">Thomas</a>, I watched few <a href="https://www.youtube.com/watch?v=6eBSHbLKuN0">presentations</a> from Anthropic people.</p> <p>Thus I installed NodeJS, <a href="https://www.anthropic.com/claude-code">installed Claude Code</a> and paid $5 in credits to test the waters.</p> <hr> <p>First thing Anthropic recommends to do is just ask Claude to analyze the existing project and tell you what it finds. It will use this analysis to create CLAUDE.md in project root and use it as initial <em>context</em>.</p> <p>❶ Start by initializing Claude in the current folder</p> <blockquote> <p>/init</p></blockquote> <p>and it goes chugging along. After few minutes, this was the result (slightly redacted by me):</p> <pre tabindex="0"><code># CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Development Commands ### Building and Running - **Build**: Use Xcode (`⌘+B`) or `xcodebuild -scheme THE-APP-scheme -project THE-APP.xcodeproj build` - **Run**: Use Xcode (`⌘+R`) or run from simulator - **Target**: The app target is named &#34;THE-APP&#34; (internal name) but the scheme is &#34;THE-APP-scheme&#34; ### Testing - No test files are currently present in the project - Tests would typically be run via Xcode (`⌘+U`) or `xcodebuild test` ## Architecture Overview ### App Structure This is a **SwiftUI-based iOS health and fitness app** with the following architectural layers: 1. **App Entry Point**: `THE-APPApp.swift` - Main app struct with dependency injection setup 2. **Navigation Flow**: `RootView.swift` → `AppState` manages flows (splash → onboarding → main app) 3. **Main Features**: Home tab, Progress tab, Settings tab, Store tab (Shopify integration) ### Key Architecture Patterns **Observable Pattern**: The app uses Swift&#39;s `@Observable` macro extensively for state management: - `DataStorage` - Central data store for model types - `HealthKitService` - Manages HealthKit integration and background processing - `AppState` - Handles app navigation flow states - `ShopifyStore` - Manages e-commerce product data **Dependency Injection**: Dependencies are injected through SwiftUI&#39;s environment system: - `AppService` - API service layer (protocol with HTTP implementation) - `DataStorage` - Observable data layer - `HealthKitService` - Health data integration - `PushNotificationService` - Firebase messaging ### Core Domains **Health Data Integration**: - `HealthKitService.swift` - HealthKit read/write operations - `THE-APPHealthKit` package - Local SPM package for health data extraction - Background processing tasks for health data sync - Local notifications for workout detection **Data Models** (in `Domain/`): - [...redacted...] - Core domain events - [...redacted...] - User interaction models - DTOs in `NetworkEncoding/` for API communication **UI Structure**: - `UI/Main/` - Tab-based main interface (Home, Progress, Settings) - `UI/Onboarding/` - User onboarding flow with coordinator pattern - `UI/Components/` - Reusable SwiftUI components (cards, pickers, camera) ### External Dependencies **Local Packages**: - `THE-APPHealthKit` - Custom HealthKit wrapper - `THE-APPMacros` - Swift macros for code generation **Third-Party Libraries**: - **Firebase**: Auth, Analytics, Crashlytics, Messaging (push notifications) - **Shopify Mobile Buy SDK**: E-commerce integration - **Pulse + Sentinel**: Network debugging and developer tools - **Nuke/NukeUI**: Image loading and caching - **SlidingRuler**: Custom UI component for weight/height selection ### Key Services **Network Layer**: - `HttpAppService` - Main API client with authorized requests - `AuthorizedHttpClient` - Firebase token-based authentication - Base URL configurable in `EnvironmentConfiguration` **Background Processing**: - Background app refresh for HealthKit sync - Background processing tasks registered in Info.plist - Local notifications for new event detection ### Configuration Notes - **Bundle ID**: App uses `$(PRODUCT_BUNDLE_IDENTIFIER)` placeholder - **Entitlements**: HealthKit, Background App Refresh, Push Notifications - **Privacy**: Camera, Photo Library, HealthKit usage descriptions in Info.plist - **Firebase**: Configured via `GoogleService-Info.plist` ### Development Notes - Uses iOS 17+ minimum deployment target - SwiftUI with iOS-only target (no watchOS/macOS) - Supports both iPhone and iPad orientations - Uses Firebase for backend services and Shopify for commerce </code></pre><p>This <em>floored</em> me. 🤯 <br> It is objectively good analysis, which cost me ~30 cents in API credits.</p> <p>Think about onboarding new developers to the team and having to explain the structure of the project. Instead, you could just place them at the <code>claude &gt;</code> prompt and let them ask whatever they want about the project, its structure, specific modules and files.</p> <p>❷ Next, I wanted to see what code it can generate.<br> I had a product details screen in the Store tab that I barely started with. <code>ShopifyStore.swift</code> (<code>@Observable</code> data store file) fetches minimal set of product attributes through GraphQL that Shopify’s Buy SDK uses and SwiftUI displays the data. This project was my first time using GraphQL thus I had just proof of concept that read product’s <code>id</code>, <code>title</code>, <code>price</code> and <code>featured_image</code>.</p> <p>I prompted with</p> <blockquote> <p>look into ProductDetails.swift and plan improvements to show multiple images in horizontal scroller and full product description</p></blockquote> <p>I did not save the actual output it gave me here but it was the most elaborate and detailed plan I have seen in my professional career. A screen and a half of step-by-step changes in several files it thinks it should do, including implementing local cache for the loaded product instances.</p> <p>Thus the next prompt:</p> <blockquote> <p>sounds good. now implement it.</p></blockquote> <p>It added necessary nodes in the GraphQL query, implemented caching in ShopifyStore file, wrote almost perfect SwiftUI with horizontal scroller using TabView with page control dots below. It wrote internal description view and added read more button to it if the text was longer than 500 chars, using <code>@State</code> and <code>@Binding</code> as it should. It also updated <code>Storefront.Product</code> dummy init in <code>#Preview</code>.</p> <p>Outside of few minor stylistic code style finesse, this was utterly perfect.</p> <p>❸ I originally had product price fixed in bottom right corner and for some reason Claude rewrote that to be inline, above the product description. So</p> <blockquote> <p>make the product-price view to be fixed and slightly inset from bottom right screen edge, so it stays visible as content is scrolled</p></blockquote> <p>It implemented the requested changes and wrote this report:</p> <p><img src="https://aplus.rs/images/2025/claude-code-example.png" alt=""></p> <p>👏🏻 The code compiled and was pretty much perfect.</p> <p>❹ OK, now was the time for real test. Since I had product listing and details working, I needed cart / checkout / payment.</p> <p>This is serious task.<br> It starts with simple UI code to add cart toolbar item but then also need add/remove/clear/pay cart management methods, decide where to keep the Cart instance, how to model it + write entire Cart UI from scratch to use all that data and functionality. As developer, you need to read Shopify docs, understand what you need to build cart / cart items, what to call to continue with payout.</p> <p>My prompt:</p> <blockquote> <p>create Cart View in Store tab. add toolbar item to access cart. Use Shopify Buy SDK to make the cart, add/remove items to it, include basic management functionality, prepare payment checkout.</p></blockquote> <p>Note that that I did not tell it exact cart management functionality to implement as I wanted to see what it would do.</p> <p>The output here was far from ideal. UI part of the task was really good, I did not see any problematic SwiftUI code. It chose 22pt body-font tall SFSymbol item for cart which was too large but other than that, all was fine.</p> <p>It modeled <code>CartItem</code> on its own, using <code>productVariantId</code> as its <code>id</code> which is fine. All the add/remove/clear stuff was good. It added <code>+</code>/<code>−</code> to adjust quantities, summary and total price calculations and button to Pay. It placed transient cart instances in the ShopifyStore and used Observable framework in the View to access it.</p> <p>But then completely borked by writing the code that uses <code>Storefront.Checkout</code> model type which does not exists. Buy V3 SDK uses <code>Cart</code>, not <code>Checkout</code>, not sure if <code>Checkout</code> exists in V2. Hence 20+ compilation errors across everything it generated.</p> <blockquote> <p>this code is not good, there is no Checkout type in Buy SDK, you should use Cart. look up this URL as docs: <a href="https://github.com/Shopify/mobile-buy-sdk-ios">https://github.com/Shopify/mobile-buy-sdk-ios</a></p></blockquote> <p>It asked for permission to access that URL and went to correct its mistakes. This time it used all the correct types. It also adjusted GraphQL product fetching queries to load product variant nodes and populate the CartItem it wrote.</p> <p>There were still issues though: the GraphQL handling code is horrible closure in closure in closure in&hellip; mess. This confused it pretty much and it took several prompts to fix almost all of it where I would prompt it with:</p> <blockquote> <p>these code lines {pasted code directly} yield this error: {paste compiler err}</p></blockquote> <p>One by one, this was slowly resolved and one particularly nasty bit I fixed by hand.</p> <p>There was several deprecation warnings which it did not manage to resolve so I manually did those too. Eventually it all compiled and worked perfectly. Products were added to cart, called correct API to generate payment checkout URL which lead to typical Shopify payment screen you have probably seen online.</p> <hr> <p>Somewhere near the end of this whole back and forth I was close to zeroing out the initial $5 worth of credits. But it was enough to convince me that this is worthwhile tool to pay for. Despite errors and shortcomings, it confirmed what many have wrote: <em>this is an amplifier</em>. You need to know the domain in order to validate what it did and guide it towards good result. But it speeds things up immensely and does away quite a lot with the tedious parts of the work I do.</p> <p>I have never been happier to pay $200/month subscription for anything.</p> <p>p.s. I also used Claude to build me proper blog archives on this very blog. This is based on Hugo generator where I never had time to read the docs and figure out proper incantations to create archives per year. It did that perfectly, with very little input from me. I was just describing what I need and gave it a link to Hugo docs. It also found a source of the annoying problem when <code>archives/index.html</code> would display Jackyll code. I mean…seriously — get this tool.</p> Objective-C framework callbacks in Swift 6 https://aplus.rs/2024/objective-c-callback-crashes-swift6/ Thu, 07 Nov 2024 08:16:00 +0000 https://aplus.rs/2024/objective-c-callback-crashes-swift6/ <p>After upgrading all my active projects to Swift 6 language mode, I was bound to encounter some head-scratching edge cases. One of the weirdest ones was this runtime crash:</p> <pre tabindex="0"><code>Thread 5 Queue : com.apple.root.default-qos (concurrent) #0 0x00000001021243f8 in _dispatch_assert_queue_fail () #1 0x0000000102124384 in dispatch_assert_queue () #2 0x00000002444c63e0 in swift_task_isCurrentExecutorImpl () #3 0x0000000105a895b4 in closure #1 in AppDelegate.setupObjCFramework(using:flowIdentifier:) () </code></pre><p>It does not happen when app is compiled and ran in Swift 5 language mode. It is not caught by Swift 6 compiler at build time. But it regularly crashed on app start (in AppDelegate) when a <em>method in Objective-C framework</em> that <em>has a callback closure</em> is executed. Code like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">objcInstance</span><span class="p">.</span><span class="n">setupObjCFramework</span><span class="p">(</span><span class="n">using</span><span class="p">:</span> <span class="n">configFile</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="p">[</span><span class="kr">unowned</span> <span class="kc">self</span><span class="p">]</span> <span class="n">err</span><span class="p">,</span> <span class="n">string</span> <span class="k">in</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>None of the callback’s code is executed thus it seemed like it crashes just before it executes the callback, meaning — it’s a bug in the framework. Or so I thought.</p> <p>After a lot of searching, I <a href="https://developer.apple.com/forums/thread/764777?answerId=807248022#807248022">found the answer</a> in Apple developer forums.</p> <blockquote> <p>The high-level summary is:<br> · Swift 6 has inserted a run-time check to catch a concurrency issue that Swift 5 did not.<br> · This isn’t caught at compile time because of a Swift / Objective-C impedance mismatch.</p></blockquote> <p>So crash is due to this compiler-inserted runtime check because:</p> <ol> <li>There is no guarantee anywhere that callback will be ran on the same thread from where it was called (main thread in my case) which is actually true here - you can see that the crash was in thread 5.</li> <li>There is no guarantee — as it is — that callback closure itself is thread-safe. That check force-crashes to tell you that it <strong>may</strong> theoretically cause the data race even if such scenario never happens in practice.</li> </ol> <p>The simplest solution? Mark the callback <code>Sendable</code> so compiler knows that whatever is done inside is thread-safe:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">objcInstance</span><span class="p">.</span><span class="n">setupObjCFramework</span><span class="p">(</span><span class="n">using</span><span class="p">:</span> <span class="n">configFile</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="p">@</span><span class="n">Sendable</span> <span class="p">[</span><span class="kr">unowned</span> <span class="kc">self</span><span class="p">]</span> <span class="n">err</span><span class="p">,</span> <span class="n">string</span> <span class="k">in</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Ideally the callback should have been marked as such in the Obj-C framework itself (with <code>NS_SWIFT_SENDABLE</code>) but that’s unlikely to happen in practice as most of these frameworks are relics that are not maintained anymore.</p> Background (multipart) file upload from iOS app https://aplus.rs/2024/background-multipart-file-upload-from-ios-app/ Thu, 17 Oct 2024 14:28:00 +0000 https://aplus.rs/2024/background-multipart-file-upload-from-ios-app/ <p>Apple has great API for this purpose: <a href="https://developer.apple.com/documentation/foundation/urlsessionuploadtask">URLSessionUploadTask</a>. Almost at the very top, Apple says:</p> <blockquote> <p>Unlike data tasks, you can use upload tasks to upload content in the background.</p></blockquote> <p>This is not by accident. File uploads are the least-loved feature of any app as no one wants to wait nor look at progress bars. Thus the proper way to do this is to <em>encapsulate upload functionality into a module which is somewhat independent from the rest of the app</em> and maintain an ability to be informed how far along the transfer is, did it finish or encountered an error etc. That module should do its job in background, without ever bothering the user.</p> <h2 id="file-uploader-module">File Uploader module</h2> <p>As example, here’s a barebone module called <code>FileUploader</code> which can be extended to support any number of functionality your app needs.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kr">final</span> <span class="kd">class</span> <span class="nc">FileUploader</span><span class="p">:</span> <span class="n">NSObject</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">typealias</span> <span class="n">Percentage</span> <span class="p">=</span> <span class="nb">Float</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">typealias</span> <span class="n">ProgressHandler</span> <span class="p">=</span> <span class="p">@</span><span class="n">Sendable</span> <span class="p">(</span><span class="n">Percentage</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nb">Void</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">/// Gives nothing if success or encountered error.</span> </span></span><span class="line"><span class="cl"> <span class="kd">typealias</span> <span class="n">CompletionHandler</span> <span class="p">=</span> <span class="p">@</span><span class="n">Sendable</span> <span class="p">(</span><span class="n">Result</span><span class="p">&lt;</span><span class="nb">Void</span><span class="p">,</span> <span class="n">Error</span><span class="p">&gt;)</span> <span class="p">-&gt;</span> <span class="nb">Void</span> </span></span></code></pre></div><p>This is the least we need to make this usable. You need to keep an <code>uploader</code> reference somewhere in the app and that’s all we need as start.</p> <p>Next — this module uses local instance of <code>URLSession</code> which is configured for background-enabled transfers:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">private</span> <span class="kd">let</span> <span class="nv">backgroundIdentifier</span> <span class="p">=</span> <span class="s">&#34;</span><span class="si">\(</span> <span class="n">Bundle</span><span class="p">.</span><span class="n">identifier</span> <span class="si">)</span><span class="s">.background&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">private</span> <span class="kd">var</span> <span class="nv">urlSessionConfiguration</span><span class="p">:</span> <span class="n">URLSessionConfiguration</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">config</span> <span class="p">=</span> <span class="n">URLSessionConfiguration</span><span class="p">.</span><span class="n">background</span><span class="p">(</span><span class="n">withIdentifier</span><span class="p">:</span> <span class="n">backgroundIdentifier</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">config</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p><code>.background(withIdentifier:)</code> is important because it offers a great piece of functionality:</p> <blockquote> <p>If an iOS app is terminated by the system and relaunched, the app can use the same identifier to create a new configuration object and session and to retrieve the status of transfers that were in progress at the time of termination.</p></blockquote> <p>Thus even if background transfers are slow and app is terminated by the system before they finish, you have the ability to pickup the pieces and continue on. That of course requires <a href="https://developer.apple.com/documentation/foundation/urlsession/building_a_resumable_upload_server_with_swiftnio">equally capable backend</a> but the possibility is there.</p> <p>Now, we can create a local instance, using the delegate variant of the API:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="c1">// Creating our custom URLSession instance. We&#39;ll do it lazily</span> </span></span><span class="line"><span class="cl"><span class="c1">// to enable &#39;self&#39; to be passed as the session&#39;s delegate:</span> </span></span><span class="line"><span class="cl"><span class="kd">private</span> <span class="kr">lazy</span> <span class="kd">var</span> <span class="nv">urlSession</span> <span class="p">=</span> <span class="n">URLSession</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">configuration</span><span class="p">:</span> <span class="n">urlSessionConfiguration</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">delegate</span><span class="p">:</span> <span class="kc">self</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">delegateQueue</span><span class="p">:</span> <span class="p">.</span><span class="n">main</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span></code></pre></div><p>We need the delegate form to receive progress reports and we enforce the delivery of those reports on the <code>OperationQueue.main</code> since the most likely purpose is to update some piece of UI.</p> <p>The minimal set of steps to upload the file is:</p> <ol> <li>Prepare the file at some local URL.</li> <li>Prepare the URLRequest with all the proper headers to perform the upload.</li> <li>Start the transfer task, making sure it’s not blocking any UI or other work in the app.</li> <li>Wait for progress / result delegate callbacks to update tracking UI.</li> <li>Cleanup temporary stuff.</li> </ol> <p>I’ll tackle steps 1/2 later in the article; first we need to prepare our module. Input point into the uploader will be a method that receives file <code>URL</code> and <code>URLRequest</code> + progress and result handlers:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">uploadFile</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">at</span> <span class="n">fileURL</span><span class="p">:</span> <span class="n">URL</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">to</span> <span class="n">urlRequest</span><span class="p">:</span> <span class="n">URLRequest</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">progressHandler</span><span class="p">:</span> <span class="p">@</span><span class="n">escaping</span> <span class="n">ProgressHandler</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">completionHandler</span><span class="p">:</span> <span class="p">@</span><span class="n">escaping</span> <span class="n">CompletionHandler</span> </span></span><span class="line"><span class="cl"><span class="p">){</span> </span></span><span class="line"><span class="cl"> <span class="p">...</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p><code>FileUploader</code> needs to keep track of upload-task’s <code>.taskIdentifier</code> versus supplied <code>ProgressHandler</code> and <code>CompletionHandler</code>, which is easy to do:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">private</span> <span class="kd">var</span> <span class="nv">completionHandlersByTaskID</span> <span class="p">=</span> <span class="p">[</span><span class="nb">Int</span> <span class="p">:</span> <span class="n">CompletionHandler</span><span class="p">]()</span> </span></span><span class="line"><span class="cl"><span class="kd">private</span> <span class="kd">var</span> <span class="nv">progressHandlersByTaskID</span> <span class="p">=</span> <span class="p">[</span><span class="nb">Int</span> <span class="p">:</span> <span class="n">ProgressHandler</span><span class="p">]()</span> </span></span></code></pre></div><p>The body of the method is very straightforward:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">task</span> <span class="p">=</span> <span class="n">urlSession</span><span class="p">.</span><span class="n">uploadTask</span><span class="p">(</span><span class="n">with</span><span class="p">:</span> <span class="n">urlRequest</span><span class="p">,</span> <span class="n">fromFile</span><span class="p">:</span> <span class="n">fileURL</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">progressHandlersByTaskID</span><span class="p">[</span><span class="n">task</span><span class="p">.</span><span class="n">taskIdentifier</span><span class="p">]</span> <span class="p">=</span> <span class="n">progressHandler</span> </span></span><span class="line"><span class="cl"><span class="n">completionHandlersByTaskID</span><span class="p">[</span><span class="n">task</span><span class="p">.</span><span class="n">taskIdentifier</span><span class="p">]</span> <span class="p">=</span> <span class="n">completionHandler</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">task</span><span class="p">.</span><span class="n">resume</span><span class="p">()</span> </span></span></code></pre></div><h3 id="tracking-the-uploads">Tracking the uploads</h3> <p>We need to handle two delegate callbacks:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">extension</span> <span class="nc">FileUploader</span><span class="p">:</span> <span class="n">URLSessionTaskDelegate</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>First — progress report:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">urlSession</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="kc">_</span> <span class="n">session</span><span class="p">:</span> <span class="n">URLSession</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">task</span><span class="p">:</span> <span class="n">URLSessionTask</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">didSendBodyData</span> <span class="n">bytesSent</span><span class="p">:</span> <span class="nb">Int64</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">totalBytesSent</span><span class="p">:</span> <span class="nb">Int64</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">totalBytesExpectedToSend</span><span class="p">:</span> <span class="nb">Int64</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">guard</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">handler</span> <span class="p">=</span> <span class="n">progressHandlersByTaskID</span><span class="p">[</span><span class="n">task</span><span class="p">.</span><span class="n">taskIdentifier</span><span class="p">],</span> </span></span><span class="line"><span class="cl"> <span class="k">else</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">progress</span> <span class="p">=</span> <span class="nb">Double</span><span class="p">(</span><span class="n">totalBytesSent</span><span class="p">)</span> <span class="o">/</span> <span class="nb">Double</span><span class="p">(</span><span class="n">totalBytesExpectedToSend</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">handler</span><span class="p">(</span><span class="nb">Float</span><span class="p">(</span><span class="n">progress</span><span class="p">))</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Second is result (completion) handler:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">urlSession</span><span class="p">(</span><span class="kc">_</span> <span class="n">session</span><span class="p">:</span> <span class="n">URLSession</span><span class="p">,</span> <span class="n">task</span><span class="p">:</span> <span class="n">URLSessionTask</span><span class="p">,</span> <span class="n">didCompleteWithError</span> <span class="n">err</span><span class="p">:</span> <span class="p">(</span><span class="n">any</span> <span class="n">Error</span><span class="p">)?)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">guard</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">completionHandler</span> <span class="p">=</span> <span class="n">completionHandlersByTaskID</span><span class="p">[</span><span class="n">task</span><span class="p">.</span><span class="n">taskIdentifier</span><span class="p">],</span> </span></span><span class="line"><span class="cl"> <span class="k">else</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="kd">let</span> <span class="nv">err</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">completionHandler</span><span class="p">(.</span><span class="n">failure</span><span class="p">(</span><span class="n">NetworkError</span><span class="p">.</span><span class="n">generalError</span><span class="p">(</span><span class="n">err</span><span class="p">)))</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">completionHandler</span><span class="p">(.</span><span class="n">success</span><span class="p">(()))</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Note that this is bare minimum. There are quite a few <a href="https://developer.apple.com/documentation/foundation/urlsessiontaskdelegate">other delegate methods</a> which are used in more advanced or edge-case scenarios.</p> <p>Handling all possible cases and scenarios is far from easy; if you get into trouble I highly recommend Apple Developer Forums and especially threads [<a href="https://forums.developer.apple.com/forums/thread/10239">1</a>], [<a href="https://developer.apple.com/forums/thread/14853">2</a>] and [<a href="https://forums.developer.apple.com/forums/thread/743847">3</a>] where Quinn “The Eskimo!” responded with elaborate details.</p> <h2 id="preparing-the-urlrequest">Preparing the URLRequest</h2> <p>I have set this as input parameter because it’s quite often unrelated to the upload itself. You might be uploading to a password-protected or OAuth2-access-token backend. Thus you will need to handle all of that <em>before</em> you attempt the upload.</p> <p>The only upload-related consideration: is your backend accepting</p> <ul> <li>raw binary files <em>or</em></li> <li>it expects multipart-form-upload, a remnant of the old web-form type of transfers.</li> </ul> <p>In the first case you just need to supply proper MIME type for <code>Content-Type</code> HTTP header and you’re done. As example, if you are uploading a JPEG photo, it would be this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">headers</span><span class="p">[</span><span class="s">&#34;Content-Type&#34;</span><span class="p">]</span> <span class="p">=</span> <span class="s">&#34;image/jpeg&#34;</span> </span></span></code></pre></div><p>For the latter — you need to specify what’s your <em>boundary</em> which is simple UUID string. More on this in the next segment.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">headers</span><span class="p">[</span><span class="s">&#34;Content-Type&#34;</span><span class="p">]</span> <span class="p">=</span> <span class="s">&#34;multipart/form-data; boundary=</span><span class="si">\(</span> <span class="n">boundary</span> <span class="si">)</span><span class="s">&#34;</span> </span></span></code></pre></div><p>Preparing a local file is highly dependent on which of these two options your backend expects.</p> <h2 id="preparing-local-file-as-upload-source">Preparing local file as upload source</h2> <p><em>Background file upload only works with local file instances.</em> You can’t use <code>Data</code> nor <code>InputStream</code> — it must be a file on the disk.</p> <p>Second important point is that the <em>source file can’t be protected in any way</em>. If you attempt to background upload a file with <a href="https://developer.apple.com/documentation/foundation/urlfileprotection">URLFileProtection</a> other than <code>.none</code> you’ll be greeted with <code>API MISUSE</code> warning in the Xcode console.</p> <p>Thus in the case of raw binary upload, I recommend to alter the file protection of the local file. Or copy the original file into some temporary location and remove file protection from that copy. Whatever the case, you should be using <a href="https://developer.apple.com/documentation/foundation/filemanager/1413667-setattributes">setAttributes(_:ofItemAtPath:)</a> API:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">fm</span> <span class="p">=</span> <span class="n">FileManager</span><span class="p">.</span><span class="k">default</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">localFilePath</span> <span class="p">=</span> <span class="n">realFileURL</span><span class="p">.</span><span class="n">path</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">try</span> <span class="n">fm</span><span class="p">.</span><span class="n">setAttributes</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="p">[.</span><span class="n">protectionKey</span><span class="p">:</span> <span class="n">FileProtectionType</span><span class="p">.</span><span class="kr">none</span><span class="p">],</span> </span></span><span class="line"><span class="cl"> <span class="n">ofItemAtPath</span><span class="p">:</span> <span class="n">localFilePath</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span></code></pre></div><p>Upload imitating web-forms is more involved.<br> The HTTP body of the URLRequest in this case is part-string, part-binary where using <code>Data</code> would be my first choice. But as I already said, <code>Data</code> as upload source is not an option. So we need to fake it: write-out upload contents to a temporary file and use that as UploadTask’ source.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">boundary</span> <span class="p">=</span> <span class="n">UUID</span><span class="p">().</span><span class="n">uuidString</span> </span></span><span class="line"><span class="cl"><span class="c1">// prepare URLRequest here</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// now prepare file source</span> </span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">realFileURL</span><span class="p">:</span> <span class="n">URL</span> <span class="c1">// real file we want to upload</span> </span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">localURL</span><span class="p">:</span> <span class="n">URL</span> <span class="c1">// temporary fileURL</span> </span></span><span class="line"><span class="cl"><span class="k">do</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">guard</span> <span class="kd">let</span> <span class="nv">outputStream</span> <span class="p">=</span> <span class="n">OutputStream</span><span class="p">(</span><span class="n">url</span><span class="p">:</span> <span class="n">localURL</span><span class="p">,</span> <span class="n">append</span><span class="p">:</span> <span class="kc">false</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">throw</span> <span class="n">OutputStream</span><span class="p">.</span><span class="n">OutputStreamError</span><span class="p">.</span><span class="n">unableToCreateFile</span><span class="p">(</span><span class="n">localURL</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">outputStream</span><span class="p">.</span><span class="n">open</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="k">try</span> <span class="n">outputStream</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="s">&#34;--</span><span class="si">\(</span> <span class="n">boundary</span> <span class="si">)</span><span class="se">\r\n</span><span class="s">&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">try</span> <span class="n">outputStream</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="s">&#34;Content-Disposition: form-data; name=</span><span class="se">\&#34;</span><span class="s">file</span><span class="se">\&#34;</span><span class="s">; filename=</span><span class="se">\&#34;</span><span class="si">\(</span> <span class="n">realFileURL</span><span class="p">.</span><span class="n">lastPathComponent</span> <span class="si">)</span><span class="se">\&#34;\r\n</span><span class="s">&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">try</span> <span class="n">outputStream</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="s">&#34;Content-Type: image/jpeg</span><span class="se">\r\n\r\n</span><span class="s">&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">try</span> <span class="n">outputStream</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="n">contentsOf</span><span class="p">:</span> <span class="n">realFileURL</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">try</span> <span class="n">outputStream</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="s">&#34;</span><span class="se">\r\n</span><span class="s">&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">try</span> <span class="n">outputStream</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="s">&#34;--</span><span class="si">\(</span> <span class="n">boundary</span> <span class="si">)</span><span class="s">--</span><span class="se">\r\n</span><span class="s">&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">outputStream</span><span class="p">.</span><span class="n">close</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// now changefile protection to none</span> </span></span><span class="line"><span class="cl"> <span class="k">try</span> <span class="n">fm</span><span class="p">.</span><span class="n">setAttributes</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="p">[.</span><span class="n">protectionKey</span><span class="p">:</span> <span class="n">FileProtectionType</span><span class="p">.</span><span class="kr">none</span><span class="p">],</span> </span></span><span class="line"><span class="cl"> <span class="n">ofItemAtPath</span><span class="p">:</span> <span class="n">localFilePath</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="k">catch</span> <span class="p">{...}</span> </span></span></code></pre></div><p>Note that <code>boundary</code> used here <em>must be</em> the same one you used to prepare the <code>Content-Type</code> header for the <code>URLRequest</code> instance.</p> <p>In the end, when file upload is done, don’t forget to cleanup all these temporary files.</p> <hr> <p>I skipped over many things as creating a robust file uploader is a lot of work accompanied by a lot of testing. But for occasional uploads in somewhat controlled environment — your app to your backend — this is likely almost everything that you’ll need.</p> Stress-testing Core Data https://aplus.rs/2024/core-data-performance-test/ Sat, 29 Jun 2024 11:18:00 +0000 https://aplus.rs/2024/core-data-performance-test/ <p>I don’t think I wrote many apps in the last 10 years that did not involve Core Data. Apps without Core Data:</p> <ul> <li>have just a handful of screens with no shared data between them</li> <li>are demonstration / help type of app, basically a glorified user manual</li> </ul> <p>But if app has any kind of model graph / data shared across multiple views, I choose Core Data without hesitation. Its strengths are in handling of relationships, its transparently cached and batched data fetching, seamless notification on insert/update of entity properties. The list of functionality you get for free is impressive.</p> <p>One aspect that Core Data is not known for is raw performance. Vast majority of apps don’t really need super high persistence performance, certainly not any of the apps I have worked on. But it’s something to keep in mind and I’m always checking few things on physical devices:</p> <ul> <li>CPU usage, especially time spent in code blocks which “lock” one CPU core</li> <li>memory consumption</li> </ul> <p>CPU usage became less important since iPhones started shipping with at least 2-core CPUs (do you remember which iPhone that was?). But memory is always a challenge and worth optimizing for; it does not matter if all the Core Data work you do happens on background threads if the app consumes GBs of memory. This can lead to excessive memory fragmentation, background app termination etc.</p> <p>Which is why <a href="https://github.com/jacobsapps/RealmVsSwiftData">recent performance tests</a> published on <a href="https://www.emergetools.com/blog/posts/swiftdata-vs-realm-performance-comparison">Emerge Tools blog</a> picked my interest. They compared Realm vs Swift Data, two persistence solutions I have no experience with, despite Swift Data being essentially the same as Core Data. Enter Helge Heß who <a href="https://github.com/Data-swift/RealmVsSwiftData">updated the test suit</a> with pure Core Data test suite.</p> <p>The <a href="https://mastodon.social/@helge/112689073238089716">numbers he posted</a> were frankly shocking, way beyond anything I have ever seen in my own Core Data usage. Granted — I never attempted to insert 100k or 1M objects in one commit but I understand this is performance stress test. Thus let’s forgo common sense for a bit and analyze the performance results, which I re-ran on iPhone 14 Pro:</p> <pre tabindex="0"><code>=============== CoreData: 100 Simple Objects =============== User instantiation: 0.0008 Create users: 0.0129 DB file size: 0.03 MB Delete all users: 0.0003 =============== CoreData: 1.000 Simple Objects =============== User instantiation: 0.0035 Create users: 0.0102 DB file size: 0.03 MB Delete all users: 0.0009 =============== CoreData: 10.000 Simple Objects =============== User instantiation: 0.0248 Create users: 0.0745 DB file size: 0.03 MB Delete all users: 0.0059 =============== CoreData: 100.000 Simple Objects =============== User instantiation: 0.2107 Create users: 0.8991 DB file size: 7.22 MB Delete all users: 0.0867 =============== CoreData: 1.000.000 Simple Objects =============== User instantiation: 2.2695 Create users: 39.7839 DB file size: 72.46 MB Delete all users: 3.8762 </code></pre><p>The problematic part is huge jump from 100k to 1M: 1s went to over 40 seconds. <em>40× more!</em></p> <p>The memory graph is terrifying with over 1GB peak usage:</p> <p><img src="https://aplus.rs/images/2024/perf-coredata-00.png" alt=""></p> <p>Looking at the test code, first thing I did was isolate each test. I see in the code that each tests begins with fresh database creation but I still wanted to completely isolate each test run. Thus I removed (commented) out all but Core Data tests:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">runAllPerformanceTests</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="n">num</span> <span class="k">in</span> <span class="p">[</span><span class="mi">100</span><span class="p">,</span> <span class="mi">1_000</span><span class="p">,</span> <span class="mi">10_000</span><span class="p">,</span> <span class="mi">100_000</span><span class="p">,</span> <span class="mi">1_000_000</span><span class="p">]</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">coreUsersPerformanceTests</span><span class="p">(</span><span class="n">with</span><span class="p">:</span> <span class="n">num</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">deleteAllDatabaseFiles</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>and also delete the app from device after each test. Results I posted above are after one such isolated run.</p> <p>Let’s look at the test suite:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">coreUsersPerformanceTests</span><span class="p">(</span><span class="n">with</span> <span class="n">usersCount</span><span class="p">:</span> <span class="nb">Int</span> <span class="p">=</span> <span class="mi">100_000</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">db</span> <span class="p">=</span> <span class="k">try</span><span class="p">!</span> <span class="n">CoreUserDB</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">try</span><span class="p">!</span> <span class="n">db</span><span class="p">.</span><span class="n">deleteAll</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">users</span> <span class="p">=</span> <span class="p">[</span><span class="n">CoreUser</span><span class="p">]()</span> </span></span><span class="line"><span class="cl"> <span class="n">logExecutionTime</span><span class="p">(</span><span class="s">&#34;User instantiation&#34;</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">users</span> <span class="p">=</span> <span class="p">(</span><span class="mf">0.</span><span class="p">.&lt;</span><span class="n">usersCount</span><span class="p">).</span><span class="n">compactMap</span> <span class="p">{</span> <span class="kc">_</span> <span class="k">in</span> <span class="n">CoreUser</span><span class="p">()</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">logExecutionTime</span><span class="p">(</span><span class="s">&#34;Create users&#34;</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">try</span><span class="p">!</span> <span class="n">db</span><span class="p">.</span><span class="n">create</span><span class="p">(</span><span class="n">users</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">logExecutionTime</span><span class="p">(</span><span class="s">&#34;Fetch users named `Jane` in age order&#34;</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="p">...</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">logExecutionTime</span><span class="p">(</span><span class="s">&#34;Rename users named `Jane` to `Wendy`&#34;</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="p">...</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">measureSize</span><span class="p">(</span><span class="n">of</span><span class="p">:</span> <span class="n">db</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">logExecutionTime</span><span class="p">(</span><span class="s">&#34;Delete all users&#34;</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">try</span><span class="p">!</span> <span class="n">db</span><span class="p">.</span><span class="n">deleteAll</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>After creating the test database, test will</p> <ol> <li>create X User instances in memory</li> <li>Insert all instances to one <code>NSManagedObjectContext</code> and then save that context</li> <li>Execute one test fetch returning subset of objects</li> <li>Update one property (<code>name</code>) then save it</li> <li>Measure database size on disk</li> <li>Measure how long it takes to delete all previously created objects from database, using batch-delete API.</li> </ol> <p>Main time sync, per logged times, is <em>step 2</em>. So let’s look at steps 1 and 2, since they are essentially one import operation:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">var</span> <span class="nv">users</span> <span class="p">=</span> <span class="p">[</span><span class="n">CoreUser</span><span class="p">]()</span> </span></span><span class="line"><span class="cl"><span class="n">users</span> <span class="p">=</span> <span class="p">(</span><span class="mf">0.</span><span class="p">.&lt;</span><span class="n">usersCount</span><span class="p">).</span><span class="n">compactMap</span> <span class="p">{</span> <span class="kc">_</span> <span class="k">in</span> <span class="n">CoreUser</span><span class="p">()</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">try</span><span class="p">!</span> <span class="n">db</span><span class="p">.</span><span class="n">create</span><span class="p">(</span><span class="n">users</span><span class="p">)</span> </span></span></code></pre></div><p>or if we unpack the <code>create(_)</code> method:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">var</span> <span class="nv">users</span> <span class="p">=</span> <span class="p">[</span><span class="n">CoreUser</span><span class="p">]()</span> </span></span><span class="line"><span class="cl"><span class="n">users</span> <span class="p">=</span> <span class="p">(</span><span class="mf">0.</span><span class="p">.&lt;</span><span class="n">usersCount</span><span class="p">).</span><span class="n">compactMap</span> <span class="p">{</span> <span class="kc">_</span> <span class="k">in</span> <span class="n">CoreUser</span><span class="p">()</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">context</span> <span class="p">=</span> <span class="n">ModelContext</span><span class="p">(</span><span class="n">container</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="n">users</span><span class="p">.</span><span class="n">insertWithAllDependencies</span><span class="p">(</span><span class="n">into</span><span class="p">:</span> <span class="n">context</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="k">try</span> <span class="n">context</span><span class="p">.</span><span class="n">save</span><span class="p">()</span> </span></span></code></pre></div><p>Let’s start optimizing this.</p> <p>First thing I added is <code>context.reset()</code> after the <code>save()</code>. We don’t use all of these objects after creating them, we are actually doing a fetch for the next step so it makes no sense to keep all these objects in memory. This one line created a noticeable dip in the memory usage:</p> <p><img src="https://aplus.rs/images/2024/perf-coredata-01-context-reset-mem.png" alt=""></p> <p>But main issue remains that we create 1 million records and save them all at once. Since we already know that 100k import is fast enough, let’s split this 1M into 10 commits with 100k records each:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">importer</span><span class="p">:</span> <span class="p">(</span><span class="nb">Int</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nb">Void</span> <span class="p">=</span> <span class="p">{</span> <span class="n">limit</span> <span class="k">in</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">users</span> <span class="p">=</span> <span class="p">[</span><span class="n">CoreUser</span><span class="p">]()</span> </span></span><span class="line"><span class="cl"> <span class="n">logExecutionTime</span><span class="p">(</span><span class="s">&#34;User instantiation&#34;</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">users</span> <span class="p">=</span> <span class="p">(</span><span class="mf">0.</span><span class="p">.&lt;</span><span class="n">limit</span><span class="p">).</span><span class="n">compactMap</span> <span class="p">{</span> <span class="kc">_</span> <span class="k">in</span> <span class="n">CoreUser</span><span class="p">()</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">logExecutionTime</span><span class="p">(</span><span class="s">&#34;Create users&#34;</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">try</span><span class="p">!</span> <span class="n">db</span><span class="p">.</span><span class="n">create</span><span class="p">(</span><span class="n">users</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">chunk</span> <span class="p">=</span> <span class="mi">100_000</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">usersCount</span> <span class="o">&lt;=</span> <span class="n">chunk</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">importer</span><span class="p">(</span><span class="n">usersCount</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="k">else</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">logExecutionTime</span><span class="p">(</span><span class="s">&#34;Total </span><span class="si">\(</span> <span class="n">usersCount</span> <span class="si">)</span><span class="s"> User instantiation + Create&#34;</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">limit</span> <span class="p">=</span> <span class="n">usersCount</span> <span class="o">/</span> <span class="n">chunk</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="n">i</span> <span class="k">in</span> <span class="p">(</span><span class="mi">0</span> <span class="p">..</span><span class="o">&lt;</span> <span class="n">limit</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="bp">print</span><span class="p">(</span><span class="s">&#34;Chunk count: </span><span class="si">\(</span> <span class="n">i</span> <span class="si">)</span><span class="s">&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">importer</span><span class="p">(</span><span class="n">chunk</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>This yields marked improvement in time, dropping from 40s to 17s:</p> <pre tabindex="0"><code>=============== CoreData: 1.000.000 Simple Objects =============== Total 1000000 User instantiation + Create: 17.0897 190 users named `Jane` Fetch users named `Jane` in age order: 0.1683 190 users named `Jane` being renamed to `Wendy` 190 users renamed to `Wendy` Rename users named `Jane` to `Wendy`: 0.0617 DB file size: 72.70 MB Delete all users: 2.7786 </code></pre><p>It’s more important what happens with memory usage, which peaks around 350 MB, a sharp drop from 1050 MB:</p> <p><img src="https://aplus.rs/images/2024/perf-coredata-02-chunk-100k.png" alt=""></p> <p>Even though this is impressive improvement, that import code is not really optimal Core Data; not even <em>proper</em> for Core Data.</p> <ul> <li>There’s literally no sense to create <code>ManagedObject</code> outside <code>ManagedObjectContext</code> if you eventually want to save that object.</li> <li>Further — Core Data’s primary good practice is to perform all operations related to it inside its own queue meaning we should do everything import-related inside <code>perform</code> or <code>performAndWait</code> methods.</li> </ul> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">importer2</span><span class="p">:</span> <span class="p">(</span><span class="nb">Int</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nb">Void</span> <span class="p">=</span> <span class="p">{</span> <span class="n">limit</span> <span class="k">in</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">context</span> <span class="p">=</span> <span class="n">db</span><span class="p">.</span><span class="n">context</span> </span></span><span class="line"><span class="cl"> <span class="n">context</span><span class="p">.</span><span class="n">performAndWait</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">logExecutionTime</span><span class="p">(</span><span class="s">&#34;User instantiation&#34;</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="mf">0.</span><span class="p">.&lt;</span><span class="n">limit</span><span class="p">).</span><span class="n">forEach</span> <span class="p">{</span> <span class="kc">_</span> <span class="k">in</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">_</span> <span class="p">=</span> <span class="n">CoreUser</span><span class="p">(</span><span class="n">moc</span><span class="p">:</span> <span class="n">context</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">logExecutionTime</span><span class="p">(</span><span class="s">&#34;Save users&#34;</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">try</span><span class="p">!</span> <span class="n">context</span><span class="p">.</span><span class="n">save</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">context</span><span class="p">.</span><span class="n">reset</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>That is recommended way to do any Core Data piece of work. CPU and memory graphs reflect that:</p> <p><img src="https://aplus.rs/images/2024/perf-coredata-03.png" alt=""></p> <p>even though raw numbers are very similar to previous approach:</p> <pre tabindex="0"><code>=============== CoreData: 1.000.000 Simple Objects =============== Total 1000000 User instantiation + Create: 16.1105 232 users named `Jane` Fetch users named `Jane` in age order: 0.0678 232 users named `Jane` being renamed to `Wendy` 232 users renamed to `Wendy` Rename users named `Jane` to `Wendy`: 0.0610 DB file size: 72.46 MB Delete all users: 2.7570 </code></pre><p>Thus by using somewhat reasonable but still sizable chunks (100k records) of the original data set and employing Core Data best practices, we <em>lowered peak memory usage 10×</em> and <em>shortened total time spent about 2.5×</em> which is no small feat.</p> <p>Updated <a href="https://github.com/radianttap/RealmVsSwiftData/tree/core-data-optim">tests are on GitHub</a>.</p> <hr> <p>This is pure stress test which has its merits; it can help choosing appropriate persistence solution based on what you are trying to solve. Keep in mind that Core Data was never designed for raw performance; it’s a super powerful object graph framework that also does persistence.</p> <p>In real apps, I advise you to never do things like making import operations with 100k records. Not even 10k records. What would you do with 100k records import if it fails? How can you even reason around conflicts or failure to save with 100k records? There is no UI you can present that will be able to help a person resolving the issue, whatever it is. Even if it’s just pure background import with no user interaction at all — in case of any failure you will need to at least repeat the downloading, processing and saving of your data. It’s much less expensive to repeat 1k record import than 10k or 100k.</p> <p>Be sensible.</p> <hr> <p>I assume similar improvements could be done for SwiftData but I don’t work with it thus won’t attempt to improve it. I ran its original test which took way more time than Core Data, spending 148s (!) for 100k records import.</p> Strange issue adding SPM package as dependency on another package https://aplus.rs/2024/strange-issue-adding-spm-package-as-dependency-to-another-package/ Wed, 19 Jun 2024 10:10:00 +0000 https://aplus.rs/2024/strange-issue-adding-spm-package-as-dependency-to-another-package/ <p>Swift Package Manager dependency resolution mostly work logically:</p> <p>① First you specify the dependency in any number of ways, here’s the most usual one:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="err">dependencies:</span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="err">.package(</span> </span></span><span class="line"><span class="cl"> <span class="err">url:</span> <span class="s2">&#34;https://github.com/radianttap/Alley&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="err">from:</span> <span class="s2">&#34;3.0.0&#34;</span> </span></span><span class="line"><span class="cl"> <span class="err">)</span><span class="p">,</span> </span></span></code></pre></div><p>② Then you reference dependent library by its product name:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="err">targets:</span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="err">.target(</span> </span></span><span class="line"><span class="cl"> <span class="err">name:</span> <span class="s2">&#34;Atlas&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="err">dependencies:</span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="s2">&#34;Alley&#34;</span> </span></span><span class="line"><span class="cl"> <span class="p">],</span> </span></span></code></pre></div><p><a href="https://github.com/radianttap/Alley">Alley</a> is simplest possible library - it has one target, called like the package, no sub-dependencies. There’s no confusion how to reference it. Most packages are like this.</p> <hr> <p>But things are not always that simple. Let’s use <a href="https://github.com/auth0/Auth0.swift">Auth0</a>’s iOS SDK as example, which is at this URL:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="err">.package(</span> </span></span><span class="line"><span class="cl"> <span class="err">url:</span> <span class="s2">&#34;https://github.com/auth0/Auth0.swift&#34;</span><span class="err">,</span> </span></span><span class="line"><span class="cl"> <span class="err">from:</span> <span class="s2">&#34;2.7.0&#34;</span> </span></span><span class="line"><span class="cl"><span class="err">),</span> </span></span></code></pre></div><p>If this is added as dependency to an iOS app, it works as any other package — you do <code>import Auth0</code> where needed and all is fine.</p> <p>But if we add this package as dependency to another package (here called <code>Backend</code>):</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="err">targets:</span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="err">.target(</span> </span></span><span class="line"><span class="cl"> <span class="err">name:</span> <span class="s2">&#34;Backend&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="err">dependencies:</span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="s2">&#34;Auth0&#34;</span> </span></span><span class="line"><span class="cl"> <span class="p">],</span> </span></span></code></pre></div><p>we get this confusing error:</p> <pre tabindex="0"><code>product &#39;Auth0&#39; required by package &#39;backend&#39; target &#39;Backend&#39; not found. Did you mean &#39;Auth0&#39;? </code></pre><p>Auth0 package is correctly resolved and checked-out, along with its 2 sub-dependencies. There’s no other package nor target called <code>Auth0</code> thus I don’t really get what’s confusing SPM dependency resolver.</p> <p>For whatever reason, we need to explicitly point where the library product is:</p> <pre tabindex="0"><code>targets: [ .target( name: &#34;Backend&#34;, dependencies: [ .product(name: &#34;Auth0&#34;, package: &#34;Auth0.swift&#34;) ], </code></pre><p>Why this? Let’s look at package manifest:</p> <pre tabindex="0"><code>let package = Package( name: &#34;Auth0&#34;, products: [.library(name: &#34;Auth0&#34;, targets: [&#34;Auth0&#34;])], </code></pre><ul> <li>package name is <code>Auth0</code></li> <li>library and target are also called <code>Auth0</code></li> <li>package URL is called <code>Auth0.swift</code> on GitHub</li> </ul> <p>I admit I haven’t delved deep into how naming resolution in SPM works thus naively I can conclude that <code>package.name</code> is pretty much irrelevant and that resolver looks into ending part of the repo URL, which is <code>Auth0.swift</code> and uses that to reference the package.</p> <p>I encountered the similar issue with a package used on another project. It’s called <a href="https://github.com/bvantagelimited/IPificationSwiftDistribution">IPificationSDK</a> but here’s how it is loaded:</p> <pre tabindex="0"><code>.package( url: &#34;https://github.com/bvantagelimited/IPificationSwiftDistribution.git&#34;, exact: &#34;2.0.12&#34; ), </code></pre><p>and of course, this is how it’s referenced:</p> <pre tabindex="0"><code>targets: [ .target( name: &#34;WrapperService&#34;, dependencies: [ .product(name: &#34;IPificationSDK&#34;, package: &#34;ipificationswiftdistribution&#34;) ], </code></pre><p>I’ve seen a similar thing happen if you add binary-distributed library as package dependency. It’s actually the search for answer to that problem that lead me to <a href="https://forums.swift.org/t/swiftpm-binary-target-with-sub-dependencies/40197/5">this SwiftPM forum post</a> which provided the solution.</p> <p>It’s incredible to me that I have not encountered this <code>.product(name:package:)</code> nugget anywhere else — not in Apple’s videos or documentation guides nor any of the blog posts that are returned as top search results for these keywords. Every article out there provides examples with simple <code>String</code> values.</p> <p>Apple definitely needs to hire more technical writers.</p> Hackintosh is (almost) dead https://aplus.rs/2024/hackintosh-almost-dead/ Sat, 16 Mar 2024 09:10:10 +0000 https://aplus.rs/2024/hackintosh-almost-dead/ <p>While I knew about and even tried <a href="https://aplus.rs/2004/mac-os-xhappily-running-on-my-amd-athlon/">various very early attempts</a> to run macOS on non-Apple hardware, it wasn’t until <a href="https://aplus.rs/2020/hmac/">early 2020</a> that I’ve built my first proper one. Then I built several more which are still seeing daily use.</p> <p>I <a href="https://aplus.rs/2020/missing-developer-mac/">explained my reasoning</a> why it was worthwhile to attempt it. The technology was mostly there thanks to a group of dedicated hackers and timing <a href="https://aplus.rs/2020/hmac-2020/">was just right</a>:</p> <blockquote> <p>But if ever there was a time to do it, it’s now.<br> Apple is transitioning to their own CPUs/GPUs over the next two years. Several years from now, I see myself purchasing whatever desktop Apple Silicon-based machine is there.</p></blockquote> <p>I also offered a prognosis which turned out partially true:</p> <blockquote> <p>Many will tell you that buying Intel-based hardware from Apple is buying obsolete models. I don’t really agree with that since it’s a given that those Intel-based Macs will be supported for 7-10 years of future macOS updates.</p></blockquote> <p>It’s true that latest macOS 14 (Sonoma) still supports the latest generations of Intel Macs and it’s very likely that at least one or two major versions will still be compatible. But there’s one particular development that is de-facto killing off the Hackintosh scene.</p> <p>In Sonoma, <strong>Apple has completely removed all traces of driver support for their oldest WiFi/Bt cards</strong>, namely various Broadcom cards that they last used in 2012/13 iMac / MacBook models. Those Mac models are not supported by macOS for few years now thus it’s not surprising the drivers are being removed. Most likely reason is that Apple is moving drivers away from <code>.kext</code> (Kernel Extensions) to <code>.dext</code> (DriverKit) thus cleaning up obsolete and unused code from macOS. They did the same with Ethernet drivers in Ventura.</p> <p>Those particular cards were the key ingredient to many fully functional Hackintosh builds for simple reason: they worked out of the box with every single (so-called) iService Apple has: Messages, FaceTime, AirDrop, Continuity, Handoff - you name it. <em>Everything worked.</em> Despite the <a href="https://dortania.github.io/OpenCore-Legacy-Patcher/SONOMA-DROP.html#versioning">valiant efforts of OCLP crew</a> to make workarounds, those cards can work in Sonoma <em>only</em> if you <a href="https://github.com/perez987/Broadcom-wifi-back-on-macOS-Sonoma-by-OCLP">seriously downgrade</a> the macOS security.</p> <p>There was some hope that <a href="https://github.com/OpenIntelWireless/itlwm">OpenIntelWireless</a> could replace those cards due to <a href="https://github.com/OpenIntelWireless">amazing work</a> zxystd did in the last 4 years. I mean, the WiFi speeds in macOS with Intel’s WiFi6 cards are <a href="https://forum.amd-osx.com/threads/wi-fi-speed-test-intel-ax200-vs-bcm94360ng.4756/">nothing short of spectacular</a>. But Apple’s continued cleanup and rewrite of their driver stack has pretty much killed-off any reliable support for Message and FaceTime despite iCloud sync still working great. zxystd <a href="https://github.com/OpenIntelWireless/itlwm/issues/953#issuecomment-1920759538">describes the new mountain to climb</a>:</p> <blockquote> <p>From Sonoma, Apple drops IO80211FamilyLegacy, I build AirportItlwmV2 on the top of IO80211Family, but using some hacks, you can simply interpret it as me implementing a set of IO80211FamilyLegacy myself. <em>This implementation may have side effects such as the iService not working etc</em>. Since IO80211Family uses skywalk API instead of original Ethernet API (Also we can foresee that the Ethernet API will also be dropped in macOS 15), without these hacks we should follow the Apple&rsquo;s API and <em>rewrite the whole driver</em>, that&rsquo;s what I would never do.</p></blockquote> <p>In 14.4, Apple <a href="https://www.tonymacx86.com/threads/usb-problems-after-14-4-update.329275/">seem to have made changes</a> in how USB subsystem works too. This was always a tedious challenge but if minor updates can almost <a href="https://forum.amd-osx.com/threads/update-from-14-3-1-to-14-4.5013/">brick the build</a> it becomes a headache. Still…USB is a known problem with known solution thus it’s annoying but solvable.</p> <p>WiFi with iServices is sadly not.</p> <p>I’ve long held the opinion that it would not be CPU nor GPU changes that kill the Hacks — it would be lack of reliable WiFi drivers. And now, ~4 years later, Hackintosh hits a brick wall of no easy WiFi options available, at all. Given how much of the macOS useful features is dependent on presence of particular WiFi chips — a decision of Apple developers I really can’t understand — I can’t really consider builds without those features to call themselves Mac.</p> <p>I did not come to this conclusion just by reading the forums.</p> <p>I have a rag-tag build sitting on my desk for several months now. It was supposed to be a quick proof-of-concept Sonoma build with Intel AX200 WiFi/Bt, AMD CPU and GPU, NVMe SSDs - everything that modern Mac should work with. It’s everything that my current Hackintosh is, with SIP intact, incremental updates working on their own etc — a perfect Mac.</p> <p><img src="https://aplus.rs/images/2024/rag-tag-build.jpg" alt="My wannabe Sonoma-compatible Mac"></p> <p>But pretty much since day one I encountered one problem after another. Things were so volatile and random that it was hard to believe, at times. Like —</p> <ul> <li>One day Ethernet controller (Intel I225-V) would work great, the next day it would just hard-crash the entire machine. No freaking idea why. Tried multiple ways and custom drivers to make it work but nothing was perfectly stable.</li> <li>WiFi works fantastic, iCloud is perfect but Messages/FaceTime wouldn’t connect at all. In either Monterey, Ventura or Sonoma. That same card worked perfectly on another motherboard with Monterey and Ventura, no issues with Messages / FaceTime at all. Again — no idea why.</li> <li>Bluetooth would work great for days but if I turn it off and restart the machine, something would become so messed up that it starts being recognised as BCM_4350C2 chip instead of Intel AX200. Only a round-trip to Windows 11 would somehow bring the chip into a state that <a href="https://github.com/OpenIntelWireless/IntelBluetoothFirmware">IntelBluetooth driver</a> can work with it.</li> <li>Sonoma 14.3.1 works great on this build. But 14.4 update won’t install. It starts booting the installer and just reboots back almost immediately.</li> </ul> <p>Hence — <strong>Hackintosh is on its death bed</strong>. Some things will work for few more months or maybe even years, depending on what you use it for and whether lack of WiFi bothers you or not. But not for me. I can live without AirDrop, Continuity and Handoff but Messages and FaceTime must work. There’re also some other things Sonoma brings that are important to me thus I want to update to it. Coupled with described lack of reliability and fretting if next minor or major update would leave me dry — nah, not worth it.</p> <p>I don’t really complain. I had a good run which helped me skip over the worst price/performance Mac lineup that I remember. There’re now plenty good choices within the current crop of M1 / M2 / M3 machines and I’ll be following eBay closely for a good used Mac mini / studio models. Or maybe even splurge on something new.</p> <p>Lest I forget — if macOS Ventura works for you, stay on it! That’s still perfectly stable without a single issue across a variety of build options.</p> <hr> <p>Just to clarify one thing, to preempt someone saying Apple did this on purpose to kill off Hackintosh: <strong>they didn’t</strong>. Apple never cared about Hackintosh scene, it’s entirely irrelevant to their business. They did what they should be doing, improving the macOS codebase. It’s always a good thing to remove obsolete and deprecated code thus Apple is doing the right thing for their product.</p> High-performance Core Data import https://aplus.rs/2024/high-performance-core-data-import/ Tue, 30 Jan 2024 12:20:00 +0000 https://aplus.rs/2024/high-performance-core-data-import/ <p>Let’s say you are writing an iOS client for Spotify’s API, specifically the <a href="https://developer.spotify.com/documentation/web-api/reference/search">search endpoint</a>. Here you can get multiple entities, like <code>Artist</code>, <code>Album</code>, <code>Track</code>, <code>Playlist</code> and <em>all</em> of them are inter-related in that same JSON. <br> You might get particular artist instance as top-level result and/or inside <code>album.artists</code> or even inside <code>track.album.artists</code>. It’s quite possible that one same artist can appear in all of those places.</p> <p>When searching repeatedly for various terms and parsing/importing each received JSON, it would be a disaster to insert the same artist as multiple records into Core Data store. Easy way to prevent this is by setting <code>id</code> as unique attribute. That path leads to <em>lots</em> of merge conflicts when attempting to save into the store and figuring out how to resolve them automatically (better UX but rather complicated in some edge cases) or bother your customers to choose (way worse UX, you should almost never do this).<br> It’s not a good path.</p> <p>Another way to prevent this is to check if an artist with particular <code>id</code> already exists and then do an update of the other properties. Naturally, such check should be done for each JSON object. This is what Apple calls <em>upsert</em> in the SwiftData videos as it’s essentially an insert/update combined into one operation.</p> <p>The least performant way is to do this for each record you encounter. Scenario of:</p> <ul> <li>parse JSON object, possibly directly into new ManagedObject</li> <li>do Core Data fetch for a record with given <code>id</code></li> <li>if it exists, populate its properties and remove the newly created object from step 1</li> <li>if it does not, keep the object for later save</li> </ul> <p>This will perform as many CoreData fetches as you have objects to import. Inherently it is super slow as for a 1000 JSON objects you will do 1000 fetches for 1 object and another one to perform save. iPhones/iPads have very fast SSD chips but they are still much, much slower than their memory chips. Thus the right way to do such import is:</p> <ul> <li>extract <code>id</code> of the objects from the JSON</li> <li>preload <em>all</em> existing records with those <code>id</code>s</li> <li>do insert/update processing in memory</li> <li>save</li> </ul> <p>This will result in just a handful of fetches and one save. The more objects you process in a batch, the more performance benefit you get with this approach.</p> <blockquote> <p>This is The Way!™</p></blockquote> NSFRC’s sections considered harmful https://aplus.rs/2023/core-data-fetched-results-controller-sections-bug/ Sat, 23 Dec 2023 11:14:00 +0000 https://aplus.rs/2023/core-data-fetched-results-controller-sections-bug/ <p>Imagine a simple sports app model: <code>League</code> entity, with some <code>id</code>, <code>name</code> and <code>displayOrder</code> properties. It has a 1-many relationship to <code>Event</code> entity that has (among other attributes) <code>id</code>, <code>name</code> and of course — reverse to-one relationship back to <code>League</code>.</p> <p>You write a simple <code>FetchRequest</code> for <code>FetchedResultsController</code> when you are loading events grouped per league, with order defined by <code>League.displayOrder</code> but the section name (title) is <code>League.name</code>.</p> <p>To get the grouping with proper section titles, your <a href="https://developer.apple.com/documentation/coredata/nsfetchedresultscontroller/1622285-sectionnamekeypath">sectionNameKeyPath</a> on the Event entity should be <code>league.name</code> .</p> <p>But to get the correct order per <code>displayOrder</code> you declare <a href="https://developer.apple.com/documentation/coredata/nsfetchrequest/1506262-sortdescriptors">sortDescriptors</a> of the FRC’s <a href="https://developer.apple.com/documentation/coredata/nsfetchrequest">fetchRequest</a> to be</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">NSSortDescriptor</span><span class="p">(</span><span class="n">key</span><span class="p">:</span> <span class="s">&#34;league.displayOrder&#34;</span><span class="p">,</span> <span class="n">ascending</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="n">NSSortDescriptor</span><span class="p">(</span><span class="n">key</span><span class="p">:</span> <span class="s">&#34;league.name&#34;</span><span class="p">,</span> <span class="n">ascending</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">...</span> </span></span></code></pre></div><p>This should be fine, right? Order of events will be correct and there should be no issues for Core Data to group per league’s name.</p> <p>The resulting <a href="https://developer.apple.com/documentation/coredata/nsfetchedresultscontroller/1622278-fetchedobjects">fetchedObjects</a> array looks correct:</p> <p><img src="https://aplus.rs/images/2023/frc-sections-bug-01.png" alt=""></p> <p>But the order of sections is not correct:</p> <p><img src="https://aplus.rs/images/2023/frc-sections-bug-02.png" alt=""></p> <p>It looks to be sorted per section title. 😣</p> <h3 id="how-bad-this-is">How bad this is..?</h3> <p>This is a serious issue since you naturally want to process the result per sections / events and feed some collection view, expecting proper order. After all — why would you group per league unless to display them per league, with proper priority order.</p> <p>Even worse, if you use CompositionalLayout and thus use newer delegate methods like <a href="https://developer.apple.com/documentation/coredata/nsfetchedresultscontrollerdelegate/3235742-controller">controller(_:didChangeContentWith:)</a> you will naturally want to use given Core Data snapshot to rebuild diffable data source of your collection view.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">controller</span><span class="p">(</span><span class="kc">_</span> <span class="n">frc</span><span class="p">:</span> <span class="n">NSFetchedResultsController</span><span class="p">&lt;</span><span class="n">NSFetchRequestResult</span><span class="p">&gt;,</span> <span class="n">didChangeContentWith</span> <span class="n">snapshot</span><span class="p">:</span> <span class="n">NSDiffableDataSourceSnapshotReference</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// String==sectionIdentifier =&gt; sectionNameKeyPath or UUID (if nil)</span> </span></span><span class="line"><span class="cl"> <span class="c1">// NSManagedObjectID of the changed object</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">cds</span> <span class="p">=</span> <span class="n">snapshot</span> <span class="k">as</span> <span class="n">NSDiffableDataSourceSnapshot</span><span class="p">&lt;</span><span class="nb">String</span><span class="p">,</span> <span class="n">NSManagedObjectID</span><span class="p">&gt;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">snapshotEvents</span><span class="p">(</span><span class="n">usingCoreDataSnapshot</span><span class="p">:</span> <span class="n">cds</span><span class="p">,</span> <span class="n">animated</span><span class="p">:</span> <span class="kc">false</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>But the <code>sectionIdentifiers</code> of the Core Data snapshot are wrongly ordered:</p> <p><img src="https://aplus.rs/images/2023/frc-sections-bug-03.png" alt=""></p> <p>and <strong>even worse</strong> — wrong set of <code>itemIdentifiers</code> is associated for each of them:</p> <p><img src="https://aplus.rs/images/2023/frc-sections-bug-04.png" alt=""></p> <p>To make it clearer what’s wrong, here’s side-by-side:</p> <p><img src="https://aplus.rs/images/2023/frc-sections-bug-05.png" alt=""></p> <p>Clearly first 6 events do not belong to “Championship” league and thus are associated with the wrong section.</p> <p>This can hard-crash your app, depending on how you use this snapshot.</p> <p>For example, with code like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">sectionIdentifiers</span> <span class="p">=</span> <span class="n">cds</span><span class="p">.</span><span class="n">sectionIdentifiers</span> </span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">si</span> <span class="k">in</span> <span class="n">sectionIdentifiers</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">secitem</span> <span class="p">=</span> <span class="p">...</span> </span></span><span class="line"><span class="cl"> <span class="n">snapshot</span><span class="p">.</span><span class="n">appendSections</span><span class="p">([</span><span class="n">secitem</span><span class="p">])</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">items</span> <span class="p">=</span> <span class="n">cds</span><span class="p">.</span><span class="n">itemIdentifiers</span><span class="p">(</span><span class="n">inSection</span><span class="p">:</span> <span class="n">si</span><span class="p">).</span><span class="bp">map</span> <span class="p">{...}</span> </span></span><span class="line"><span class="cl"> <span class="n">snapshot</span><span class="p">.</span><span class="n">appendItems</span><span class="p">(</span><span class="n">items</span><span class="p">,</span> <span class="n">toSection</span><span class="p">:</span> <span class="n">secitem</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>you will get a crash with:</p> <pre tabindex="0"><code>*** Terminating app due to uncaught exception &#39;NSInternalInconsistencyException&#39;, reason: &#39;Diffable data source detected an attempt to insert or append 1 section identifier that already exists in the snapshot. Identifiers in a snapshot must be unique. Section identifier that already exists: EventsListDataSource.SectionItem.league(leagueId: 122, name: &#34;Championship&#34;)&#39; </code></pre><p>Anything that can lead to hard-crash is severe bug. And I am now revisiting all the places I use sectionNameKeyPath and checking to see if intermittent crashes I get are caused by this issue.</p> <h3 id="how-to-get-around-this">How to get around this?</h3> <p>Both <em>frc.fetchedObjects</em> and <em>cds.itemIdentifiers</em> are in the correct order. Thus both can be used to recreate section/item hierarchy but I’m not aware of any easy way to rebuild sectionIdentifiers.</p> <p>Re-implementing the FRC.sections iterator is fairly straightforward though, on case-by-case basis. Here:</p> <pre tabindex="0"><code>let leagues = events.map({ $0.league }).unique() for league in leagues { let leagueEvents = validEvents.filter({ $0.league.leagueId == league.leagueId }) } </code></pre><p>So that’s what I use and completely ignore Core Data snapshot, for now.</p> Saving native Swift struct into Core Data's transformable attribute https://aplus.rs/2023/saving-struct-into-core-data-transformable/ Thu, 28 Sep 2023 16:40:00 +0000 https://aplus.rs/2023/saving-struct-into-core-data-transformable/ <p>When designing Core Data object graph, I spend copious amount of time thinking through each entity’s attributes and relationships. If I’m not careful, I can easily end-up with expansive graph containing many one-of leaf nodes. Analysing various graphs in the past, I noticed that many complex entities (even with dozen or so attributes) are really just a complex property of their 1-1 relationship pair.</p> <p>Say you have some <code>Event</code> object that has an elaborate set of configuration options. That configuration object is always used and accessed as a whole with the particular Event instance. Thus it should really be an attribute and not a relationship which is much harder to manage.</p> <p>Let’s see how we can use Transformable attribute to save and later restore an entire struct.</p> <p>First, this struct — called <code>MarketGroupsConfiguration</code> in this example — must support <code>Codable</code> to serialize its instances into <code>Data</code>.</p> <h3 id="model-setup">Model setup</h3> <p>This is the usual setup of the said attribute in the Core Data modeller from Xcode:</p> <p><img src="https://aplus.rs/images/2023/transformable-modeler.png" alt=""></p> <p>which translates to this declaration in the model class:</p> <pre tabindex="0"><code>@NSManaged var marketGroupsConfiguration: MarketGroupsConfigurationBox? </code></pre><h3 id="wrapper-class">Wrapper class</h3> <p>The custom class with <code>*Box</code> suffix is needed because we can’t directly save Swift&rsquo;s <code>struct</code> — it must be <code>NSObject</code> subclass:</p> <pre tabindex="0"><code>final class MarketGroupsConfigurationBox: NSObject { let unbox: MarketGroupsConfiguration init(_ value: MarketGroupsConfiguration) { self.unbox = value } } extension MarketGroupsConfiguration { var boxed: MarketGroupsConfigurationBox { return MarketGroupsConfigurationBox(self) } } </code></pre><p>Our wrapper class must implement <code>NSSecureCoding</code> which is required for recent Core Data versions:</p> <pre tabindex="0"><code>extension MarketGroupsConfigurationBox: NSSecureCoding { static var supportsSecureCoding: Bool { return true } func encode(with coder: NSCoder) { do { let encoder = JSONEncoder() let data = try encoder.encode(unbox) coder.encode(data, forKey: &#34;unbox&#34;) } catch let codableError { print(codableError) } } convenience init?(coder: NSCoder) { if let data = coder.decodeObject(of: NSData.self, forKey: &#34;unbox&#34;) { do { let decoder = JSONDecoder() let b = try decoder.decode( MarketGroupsConfiguration.self, from: (data as Data) ) self.init(b) } catch let codableError { print(codableError) return nil } } else { return nil } } } </code></pre><h3 id="transformer">Transformer</h3> <p><code>ValueTransformer</code> implementation is pretty involved but <a href="https://developer.apple.com/documentation/foundation/valuetransformer">official documentation</a> has enough details to get us started.</p> <pre tabindex="0"><code>@objc(MarketGroupsConfigurationTransformer) final class MarketGroupsConfigurationTransformer: NSSecureUnarchiveFromDataTransformer { static let name = NSValueTransformerName( rawValue: String(describing: MarketGroupsConfigurationTransformer.self) ) public static func register() { let transformer = MarketGroupsConfigurationTransformer() ValueTransformer.setValueTransformer(transformer, forName: name) } override static var allowedTopLevelClasses: [AnyClass] { return [ MarketGroupsConfigurationBox.self, NSData.self ] } override public class func transformedValueClass() -&gt; AnyClass { return MarketGroupsConfigurationBox.self } override public class func allowsReverseTransformation() -&gt; Bool { return true } override public func transformedValue(_ value: Any?) -&gt; Any? { guard let data = value as? Data else { return nil } do { let box = try NSKeyedUnarchiver.unarchivedObject( ofClass: MarketGroupsConfigurationBox.self, from: data ) return box } catch { return nil } } override public func reverseTransformedValue(_ value: Any?) -&gt; Any? { guard let box = value as? MarketGroupsConfigurationBox else { return nil } do { let data = try NSKeyedArchiver.archivedData( withRootObject: box, requiringSecureCoding: true ) return data } catch { return nil } } } </code></pre><p>Most of these methods are metadata required for <code>ValueTransformer</code> to work. The essence is inside <code>transformedValue()</code> and <code>reverseTransformedValue ()</code> methods which deserialise and serialise our instance.</p> <p>Lastly, somewhere <em>very early</em> in the app lifecycle, you must register this <code>ValueTransformer</code> type so it can be properly recognised by iOS SDK:</p> <pre tabindex="0"><code>MarketGroupsConfigurationTransformer.register() </code></pre><p>I usually do this right before Core Data container / model is initialised.</p> <p>And that’s it!<br> As you can see, this code is easily adaptable to as many transformable attributes as you need.</p> Implement data detectors inside UILabel https://aplus.rs/2023/uilabel-data-detectors/ Mon, 18 Sep 2023 14:23:00 +0000 https://aplus.rs/2023/uilabel-data-detectors/ <p><code>UILabel</code> has ability to render rather complex text through <code>.attributtedText</code> but is not user-interactive by default. Thus even though particular text can contain URLs or phone numbers, there is no native capability to tap on said URL and open a web view.</p> <p>Curiously, most open-source components that aim to enable this are years-old, written in Objective-C. Here’s how you can re-implement this feature today.</p> <p>First, we need an extension over <code>String</code> which will detect all the <a href="https://developer.apple.com/documentation/foundation/nstextcheckingresult/checkingtype">CheckingType</a>s we care about:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">extension</span> <span class="nc">String</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span> <span class="nf">detectCheckingTypes</span><span class="p">(</span><span class="kc">_</span> <span class="n">types</span><span class="p">:</span> <span class="n">NSTextCheckingResult</span><span class="p">.</span><span class="n">CheckingType</span> <span class="p">=</span> <span class="p">[.</span><span class="n">link</span><span class="p">])</span> <span class="p">-&gt;</span> <span class="p">[</span><span class="n">NSTextCheckingResult</span><span class="p">]</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">guard</span> <span class="kd">let</span> <span class="nv">detector</span> <span class="p">=</span> <span class="k">try</span><span class="p">?</span> <span class="n">NSDataDetector</span><span class="p">.</span><span class="kd">init</span><span class="p">(</span><span class="n">types</span><span class="p">:</span> <span class="n">types</span><span class="p">.</span><span class="n">rawValue</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="p">[]</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">s</span> <span class="p">=</span> <span class="kc">self</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">matches</span> <span class="p">=</span> <span class="n">detector</span><span class="p">.</span><span class="n">matches</span><span class="p">(</span><span class="k">in</span><span class="p">:</span> <span class="n">s</span><span class="p">,</span> <span class="n">options</span><span class="p">:</span> <span class="p">[],</span> <span class="n">range</span><span class="p">:</span> <span class="n">NSRange</span><span class="p">(</span><span class="n">location</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="n">length</span><span class="p">:</span> <span class="n">s</span><span class="p">.</span><span class="n">utf16</span><span class="p">.</span><span class="bp">count</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">matches</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>There are about dozen types of data you can detect with this but I’ll focus on <code>.link</code> to keep things simple.</p> <p>Now, when we have <code>UILabel</code> instance inside some <code>UIView</code>, we can check if there’s anything detectable. If yes, then populate <code>.attributedText</code> after highlighting the found term (using AccentColor is good choice) and attaching <a href="https://developer.apple.com/documentation/uikit/uitapgesturerecognizer">UITapGestureRecognizer</a> to the label.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">label</span><span class="p">.</span><span class="n">text</span> <span class="p">=</span> <span class="n">string</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">detectedLinks</span> <span class="p">=</span> <span class="n">string</span><span class="p">.</span><span class="n">detectCheckingTypes</span><span class="p">([.</span><span class="n">link</span><span class="p">])</span> </span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">detectedLinks</span><span class="p">.</span><span class="bp">count</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">attrString</span> <span class="p">=</span> <span class="n">NSMutableAttributedString</span><span class="p">(</span><span class="n">string</span><span class="p">:</span> <span class="n">string</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="n">match</span> <span class="k">in</span> <span class="n">detectedLinks</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">matchRange</span> <span class="p">=</span> <span class="n">match</span><span class="p">.</span><span class="n">range</span> </span></span><span class="line"><span class="cl"> <span class="k">guard</span> <span class="kd">let</span> <span class="nv">range</span> <span class="p">=</span> <span class="nb">Range</span><span class="p">(</span><span class="n">matchRange</span><span class="p">,</span> <span class="k">in</span><span class="p">:</span> <span class="n">string</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span> <span class="k">continue</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">term</span> <span class="p">=</span> <span class="nb">String</span><span class="p">(</span><span class="n">string</span><span class="p">[</span><span class="n">range</span><span class="p">])</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// highlight link</span> </span></span><span class="line"><span class="cl"> <span class="n">attrString</span><span class="p">.</span><span class="n">colorize</span><span class="p">(</span><span class="n">term</span><span class="p">:</span> <span class="n">term</span><span class="p">,</span> <span class="n">usingColor</span><span class="p">:</span> <span class="p">.</span><span class="n">accentColor</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// handle tap on link</span> </span></span><span class="line"><span class="cl"> <span class="n">label</span><span class="p">.</span><span class="n">handleInteractionWithCheckingType</span><span class="p">(</span><span class="n">match</span><span class="p">,</span> <span class="n">inside</span><span class="p">:</span> <span class="kc">self</span><span class="p">.</span><span class="n">view</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="n">label</span><span class="p">.</span><span class="n">attributedText</span> <span class="p">=</span> <span class="n">attrString</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Let’s now implement <code>handleInteractionWithCheckingType</code> method on <code>UILabel</code>. First, some sanity checking — if label has no text/attributedText, there’s nothing to do here.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">extension</span> <span class="nc">UILabel</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span> <span class="nf">handleInteractionWithCheckingType</span><span class="p">(</span><span class="kc">_</span> <span class="n">match</span><span class="p">:</span> <span class="n">NSTextCheckingResult</span><span class="p">,</span> <span class="n">inside</span> <span class="n">view</span><span class="p">:</span> <span class="n">UIView</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">guard</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">string</span> <span class="p">=</span> <span class="kc">self</span><span class="p">.</span><span class="n">text</span><span class="p">,</span> <span class="n">string</span><span class="p">.</span><span class="bp">count</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">attrString</span> <span class="p">=</span> <span class="kc">self</span><span class="p">.</span><span class="n">attributedText</span> </span></span><span class="line"><span class="cl"> <span class="k">else</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="o">--</span><span class="p">[</span><span class="n">CONTINUES</span> <span class="n">BELOW</span><span class="p">]</span><span class="o">--</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Now, re-run the matching and — very important — allow user-interaction on the label instance.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">matchRange</span> <span class="p">=</span> <span class="n">match</span><span class="p">.</span><span class="n">range</span> </span></span><span class="line"><span class="cl"><span class="k">guard</span> <span class="kd">let</span> <span class="nv">range</span> <span class="p">=</span> <span class="nb">Range</span><span class="p">(</span><span class="n">matchRange</span><span class="p">,</span> <span class="k">in</span><span class="p">:</span> <span class="n">string</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">term</span> <span class="p">=</span> <span class="nb">String</span><span class="p">(</span><span class="n">string</span><span class="p">[</span><span class="n">range</span><span class="p">])</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">isUserInteractionEnabled</span> <span class="p">=</span> <span class="kc">true</span> </span></span></code></pre></div><p>If we have a match, switch-over the <code>CheckingType</code> value and implement behavior you need. Here I’m</p> <ul> <li>looking if the match is <code>.link</code></li> <li>making sure that matched term is actually a valid URL</li> <li>checking if the person has tapped on the term itself</li> </ul> <p>Only then we can present the URL in some appropriate way for the given app.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="k">switch</span> <span class="n">match</span><span class="p">.</span><span class="n">resultType</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="p">.</span><span class="n">link</span><span class="p">:</span> </span></span><span class="line"><span class="cl"> <span class="c1">// handle tap on links</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">tapGR</span> <span class="p">=</span> <span class="n">UITapGestureRecognizer</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="p">[</span><span class="kr">weak</span> <span class="n">view</span><span class="p">,</span> <span class="kr">weak</span> <span class="kc">self</span><span class="p">]</span> <span class="n">gr</span> <span class="k">in</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">guard</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">view</span><span class="p">,</span> <span class="kd">let</span> <span class="nv">self</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">url</span> <span class="p">=</span> <span class="n">URL</span><span class="p">(</span><span class="n">string</span><span class="p">:</span> <span class="n">term</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="kc">self</span><span class="p">.</span><span class="n">hasInteractedWith</span><span class="p">(</span><span class="n">gr</span><span class="p">,</span> <span class="n">over</span><span class="p">:</span> <span class="n">attrString</span><span class="p">,</span> <span class="n">atRange</span><span class="p">:</span> <span class="n">matchRange</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// Show WKWebView with this URL</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="n">addGestureRecognizer</span><span class="p">(</span><span class="n">tapGR</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">default</span><span class="p">:</span> </span></span><span class="line"><span class="cl"> <span class="k">break</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Finally, let’s see how to detect if the tap happened over the link and not some other text — let’s implement <code>hasInteractedWith</code> method.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">extension</span> <span class="nc">UILabel</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span> <span class="nf">hasInteractedWith</span><span class="p">(</span><span class="kc">_</span> <span class="n">gr</span><span class="p">:</span> <span class="n">UIGestureRecognizer</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">over</span> <span class="n">attrString</span><span class="p">:</span> <span class="n">NSAttributedString</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">atRange</span> <span class="n">matchRange</span><span class="p">:</span> <span class="n">NSRange</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="nb">Bool</span> </span></span><span class="line"><span class="cl"> <span class="p">{</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="o">--</span><span class="p">[</span><span class="n">CONTINUES</span> <span class="n">BELOW</span><span class="p">]</span><span class="o">--</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p><code>UITapGestureRecognizer</code>’s <a href="https://developer.apple.com/documentation/uikit/uigesturerecognizer/1624219-location">location(in:)</a> method gives us <code>CGPoint</code> inside the label’s bounds. <a href="https://developer.apple.com/documentation/appkit/textkit">TextKit framework</a> has all the tools we need to match that point with actual character in the label’s text string.</p> <p>First, instantiate required TextKit objects and copy-over <code>UILabel</code> properties that influence its rendered size to the <code>NSTextContainer</code>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">layoutManager</span> <span class="p">=</span> <span class="n">NSLayoutManager</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">textContainer</span> <span class="p">=</span> <span class="n">NSTextContainer</span><span class="p">(</span><span class="n">size</span><span class="p">:</span> <span class="p">.</span><span class="n">zero</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">textStorage</span> <span class="p">=</span> <span class="n">NSTextStorage</span><span class="p">.</span><span class="kd">init</span><span class="p">(</span><span class="n">attributedString</span><span class="p">:</span> <span class="n">attrString</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">layoutManager</span><span class="p">.</span><span class="n">addTextContainer</span><span class="p">(</span><span class="n">textContainer</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="n">textStorage</span><span class="p">.</span><span class="n">addLayoutManager</span><span class="p">(</span><span class="n">layoutManager</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// configure textContainer for the label</span> </span></span><span class="line"><span class="cl"><span class="n">textContainer</span><span class="p">.</span><span class="n">lineFragmentPadding</span> <span class="p">=</span> <span class="mf">0.0</span> </span></span><span class="line"><span class="cl"><span class="n">textContainer</span><span class="p">.</span><span class="n">lineBreakMode</span> <span class="p">=</span> <span class="kc">self</span><span class="p">.</span><span class="n">lineBreakMode</span> </span></span><span class="line"><span class="cl"><span class="n">textContainer</span><span class="p">.</span><span class="n">maximumNumberOfLines</span> <span class="p">=</span> <span class="kc">self</span><span class="p">.</span><span class="n">numberOfLines</span> </span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">labelSize</span> <span class="p">=</span> <span class="kc">self</span><span class="p">.</span><span class="n">bounds</span><span class="p">.</span><span class="n">size</span> </span></span><span class="line"><span class="cl"><span class="n">textContainer</span><span class="p">.</span><span class="n">size</span> <span class="p">=</span> <span class="n">labelSize</span> </span></span></code></pre></div><p>Now, figure out the character index in the string that corresponds to the touch point:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">loc</span> <span class="p">=</span> <span class="n">gr</span><span class="p">.</span><span class="n">location</span><span class="p">(</span><span class="k">in</span><span class="p">:</span> <span class="kc">self</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">textBoundingBox</span> <span class="p">=</span> <span class="n">layoutManager</span><span class="p">.</span><span class="n">usedRect</span><span class="p">(</span><span class="k">for</span><span class="p">:</span> <span class="n">textContainer</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">textContainerOffset</span> <span class="p">=</span> <span class="n">CGPointMake</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="n">labelSize</span><span class="p">.</span><span class="n">width</span> <span class="o">-</span> <span class="n">textBoundingBox</span><span class="p">.</span><span class="n">size</span><span class="p">.</span><span class="n">width</span><span class="p">)</span> <span class="o">*</span> <span class="mf">0.5</span> <span class="o">-</span> <span class="n">textBoundingBox</span><span class="p">.</span><span class="n">origin</span><span class="p">.</span><span class="n">x</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="n">labelSize</span><span class="p">.</span><span class="n">height</span> <span class="o">-</span> <span class="n">textBoundingBox</span><span class="p">.</span><span class="n">size</span><span class="p">.</span><span class="n">height</span><span class="p">)</span> <span class="o">*</span> <span class="mf">0.5</span> <span class="o">-</span> <span class="n">textBoundingBox</span><span class="p">.</span><span class="n">origin</span><span class="p">.</span><span class="n">y</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">locationOfTouchInTextContainer</span> <span class="p">=</span> <span class="n">CGPointMake</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">loc</span><span class="p">.</span><span class="n">x</span> <span class="o">-</span> <span class="n">textContainerOffset</span><span class="p">.</span><span class="n">x</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">loc</span><span class="p">.</span><span class="n">y</span> <span class="o">-</span> <span class="n">textContainerOffset</span><span class="p">.</span><span class="n">y</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">indexOfCharacter</span> <span class="p">=</span> <span class="n">layoutManager</span><span class="p">.</span><span class="n">characterIndex</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span><span class="p">:</span> <span class="n">locationOfTouchInTextContainer</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="k">in</span><span class="p">:</span> <span class="n">textContainer</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">fractionOfDistanceBetweenInsertionPoints</span><span class="p">:</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span></code></pre></div><p>Finally, return <code>true</code> if the indexOfCharacter falls into matched term’s range:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="k">return</span> <span class="n">matchRange</span><span class="p">.</span><span class="bp">contains</span><span class="p">(</span><span class="n">indexOfCharacter</span><span class="p">)</span> </span></span></code></pre></div><p>The main fact here is that calculations above are done in the gesture-recogniser’s tap handler. That means all of UIKit’s rendering is complete and there can be no errors in pinpointing where the interaction happened.</p> <p>Add these 3 methods into your project and you’ll have the ability to trigger detection of any <code>CheckingType</code> available in iOS SDK.</p> Using NSFetchedResultsController’s diffable snapshot with UICollectionView’s diffable data source https://aplus.rs/2022/diffable-data-source-core-data-frc/ Wed, 13 Jul 2022 10:30:00 +0000 https://aplus.rs/2022/diffable-data-source-core-data-frc/ <p>Last several weeks, we (<a href="https://radianttap.com">Radiant Tap</a>) are working on a major app redesign for one of our clients. App is super-data-intensive, combining multiple different data sources into one complex bi-directional scrollable view. Here’s some samples of the design:</p> <p><img src="https://aplus.rs/images/2022/diffable-core-data/layout-01.png" alt=""> <img src="https://aplus.rs/images/2022/diffable-core-data/layout-02.png" alt=""></p> <p>Important bits what’s in there:</p> <ol> <li>First section is the horizontally scrollable set of ads, which are fetched from JSON file. Rarely changes but it can.</li> <li>All the other sections are using data from Core Data store where some of them change very often, every 3-5s.</li> <li>Second section is horizontally scrollable set of <em>active sports</em> driven by <code>NSFetchedResultsController</code> (FRC for short) which has no <a href="https://developer.apple.com/documentation/coredata/nsfetchedresultscontroller/1622285-sectionnamekeypath">sectionNameKeyPath</a> set.</li> <li>Third section is horizontally scrollable set of <em>matches</em> (events) driven by another custom FRC, again with <code>sectionNameKeyPath</code> not set.</li> <li>Then we have another FRC loading non-live matches with <code>sectionNameKeyPath</code> set to the sport’s <code>name</code> attribute (so the matches are grouped by sport). There can be 0 or more of these sections.</li> <li>This is followed by another FRC with live-matches, again grouped per sport. There can also be 0 or more of these sections.</li> </ol> <p>It’s very complex so far but it does not stop there since there’s quite a bit of additional state we need to maintain on top of displaying incoming data. So we have 3 semantically different UI-update triggers:</p> <ul> <li>New ads (dis)appearing in the first section.</li> <li>Sport/Event changes in Core Data’s context.</li> <li>Auxiliary state changes thus even though actual underlying Core Data source did not change, the UICV still needs an update.</li> </ul> <p>I’m describing all this to set the stage, to make it clearer why we did things the way we did them and why it did not always work reliably.<br> And what eventually did.</p> <h2 id="modeling-disparate-data-source-into-one-snapshot">Modeling disparate data source into one snapshot</h2> <p>Natural choice here in modern iOS is to use UIKit’s <a href="https://developer.apple.com/documentation/uikit/uicollectionviewcompositionallayout">compositional layout</a>. Most examples you find online are using simple types like <code>Int</code>, <code>String</code> or <code>UUID</code> as <em>SectionIdentifierType</em> or <em>ItemIdentifierType</em> in the <a href="https://developer.apple.com/documentation/uikit/nsdiffabledatasourcesnapshot">NSDiffableDataSourceSnapshot</a>.</p> <p>That is not really an option in the complex case like what we have here. You need much more specificity and customisation. <code>enum</code> with associated values is perfect tool for that and here’s (simplified) set of cases we ended up using for the <code>UICollectionViewDiffableDataSource&lt;SectionItem, GridItem&gt;</code> acting as UICV’s data source:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">enum</span> <span class="nc">SectionItem</span><span class="p">:</span> <span class="nb">Hashable</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">banners</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">sportspicker</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">featuredMatches</span><span class="p">(</span><span class="n">events</span><span class="p">:</span> <span class="p">[</span><span class="n">Event</span><span class="p">])</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">topstandardsport</span><span class="p">(</span><span class="n">Sport</span><span class="p">,</span> <span class="n">events</span><span class="p">:</span> <span class="p">[</span><span class="n">Event</span><span class="p">])</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">toplivesport</span><span class="p">(</span><span class="n">Sport</span><span class="p">,</span> <span class="n">events</span><span class="p">:</span> <span class="p">[</span><span class="n">Event</span><span class="p">])</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">enum</span> <span class="nc">GridItem</span><span class="p">:</span> <span class="nb">Hashable</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">banner</span><span class="p">(</span><span class="n">ad</span><span class="p">:</span> <span class="n">Ad</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">sport</span><span class="p">(</span><span class="n">Sport</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">featuredMatch</span><span class="p">(</span><span class="n">Event</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">featuredSelection</span><span class="p">(</span><span class="n">Selection</span><span class="p">,</span> <span class="n">isAddedToBetslip</span><span class="p">:</span> <span class="nb">Bool</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">eventHeader</span><span class="p">(</span><span class="n">Event</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">marketHeader</span><span class="p">(</span><span class="n">Game</span><span class="p">,</span> <span class="n">sectionIndex</span><span class="p">:</span> <span class="nb">Int</span><span class="p">,</span> <span class="n">rowIndex</span><span class="p">:</span> <span class="nb">Int</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">selection</span><span class="p">(</span><span class="n">Selection</span><span class="p">,</span> <span class="n">isAddedToBetslip</span><span class="p">:</span> <span class="nb">Bool</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">message</span><span class="p">(</span><span class="n">sectionIndex</span><span class="p">:</span> <span class="nb">Int</span><span class="p">,</span> <span class="n">rowIndex</span><span class="p">:</span> <span class="nb">Int</span><span class="p">,</span> <span class="n">message</span><span class="p">:</span> <span class="nb">String</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">locked</span><span class="p">(</span><span class="n">sectionIndex</span><span class="p">:</span> <span class="nb">Int</span><span class="p">,</span> <span class="n">rowIndex</span><span class="p">:</span> <span class="nb">Int</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>All these types — <code>Sport</code>, <code>Event</code>, <code>Game</code>, <code>Selection</code> — are Core Data entities thus all are subclasses of <code>NSManagedObject</code>. <code>Ad</code> is a struct. The accompanying snapshot uses the same identifiers, which are typealias-ed for clearer code:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">typealias</span> <span class="n">GridSource</span> <span class="p">=</span> <span class="n">UICollectionViewDiffableDataSource</span><span class="p">&lt;</span><span class="n">SectionItem</span><span class="p">,</span> <span class="n">GridItem</span><span class="p">&gt;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">typealias</span> <span class="n">Snapshot</span> <span class="p">=</span> <span class="n">NSDiffableDataSourceSnapshot</span><span class="p">&lt;</span><span class="n">SectionItem</span><span class="p">,</span> <span class="n">GridItem</span><span class="p">&gt;</span> </span></span></code></pre></div><p>Why is this good? Having the full objects as part of the diffable data source makes vending cells and building the layout to house them very straightforward. For example, cell vending:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">cell</span><span class="p">(</span><span class="n">collectionView</span><span class="p">:</span> <span class="n">UICollectionView</span><span class="p">,</span> <span class="n">indexPath</span><span class="p">:</span> <span class="n">IndexPath</span><span class="p">,</span> <span class="n">item</span><span class="p">:</span> <span class="n">GridItem</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="n">UICollectionViewCell</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">switch</span> <span class="n">item</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="p">.</span><span class="n">banner</span><span class="p">(</span><span class="kd">let</span> <span class="nv">ad</span><span class="p">):</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">cell</span><span class="p">:</span> <span class="n">AdCell</span> <span class="p">=</span> <span class="n">collectionView</span><span class="p">.</span><span class="n">dequeueReusableCell</span><span class="p">(</span><span class="n">forIndexPath</span><span class="p">:</span> <span class="n">indexPath</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">cell</span><span class="p">.</span><span class="n">populate</span><span class="p">(</span><span class="n">with</span><span class="p">:</span> <span class="n">ad</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">cell</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="p">.</span><span class="n">sport</span><span class="p">(</span><span class="kd">let</span> <span class="nv">sport</span><span class="p">):</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">cell</span><span class="p">:</span> <span class="n">SportPickerCell</span> <span class="p">=</span> <span class="n">collectionView</span><span class="p">.</span><span class="n">dequeueReusableCell</span><span class="p">(</span><span class="n">forIndexPath</span><span class="p">:</span> <span class="n">indexPath</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">cell</span><span class="p">.</span><span class="n">populate</span><span class="p">(</span><span class="n">with</span><span class="p">:</span> <span class="n">sport</span><span class="p">.</span><span class="n">icon</span><span class="p">,</span> <span class="n">caption</span><span class="p">:</span> <span class="n">sport</span><span class="p">.</span><span class="n">name</span><span class="p">,</span> <span class="n">isSelected</span><span class="p">:</span> <span class="kc">false</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">cell</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="p">.</span><span class="n">featuredMatch</span><span class="p">(</span><span class="kd">let</span> <span class="nv">event</span><span class="p">):</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">cell</span><span class="p">:</span> <span class="n">FeaturedEventCell</span> <span class="p">=</span> <span class="n">collectionView</span><span class="p">.</span><span class="n">dequeueReusableCell</span><span class="p">(</span><span class="n">forIndexPath</span><span class="p">:</span> <span class="n">indexPath</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">cell</span><span class="p">.</span><span class="n">populate</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">with</span><span class="p">:</span> <span class="n">event</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">isStreaming</span><span class="p">:</span> <span class="n">bettingContentManager</span><span class="p">.</span><span class="n">isEventStreamingOnTV</span><span class="p">(</span><span class="n">event</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">cell</span> </span></span></code></pre></div><p>With all this setup, now we need to configure UICV to use <code>GridSource</code> as data source, setup cell vending method and…build a snapshot:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">gridSource</span> <span class="p">=</span> <span class="n">GridSource</span><span class="p">(</span><span class="n">collectionView</span><span class="p">:</span> <span class="n">cv</span><span class="p">,</span> <span class="n">cellProvider</span><span class="p">:</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="p">[</span><span class="kr">unowned</span> <span class="kc">self</span><span class="p">]</span> <span class="n">cv</span><span class="p">,</span> <span class="n">indexPath</span><span class="p">,</span> <span class="n">gridItem</span> <span class="k">in</span> </span></span><span class="line"><span class="cl"> <span class="kc">self</span><span class="p">.</span><span class="n">cell</span><span class="p">(</span><span class="n">collectionView</span><span class="p">:</span> <span class="n">cv</span><span class="p">,</span> <span class="n">indexPath</span><span class="p">:</span> <span class="n">indexPath</span><span class="p">,</span> <span class="n">item</span><span class="p">:</span> <span class="n">gridItem</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">})</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">snapshot</span><span class="p">(</span><span class="n">animated</span><span class="p">:</span> <span class="kc">false</span><span class="p">)</span> </span></span></code></pre></div><p>So what about snapshot?</p> <p>The simplest approach here we tried first:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">snapshot</span><span class="p">(</span><span class="n">animated</span><span class="p">:</span> <span class="nb">Bool</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="n">collectionView</span> <span class="p">==</span> <span class="kc">nil</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">snapshot</span> <span class="p">=</span> <span class="n">Snapshot</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">sectionIndex</span><span class="p">:</span> <span class="nb">Int</span> <span class="p">=</span> <span class="mi">0</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// ads</span> </span></span><span class="line"><span class="cl"> <span class="n">snapshot</span> <span class="p">=</span> <span class="n">populateSnapshotWithAds</span><span class="p">(</span><span class="n">snapshot</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">sectionIndex</span> <span class="o">+=</span> <span class="mi">1</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// sports picker</span> </span></span><span class="line"><span class="cl"> <span class="n">snapshot</span> <span class="p">=</span> <span class="n">populateSnapshotWithSports</span><span class="p">(</span><span class="n">snapshot</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">sectionIndex</span> <span class="o">+=</span> <span class="mi">1</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// featured matches</span> </span></span><span class="line"><span class="cl"> <span class="n">snapshot</span> <span class="p">=</span> <span class="n">populateSnapshotWithFeaturedMatches</span><span class="p">(</span><span class="n">snapshot</span><span class="p">,</span> <span class="n">sectionIndex</span><span class="p">:</span> <span class="n">sectionIndex</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">sectionIndex</span> <span class="o">+=</span> <span class="mi">1</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// top standard matches (events)</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="n">section</span> <span class="k">in</span> <span class="n">frcTopStandardEvents</span><span class="p">?.</span><span class="n">sections</span> <span class="p">??</span> <span class="p">[]</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">snapshot</span> <span class="p">=</span> <span class="n">populateSnapshot</span><span class="p">(</span><span class="n">snapshot</span><span class="p">,</span> <span class="n">withTopStandardSection</span><span class="p">:</span> <span class="n">section</span><span class="p">,</span> <span class="n">sectionIndex</span><span class="p">:</span> <span class="n">sectionIndex</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">sectionIndex</span> <span class="o">+=</span> <span class="mi">1</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// top live matches (events)</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="n">section</span> <span class="k">in</span> <span class="n">frcTopLiveEvents</span><span class="p">?.</span><span class="n">sections</span> <span class="p">??</span> <span class="p">[]</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">snapshot</span> <span class="p">=</span> <span class="n">populateSnapshot</span><span class="p">(</span><span class="n">snapshot</span><span class="p">,</span> <span class="n">withTopLiveSection</span><span class="p">:</span> <span class="n">section</span><span class="p">,</span> <span class="n">sectionIndex</span><span class="p">:</span> <span class="n">sectionIndex</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">sectionIndex</span> <span class="o">+=</span> <span class="mi">1</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">gridSource</span><span class="p">.</span><span class="n">apply</span><span class="p">(</span><span class="n">snapshot</span><span class="p">,</span> <span class="n">animatingDifferences</span><span class="p">:</span> <span class="n">animated</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><ol> <li>Watch for changes in the data / state. When they happen, call <code>snapshot(animated: true)</code></li> <li>That would build entire new full snapshot, for all sections/items.</li> <li>For Core Data stuff, we simply iterate over each FRC’s <code>sections</code> and <code>fetchedObjects</code> arrays and convert them into <code>SectionItem</code> and <code>GridItem</code> instances, as needed</li> <li>Call <code>applySnapshot</code> and let UIKit figure-out the differences and render them.</li> </ol> <p>All changes including those from Core Data are simple to observe. For Core Data, laziest possible approach:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">extension</span> <span class="nc">HomeDataSource</span><span class="p">:</span> <span class="n">NSFetchedResultsControllerDelegate</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span> <span class="nf">controllerDidChangeContent</span><span class="p">(</span><span class="kc">_</span> <span class="n">controller</span><span class="p">:</span> <span class="n">NSFetchedResultsController</span><span class="p">&lt;</span><span class="n">NSFetchRequestResult</span><span class="p">&gt;)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">snapshot</span><span class="p">(</span><span class="n">animated</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Looks super easy, right? This…actually works very well and in limited testing — “works on my dev machine” kind of testing — it yielded no issues, no crashes.</p> <h2 id="core-data-crashes-and-why-they-happen">Core Data crashes and why they happen</h2> <p>Shortly after entering limited internal QA, we started receiving multiple crash reports per tester. They looked like this:</p> <p><img src="https://aplus.rs/images/2022/diffable-core-data/frc-crash-01.png" alt=""></p> <p>I knew from past experience what a crash in those <em>UICollectionView.m</em> lines meant; the return of dreaded <code>NSInternalInconsistencyException</code> crash, better known as <code>attempt to delete item X1 from section Y which only contains X2 items before the update</code> and similar exception messages.<br> DiffableDataSource is supposed to deliver us from it thus disheartening to see it roaring back like this.</p> <p>There’s not much good posts on this topic to be found. <a href="https://www.avanderlee.com/swift/diffable-data-sources-core-data/">One real gem</a> is from Antoine van der Lee who dived deep into this and figured out the reason for the crashes:</p> <blockquote> <p>During the transition from an old snapshot to a new snapshot, it could be that the data source is asking for a cell for a given index path that not yet exists in the collection of fetched objects.<br> The fetched objects in an NSFetchedResultsController are updated after the snapshot is applied but a cell can be asked before that transition finishes. <br> This mainly happens when a transition is animated.</p></blockquote> <p>Entire post is worth a detailed and careful read and boils down to this:</p> <ul> <li>You should use that new FRC delegate method that delivers the snapshot of changes FRC has already digested.</li> <li>Do not use FRC’s <code>fetchedObjects</code> when building your diffable data model since it’s populated after FRC reports snapshot changes.</li> </ul> <p>Second point was confirmed once I naively tried the easy way out: keep my code as it is but use the newer delegate method:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">extension</span> <span class="nc">HomeDataSource</span><span class="p">:</span> <span class="n">NSFetchedResultsControllerDelegate</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span> <span class="nf">controller</span><span class="p">(</span><span class="kc">_</span> <span class="n">controller</span><span class="p">:</span> <span class="n">NSFetchedResultsController</span><span class="p">&lt;</span><span class="n">NSFetchRequestResult</span><span class="p">&gt;,</span> <span class="n">didChangeContentWith</span> <span class="n">newSnapshot</span><span class="p">:</span> <span class="n">NSDiffableDataSourceSnapshotReference</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">animateChanges</span> <span class="p">=</span> <span class="n">collectionView</span><span class="p">.</span><span class="n">numberOfSections</span> <span class="p">??</span> <span class="mi">0</span> <span class="o">&gt;</span> <span class="mi">0</span> </span></span><span class="line"><span class="cl"> <span class="n">snapshot</span><span class="p">(</span><span class="n">animated</span><span class="p">:</span> <span class="n">animateChanges</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>This yielded the same intermittent crashes:</p> <p><img src="https://aplus.rs/images/2022/diffable-core-data/frc-crash-02.png" alt=""></p> <p>Using FRC-provided sections and objects <em>almost</em> works. But in some edge cases it leads to inconsistent internal state in the UICV data source thus causing a crash. There’s no solution for it with the code approach above; <em>it needs a complete overhaul</em>.</p> <hr> <p>The thing to understand here is that FRC does not have layout or cells to worry about so it’s free to update its <code>sections</code> and <code>fetchedObjects</code> properties whenever it sees fit. You can’t depend on them to be ready in time for your snapshot builder accessing them. You are opening yourself to race conditions which rarely happen but eventually for someone they do. <br> In our testing it turned out that we can have 100s or even 1000s of successful snapshot driven updates before encountering a crashing condition.</p> <p>FRC internally works out the inserted/updated/deleted set of changes and the delegate method reports that to you but there are apparently no guarantees that you can use <code>sections</code> and <code>fetchedObjects</code> as soon as you get the delegate call. When you think about it — there must be some good reason you are given a set of <code>NSManagedObjectID</code>s and not ready-to-use <code>NSManagedObject</code> instances.</p> <p>Hence why Antoine’s cell-vending method uses <code>NSManagedObjectID</code> as input and then fetches full object from the <code>NSManagedObjectContext</code> when it needs to populate the cell:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">diffableDataSource</span> <span class="p">=</span> <span class="n">UICollectionViewDiffableDataSource</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">NSManagedObjectID</span><span class="p">&gt;(</span><span class="n">collectionView</span><span class="p">:</span> <span class="n">collectionView</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="p">(</span><span class="n">collectionView</span><span class="p">,</span> <span class="n">indexPath</span><span class="p">,</span> <span class="n">objectID</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="n">UICollectionViewCell</span><span class="p">?</span> <span class="k">in</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">guard</span> <span class="kd">let</span> <span class="nv">object</span> <span class="p">=</span> <span class="k">try</span><span class="p">?</span> <span class="n">managedObjectContext</span><span class="p">.</span><span class="n">existingObject</span><span class="p">(</span><span class="n">with</span><span class="p">:</span> <span class="n">objectID</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="bp">preconditionFailure</span><span class="p">(</span><span class="s">&#34;Managed object should be available&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">cell</span> <span class="p">=</span> <span class="p">...</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>We need to change our Section/Grid enum to use objectIDs and hope for the best. But first we need to see what exactly is Core Data giving us.</p> <h2 id="demystifying-core-data-snapshots">Demystifying Core Data snapshots</h2> <p>The snapshot’s type used by Core Data delegate method is <code>NSDiffableDataSourceSnapshotReference</code> and <a href="https://developer.apple.com/documentation/uikit/nsdiffabledatasourcesnapshotreference/">Apple says</a> that you should use Swift-ified counterparts:</p> <blockquote> <p>Avoid using this type in Swift code.<br> Only use this type to bridge from Objective-C code to Swift code by typecasting from a snapshot reference to a snapshot:<br> <code>let snapshot = snapshotReference as NSDiffableDataSourceSnapshot&lt;Int, UUID&gt;</code></p></blockquote> <p>In the SwiftLee post, it is type-casted into <code>...&lt;Int, NSManagedObjectID&gt;</code>. I found both of these to be wrong. Maybe something has changed since 2020 but the true type in my testing turns out to be:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">NSDiffableDataSourceSnapshot</span><span class="p">&lt;</span><span class="nb">String</span><span class="p">,</span> <span class="n">NSManagedObjectID</span><span class="p">&gt;</span> </span></span></code></pre></div><p>where <code>String</code> value of <em>sectionIdentifier</em> is:</p> <ol> <li>Some (random?) string when FRC’s <code>sectionNameKeyPath</code> is <code>nil</code>.</li> <li>Attribute value returned as <code>sectionNameKeyPath</code> when key-path is not <code>nil</code>.</li> </ol> <p>Here are examples logged in Xcode console:</p> <pre tabindex="0"><code>[ &#34;d69e6c783a242772974cfc99189691b88e9d37c3 : x-coredata://E43CC0BD-13BD-40F2-B354-BB660E18C80A/Sport/p27&#34;, &#34;d69e6c783a242772974cfc99189691b88e9d37c3 : x-coredata://E43CC0BD-13BD-40F2-B354-BB660E18C80A/Sport/p23&#34; ] ... [ &#34;Football : x-coredata://E43CC0BD-13BD-40F2-B354-BB660E18C80A/Event/p735&#34;, &#34;Football : x-coredata://E43CC0BD-13BD-40F2-B354-BB660E18C80A/Event/p747&#34;, &#34;Football : x-coredata://E43CC0BD-13BD-40F2-B354-BB660E18C80A/Event/p752&#34;, &#34;Basketball : x-coredata://E43CC0BD-13BD-40F2-B354-BB660E18C80A/Event/p745&#34;] </code></pre><p>Now that we know what we are working with, let’s dig in.</p> <h2 id="proper-marriage-of-frc-snapshots-with-uicv-diffable-data-source">Proper marriage of FRC snapshots with UICV diffable data source</h2> <p>First, we update the diffable data model to:</p> <ol> <li>use <code>NSManagedObjectID</code> instead of full Core Data entities</li> <li>use <code>String</code> sectionIdentifier we are receiving instead of <code>Int</code> sectionIndex (these have some internal use, specific to our layout)</li> </ol> <p>With some other changes specific to our use-case, this was the end result:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">enum</span> <span class="nc">SectionItem</span><span class="p">:</span> <span class="nb">Hashable</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">banners</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">sportspicker</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">featuredMatches</span><span class="p">(</span><span class="n">events</span><span class="p">:</span> <span class="p">[</span><span class="n">NSManagedObjectID</span><span class="p">])</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">topstandardsport</span><span class="p">(</span><span class="n">sportId</span><span class="p">:</span> <span class="nb">Int64</span><span class="p">,</span> <span class="n">events</span><span class="p">:</span> <span class="p">[</span><span class="n">NSManagedObjectID</span><span class="p">])</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">toplivesport</span><span class="p">(</span><span class="n">sportId</span><span class="p">:</span> <span class="nb">Int64</span><span class="p">,</span> <span class="n">events</span><span class="p">:</span> <span class="p">[</span><span class="n">NSManagedObjectID</span><span class="p">])</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">enum</span> <span class="nc">GridItem</span><span class="p">:</span> <span class="nb">Hashable</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">banner</span><span class="p">(</span><span class="n">ad</span><span class="p">:</span> <span class="n">Ad</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">sport</span><span class="p">(</span><span class="n">NSManagedObjectID</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">featuredMatch</span><span class="p">(</span><span class="n">NSManagedObjectID</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">featuredSelection</span><span class="p">(</span><span class="n">NSManagedObjectID</span><span class="p">,</span> <span class="n">isAddedToBetslip</span><span class="p">:</span> <span class="nb">Bool</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">eventHeader</span><span class="p">(</span><span class="n">NSManagedObjectID</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">marketHeader</span><span class="p">(</span><span class="n">NSManagedObjectID</span><span class="p">,</span> <span class="n">sectionIdentifier</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span> <span class="n">rowIndex</span><span class="p">:</span> <span class="nb">Int</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">selection</span><span class="p">(</span><span class="n">NSManagedObjectID</span><span class="p">,</span> <span class="n">isAddedToBetslip</span><span class="p">:</span> <span class="nb">Bool</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">message</span><span class="p">(</span><span class="n">sectionIdentifier</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span> <span class="n">rowIndex</span><span class="p">:</span> <span class="nb">Int</span><span class="p">,</span> <span class="n">message</span><span class="p">:</span> <span class="nb">String</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">locked</span><span class="p">(</span><span class="n">sectionIdentifier</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span> <span class="n">rowIndex</span><span class="p">:</span> <span class="nb">Int</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Next — apply all recommendations from SwiftLee post, namely:</p> <ul> <li>do not access FRC’s <code>fetchedObjects</code> directly</li> <li>instead setup each FRC, set its <code>delegate</code> and only then call <code>performFetch()</code></li> <li>in the delegate method, call out <em>your</em> snapshot builder passing the snapshot received from Core Data</li> <li>convert Core Data snapshot into your own and apply differences</li> </ul> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">controller</span><span class="p">(</span><span class="kc">_</span> <span class="n">frc</span><span class="p">:</span> <span class="n">NSFetchedResultsController</span><span class="p">&lt;</span><span class="n">NSFetchRequestResult</span><span class="p">&gt;,</span> <span class="n">didChangeContentWith</span> <span class="n">snapshot</span><span class="p">:</span> <span class="n">NSDiffableDataSourceSnapshotReference</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">cds</span> <span class="p">=</span> <span class="n">snapshot</span> <span class="k">as</span> <span class="n">NSDiffableDataSourceSnapshot</span><span class="p">&lt;</span><span class="nb">String</span><span class="p">,</span> <span class="n">NSManagedObjectID</span><span class="p">&gt;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="n">frc</span> <span class="p">==</span> <span class="n">frcSports</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">snapshotSports</span><span class="p">(</span><span class="n">usingCoreDataSnapshot</span><span class="p">:</span> <span class="n">cds</span><span class="p">,</span> <span class="n">animated</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="n">frc</span> <span class="p">==</span> <span class="n">frcFeaturedEvents</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">snapshotFeaturedMatches</span><span class="p">(</span><span class="n">usingCoreDataSnapshot</span><span class="p">:</span> <span class="n">cds</span><span class="p">,</span> <span class="n">animated</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="n">frc</span> <span class="p">==</span> <span class="n">frcTopStandardEvents</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">snapshotTopStandardEvents</span><span class="p">(</span><span class="n">usingCoreDataSnapshot</span><span class="p">:</span> <span class="n">cds</span><span class="p">,</span> <span class="n">animated</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="n">frc</span> <span class="p">==</span> <span class="n">frcTopLiveEvents</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">snapshotTopLiveEvents</span><span class="p">(</span><span class="n">usingCoreDataSnapshot</span><span class="p">:</span> <span class="n">cds</span><span class="p">,</span> <span class="n">animated</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Key thing we found here is that Core Data snapshot you receive contains entire set of objects that satisfy the FRC’s predicate. It’s not some delta of few changed objects — it’s all of them. That is awesome since we don’t have to deal with merging of existing data and received delta.</p> <p>Since we have multiple FRCs and some other additional data sources, we can’t just blindly call <code>applySnapshot(...)</code>. Doing that will essentially remove all data and display this one section you just received. Correct way is to take full existing snapshot of your data source, delete the relevant section and then replace it with data from received Core Data snapshot.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">snapshotFeaturedMatches</span><span class="p">(</span><span class="n">usingCoreDataSnapshot</span> <span class="n">cds</span><span class="p">:</span> <span class="n">NSDiffableDataSourceSnapshot</span><span class="p">&lt;</span><span class="nb">String</span><span class="p">,</span> <span class="n">NSManagedObjectID</span><span class="p">&gt;,</span> <span class="n">animated</span><span class="p">:</span> <span class="nb">Bool</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// start with existing snapshot</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">snapshot</span> <span class="p">=</span> <span class="n">gridSource</span><span class="p">.</span><span class="n">snapshot</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// delete corresponding section</span> </span></span><span class="line"><span class="cl"> <span class="n">snapshot</span><span class="p">.</span><span class="n">deleteSections</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="c1">//target .featuredMatches section here</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// this helps maintain correct order of UICV sections </span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">nextSection</span> <span class="p">=</span> <span class="n">snapshot</span><span class="p">.</span><span class="n">sectionIdentifiers</span><span class="p">.</span><span class="bp">first</span><span class="p">(</span><span class="k">where</span><span class="p">:</span> <span class="p">{</span> <span class="p">...</span> <span class="p">})</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// convert CoreData snapshot into our snapshot</span> </span></span><span class="line"><span class="cl"> <span class="n">snapshot</span> <span class="p">=</span> <span class="n">populateSnapshotWithFeaturedMatches</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">snapshot</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">with</span><span class="p">:</span> <span class="n">cds</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">beforeSection</span><span class="p">:</span> <span class="n">nextSection</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// apply our updated snapshot</span> </span></span><span class="line"><span class="cl"> <span class="n">gridSource</span><span class="p">.</span><span class="n">apply</span><span class="p">(</span><span class="n">snapshot</span><span class="p">,</span> <span class="n">animatingDifferences</span><span class="p">:</span> <span class="n">animated</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>We also implemented some simple logic to maintain the order of sections. You can’t just <code>appendSections</code> each time you receive a set of data since your UI sections will constantly shuffle downwards.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">populateSnapshotWithFeaturedMatches</span><span class="p">(</span><span class="kc">_</span> <span class="n">currentSnapshot</span><span class="p">:</span> <span class="n">Snapshot</span><span class="p">,</span> <span class="n">with</span> <span class="n">cds</span><span class="p">:</span> <span class="n">NSDiffableDataSourceSnapshot</span><span class="p">&lt;</span><span class="nb">String</span><span class="p">,</span> <span class="n">NSManagedObjectID</span><span class="p">&gt;,</span> <span class="n">beforeSection</span><span class="p">:</span> <span class="n">SectionItem</span><span class="p">?</span> <span class="p">=</span> <span class="kc">nil</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="n">Snapshot</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">eventMOIDs</span> <span class="p">=</span> <span class="n">cds</span><span class="p">.</span><span class="n">itemIdentifiers</span> </span></span><span class="line"><span class="cl"> <span class="k">guard</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">sectionIdentifier</span> <span class="p">=</span> <span class="n">cds</span><span class="p">.</span><span class="n">sectionIdentifiers</span><span class="p">.</span><span class="bp">first</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">eventMOIDs</span><span class="p">.</span><span class="bp">count</span> <span class="o">&gt;</span> <span class="mi">0</span> </span></span><span class="line"><span class="cl"> <span class="k">else</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">currentSnapshot</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">snapshot</span> <span class="p">=</span> <span class="n">currentSnapshot</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">secitem</span> <span class="p">=</span> <span class="n">HomeDataSource</span><span class="p">.</span><span class="n">SectionItem</span><span class="p">.</span><span class="n">featuredMatches</span><span class="p">(</span><span class="n">events</span><span class="p">:</span> <span class="n">eventMOIDs</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="kd">let</span> <span class="nv">nextSection</span> <span class="p">=</span> <span class="n">beforeSection</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">snapshot</span><span class="p">.</span><span class="n">insertSections</span><span class="p">([</span><span class="n">secitem</span><span class="p">],</span> <span class="n">beforeSection</span><span class="p">:</span> <span class="n">nextSection</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">snapshot</span><span class="p">.</span><span class="n">appendSections</span><span class="p">([</span><span class="n">secitem</span><span class="p">])</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// Here convert data from cds into </span> </span></span><span class="line"><span class="cl"> <span class="c1">// GridItem instances added to secitem</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">snapshot</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>That’s&hellip;pretty much done. The rest of the work was carefully going over each FRC, check what its snapshot really is and build out the snapshot conversion into enums we are using.</p> <p>Cell vending changed too - we take the <code>NSManagedObjectID</code> and ask the <code>NSManagedObjectContext</code> for the full object:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">object</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;(</span><span class="n">with</span> <span class="n">moID</span><span class="p">:</span> <span class="n">NSManagedObjectID</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="n">T</span><span class="p">?</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">do</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">object</span> <span class="p">=</span> <span class="k">try</span> <span class="n">moc</span><span class="p">.</span><span class="n">existingObject</span><span class="p">(</span><span class="n">with</span><span class="p">:</span> <span class="n">moID</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">object</span> <span class="k">as</span><span class="p">?</span> <span class="n">T</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="k">catch</span> <span class="kd">let</span> <span class="nv">err</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="kc">nil</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">cell</span><span class="p">(</span><span class="n">collectionView</span><span class="p">:</span> <span class="n">UICollectionView</span><span class="p">,</span> <span class="n">indexPath</span><span class="p">:</span> <span class="n">IndexPath</span><span class="p">,</span> <span class="n">item</span><span class="p">:</span> <span class="n">GridItem</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="n">UICollectionViewCell</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">switch</span> <span class="n">item</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="p">...</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="p">.</span><span class="n">sport</span><span class="p">(</span><span class="kd">let</span> <span class="nv">moID</span><span class="p">):</span> </span></span><span class="line"><span class="cl"> <span class="k">guard</span> <span class="kd">let</span> <span class="nv">sport</span><span class="p">:</span> <span class="n">Sport</span> <span class="p">=</span> <span class="n">object</span><span class="p">(</span><span class="n">with</span><span class="p">:</span> <span class="n">moID</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="bp">preconditionFailure</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">cell</span><span class="p">:</span> <span class="n">SportPickerCell</span> <span class="p">=</span> <span class="n">collectionView</span><span class="p">.</span><span class="n">dequeueReusableCell</span><span class="p">(</span><span class="n">forIndexPath</span><span class="p">:</span> <span class="n">indexPath</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">cell</span><span class="p">.</span><span class="n">populate</span><span class="p">(</span><span class="n">with</span><span class="p">:</span> <span class="n">sport</span><span class="p">.</span><span class="n">icon</span><span class="p">,</span> <span class="n">caption</span><span class="p">:</span> <span class="n">sport</span><span class="p">.</span><span class="n">name</span><span class="p">,</span> <span class="n">isSelected</span><span class="p">:</span> <span class="kc">false</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">cell</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="p">...</span> </span></span></code></pre></div><hr> <p>As you can see, this is far from simple and easy. There’s lots of experimentation and verification to do. But the end result is a system that works really, really well.</p> <p>Before compositional layout, an UI like this would be multiple nested collection views with flow layout — we had that and maintaining it was super hard and was never 100% crash-free. Now, having everything in one single <code>UICollectionView</code> is freaking amazing from performance aspect: everything scroll super smooth, memory consumption is very low thanks to Core Data efficiency and so far it looks to be crash-free 🤞🏻.</p> <p>All of this is supported since iOS 13 so right now it’s pretty much guaranteed you can freely use it in just about any app.</p> Bitmask (bitwise) values in Core Data https://aplus.rs/2022/bitmask-attributes-in-core-data/ Sun, 03 Jul 2022 09:30:00 +0000 https://aplus.rs/2022/bitmask-attributes-in-core-data/ <p><a href="https://developer.apple.com/documentation/swift/optionset">OptionSet</a> is great tool of the Swift language. It’s fantastic for modelling multi-option values like: blog tags, various kinds of flags and markers etc.</p> <p>Here’s a typical (for me) use case: customise cell design where content can have label and icon, might have a badge, maybe disclosure indicator, some kind of background view with or without shadow etc. You can either create a separate UICVCell subclass for each combo or do them all at once and then supply a value with enabled options:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">struct</span> <span class="nc">LayoutFlags</span><span class="p">:</span> <span class="n">OptionSet</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">public</span> <span class="kd">let</span> <span class="nv">rawValue</span><span class="p">:</span> <span class="nb">Int</span> </span></span><span class="line"><span class="cl"> <span class="kd">public</span> <span class="kd">init</span><span class="p">(</span><span class="n">rawValue</span><span class="p">:</span><span class="nb">Int</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kc">self</span><span class="p">.</span><span class="n">rawValue</span> <span class="p">=</span> <span class="n">rawValue</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">static</span> <span class="kd">let</span> <span class="nv">label</span> <span class="p">=</span> <span class="kc">Self</span><span class="p">(</span><span class="n">rawValue</span><span class="p">:</span> <span class="mi">1</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">static</span> <span class="kd">let</span> <span class="nv">icon</span> <span class="p">=</span> <span class="kc">Self</span><span class="p">(</span><span class="n">rawValue</span><span class="p">:</span> <span class="mi">2</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">static</span> <span class="kd">let</span> <span class="nv">badge</span> <span class="p">=</span> <span class="kc">Self</span><span class="p">(</span><span class="n">rawValue</span><span class="p">:</span> <span class="mi">4</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">static</span> <span class="kd">let</span> <span class="nv">disclosure</span> <span class="p">=</span> <span class="kc">Self</span><span class="p">(</span><span class="n">rawValue</span><span class="p">:</span> <span class="mi">8</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">static</span> <span class="kd">let</span> <span class="nv">backView</span> <span class="p">=</span> <span class="kc">Self</span><span class="p">(</span><span class="n">rawValue</span><span class="p">:</span> <span class="mi">16</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">static</span> <span class="kd">let</span> <span class="nv">shadowBackView</span> <span class="p">=</span> <span class="kc">Self</span><span class="p">(</span><span class="n">rawValue</span><span class="p">:</span> <span class="mi">32</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">value</span><span class="p">:</span> <span class="n">LayoutFlags</span> <span class="p">=</span> <span class="p">[.</span><span class="n">label</span><span class="p">,</span> <span class="p">.</span><span class="n">badge</span><span class="p">]</span> </span></span></code></pre></div><p>Another implementation might be to model bitwise masks:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">struct</span> <span class="nc">BIOMarker</span><span class="p">:</span> <span class="n">OptionSet</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">rawValue</span><span class="p">:</span> <span class="nb">Int64</span> </span></span><span class="line"><span class="cl"> <span class="kd">init</span><span class="p">(</span><span class="n">rawValue</span><span class="p">:</span> <span class="nb">Int64</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kc">self</span><span class="p">.</span><span class="n">rawValue</span> <span class="p">=</span> <span class="n">rawValue</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">static</span> <span class="kd">let</span> <span class="nv">RAW</span> <span class="p">=</span> <span class="n">BIOMarker</span><span class="p">(</span><span class="n">rawValue</span><span class="p">:</span> <span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mi">0</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">static</span> <span class="kd">let</span> <span class="nv">HRM</span> <span class="p">=</span> <span class="n">BIOMarker</span><span class="p">(</span><span class="n">rawValue</span><span class="p">:</span> <span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mi">1</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">static</span> <span class="kd">let</span> <span class="nv">PLX</span> <span class="p">=</span> <span class="n">BIOMarker</span><span class="p">(</span><span class="n">rawValue</span><span class="p">:</span> <span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="mi">2</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">value</span><span class="p">:</span> <span class="n">BIOMarker</span> <span class="p">=</span> <span class="p">[.</span><span class="n">RAW</span><span class="p">,</span> <span class="p">.</span><span class="n">PLX</span><span class="p">]</span> </span></span></code></pre></div><p>How can you save these values into Core Data store, which has no idea about <code>OptionSet</code>? Simple: you save/restore the <code>rawValue</code> which is <code>Int</code> or <code>Int64</code> or anything else.</p> <p>To avoid modelling the transformation manually, you can build on the <a href="https://aplus.rs/2017/custom-typed-coredata-attributes/">idea I wrote about in 2017</a>: use custom <em>userInfo</em> of the CoreData modeler to set actual type name you want to use, for (mogenerator) example:</p> <p><code>attributeValueScalarType: BIOMarker</code></p> <p>First we make <code>OptionSet</code> adopt <code>CoreDataRepresentable</code> protocol described in that post:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">extension</span> <span class="nc">OptionSet</span> <span class="k">where</span> <span class="kc">Self</span><span class="p">:</span> <span class="n">CoreDataRepresentable</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">coredataValue</span><span class="p">:</span> <span class="kc">Self</span><span class="p">.</span><span class="n">RawValue</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">self</span><span class="p">.</span><span class="n">rawValue</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="kd">init</span><span class="p">?(</span><span class="n">coredataValue</span><span class="p">:</span> <span class="n">CoreDataBaseType</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kc">self</span><span class="p">.</span><span class="kd">init</span><span class="p">(</span><span class="n">rawValue</span><span class="p">:</span> <span class="n">coredataValue</span> <span class="k">as</span><span class="p">!</span> <span class="kc">Self</span><span class="p">.</span><span class="n">RawValue</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Our OptionSet type needs to specify what’s the fallback value, simplest way is empty set:</p> <pre tabindex="0"><code>extension BIOMarker: CoreDataRepresentable { static let coredataFallback: BIOMarker = [] } </code></pre><p>Then using mogenerator or SwiftGen, you can parse the model and generate the code we need for entity attributes where such <em>userInfo</em> is set. This is the code we need generated:</p> <pre tabindex="0"><code>var bioMarker: BIOMarker { get { let key = &#34;bioMarker&#34; willAccessValue(forKey: key) defer { didAccessValue(forKey: key) } if let primitiveValue = primitiveValue(forKey: key) as? BIOMarker.CoreDataBaseType, let value = BIOMarker(coredataValue: primitiveValue) { return value } return BIOMarker.coredataFallback } set { let key = &#34;bioMarker&#34; willChangeValue(forKey: key) defer { didChangeValue(forKey: key) } setPrimitiveValue(newValue.coredataValue, forKey: key) } } </code></pre><p>Code like this is very scriptable which is the reason I mention <a href="https://github.com/rentzsch/mogenerator">mogenerator</a> or <a href="https://github.com/SwiftGen/SwiftGen">SwiftGen</a>. Never maintain model parsing code by hand, you are certain to forget updating it at some point; it’s best to setup Run Script build phase that does this on each build.</p> <p>OK — now that we have such values in Core Data store, can we build predicates to read data based on these values? Sure we can — SQLite has <a href="https://www.sqlite.org/changes.html#version_2_0_3">support for bitwise operators</a> since 2001 so it’s easy to write a predicate to read all entities where <code>HRM</code> bit is set:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">hrm</span><span class="p">:</span> <span class="n">BIOMarker</span> <span class="p">=</span> <span class="p">[.</span><span class="n">HRM</span><span class="p">]</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">NSPredicate</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">format</span><span class="p">:</span> <span class="s">&#34;(%K &amp; %i) != 0&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="s">&#34;bioMarker&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">hrm</span><span class="p">.</span><span class="n">rawValue</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span></code></pre></div> Coordinator pattern with UITabBarController https://aplus.rs/2022/coordinator-with-tabbarcontroller/ Sun, 29 May 2022 09:35:00 +0000 https://aplus.rs/2022/coordinator-with-tabbarcontroller/ <p>Leonardo Maia Pugliese recently published a blog post that picked my interest: <a href="https://holyswift.app/coordinators-and-tab-bars-a-love-story">Coordinators and Tab Bars: A Love Story</a>. I always love to read how other people employ this useful pattern thus I carefully read through it.</p> <p>The problem he is solving is this:</p> <blockquote> <p>Imagine that you have a tab bar with two tabs and each tab is a navigation controller. The Home Tab has 2 levels deep and the Orders Tab has 3 levels deep. <br> The problem is to navigate from the second level deep view controller of home Tab to the 3 level deep Orders tab maintaining the hierarchical sequence of orders tab, this means that the return button should return to level 2 of level tab and so on.</p></blockquote> <p>In UIKit, event/data flow is achieved through <code>UIResponder</code> + a special set of overridable methods on <code>UIViewController</code>, like <code>show(vc)</code>, <code>present(vc)</code> etc.</p> <p>UIKit’s <code>show()</code> method has super simple logic. When called inside any UIVC, this method will:</p> <ol> <li>check if current UIVC has <code>parent</code> - if yes, call <code>parent.show(vc)</code> and do nothing else</li> <li>If no parent, popup the target UIVC (a rather crude fallback)</li> </ol> <p>Sadly, UIKit falls way short of making true separation of concerns. Per example project from Leonardo’s post, to switch from second screen in Home tab to a third screen in Orders tab <em>and</em> to keep the Orders drill-down navigation hierarchy, UIKit forces you to do this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">controllers</span> <span class="p">=</span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="n">OrdersViewController</span><span class="p">(),</span> </span></span><span class="line"><span class="cl"> <span class="n">Orders2ViewController</span><span class="p">(),</span> </span></span><span class="line"><span class="cl"> <span class="n">Orders3ViewController</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">]</span> </span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">target</span> <span class="p">=</span> <span class="n">tabBarController</span><span class="p">.</span><span class="n">viewControllers</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="k">as</span><span class="p">?</span> <span class="n">UINavigationController</span> </span></span><span class="line"><span class="cl"><span class="n">target</span><span class="p">?.</span><span class="n">viewControllers</span> <span class="p">=</span> <span class="n">controllers</span> </span></span></code></pre></div><p>Poor <code>Home2ViewController</code> needs to</p> <ol> <li>create and configure 3 completely separate controllers to whom it has no actual relation</li> <li>needs to know exactly where itself is located in the UIKit hierarchy and call specific properties after performing fixed type-casting</li> </ol> <p>That’s just&hellip;horrible. Navigation remains the worst aspect of iOS SDK, regardless if you are using UIKit or SwiftUI.</p> <hr> <p>To solve these issues, Leonardo correctly declares the app hierarchy independent of actual UIKit objects:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">enum</span> <span class="nc">AppFlow</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">home</span><span class="p">(</span><span class="n">HomeScreen</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">orders</span><span class="p">(</span><span class="n">OrdersScreen</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">enum</span> <span class="nc">HomeScreen</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">initialScreen</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">doubleButtonScreen</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">enum</span> <span class="nc">OrdersScreen</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">firstScreen</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">secondScreen</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">thirdScreen</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>This works for an example project. It can easily be extended into truly complex flows, screens that accept parameters (using associated values on cases) etc. Thus it’s good approach.</p> <p>Further, he implements Coordinators to create and configure UIVC for each of these screens. There’s <code>MainCoordinator</code> which manages <code>UITabBarController</code> while each of the tabs are child Coordinator managing their own <code>UINavigationController</code> instance. Again — this is good implementation where each Coordinator is responsible to create and display its subset of UIVCs. For example, second screen in Home tab:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">goToHome2ScreenWith</span><span class="p">(</span><span class="n">title</span><span class="p">:</span> <span class="nb">String</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">home2ViewController</span> <span class="p">=</span> <span class="n">Home2ViewController</span><span class="p">(</span><span class="n">coordinator</span><span class="p">:</span> <span class="kc">self</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">home2ViewController</span><span class="p">.</span><span class="n">title</span> <span class="p">=</span> <span class="n">title</span> </span></span><span class="line"><span class="cl"> <span class="n">navigationRootViewController</span><span class="p">?.</span><span class="n">pushViewController</span><span class="p">(</span><span class="n">home2ViewController</span><span class="p">,</span> <span class="n">animated</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>So far, this solves the first issue with UIKit navigation.</p> <p>Where it falls short is the second problem: the implementation of <em>Home2ViewController request flow transfer to Orders3ViewController</em> is not good.</p> <hr> <p>The solution used is the one that builds on Soroush’s <a href="https://khanlou.com/2015/10/coordinators-redux/">original approach</a> of using Delegate pattern to offload navigation (switching from one screen/view to another) from the view-controllers to coordinators. Simplified:</p> <ul> <li>UIViewController will ask its Coordinator to switch to another UIVC</li> <li>if the requested UIVC is in another tab, Coordinator will ask its parent Coordinator to do that</li> </ul> <p>It’s a really simple approach but also very limiting one. Delegate pattern is great when you need to transfer something from point A to point B. It does not matter which points they actually are; the limiting factor is that there’s only two points. The pattern does not allow transfer to potential point C (or D or E). If you need that, you need to implement another Delegate pattern which handles transfer from B to C etc.</p> <p>Navigating between screens may mean that you have to transfer data or flow between many points. In our practice, we regularly encountered transferring stuff from 5-6 levels deep to the top and then to completely different flow stack, to screen maybe 2-3 levels down. Delegating between all those levels leads to super rigid structure which becomes additional maintenance issue.</p> <p>You can glimpse the problem in this code:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">protocol</span> <span class="nc">FlowCoordinator</span><span class="p">:</span> <span class="nb">AnyObject</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">parentCoordinator</span><span class="p">:</span> <span class="n">MainBaseCoordinator</span><span class="p">?</span> <span class="p">{</span> <span class="kr">get</span> <span class="kr">set</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">protocol</span> <span class="nc">Coordinator</span><span class="p">:</span> <span class="n">FlowCoordinator</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">protocol</span> <span class="nc">MainBaseCoordinator</span><span class="p">:</span> <span class="n">Coordinator</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">homeCoordinator</span><span class="p">:</span> <span class="n">HomeBaseCoordinator</span> <span class="p">{</span> <span class="kr">get</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">ordersCoordinator</span><span class="p">:</span> <span class="n">OrdersBaseCoordinator</span> <span class="p">{</span> <span class="kr">get</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">deepLinkCoordinator</span><span class="p">:</span> <span class="n">DeepLinkBaseCoordinator</span> <span class="p">{</span> <span class="kr">get</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span> <span class="nf">moveTo</span><span class="p">(</span><span class="n">flow</span><span class="p">:</span> <span class="n">AppFlow</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span> <span class="nf">handleDeepLink</span><span class="p">(</span><span class="n">text</span><span class="p">:</span> <span class="nb">String</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">protocol</span> <span class="nc">HomeBaseCoordinator</span><span class="p">:</span> <span class="n">Coordinator</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span> <span class="nf">goToHome2ScreenWith</span><span class="p">(</span><span class="n">title</span><span class="p">:</span> <span class="nb">String</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span> <span class="nf">goToFavoritesFlow</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span> <span class="nf">goToDeepViewInFavoriteTab</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">protocol</span> <span class="nc">OrdersBaseCoordinator</span><span class="p">:</span> <span class="n">Coordinator</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="p">@</span><span class="n">discardableResult</span> <span class="kd">func</span> <span class="nf">goToOrder2Screen</span><span class="p">(</span><span class="n">animated</span><span class="p">:</span> <span class="nb">Bool</span> <span class="p">)</span> <span class="p">-&gt;</span> <span class="kc">Self</span> </span></span><span class="line"><span class="cl"> <span class="p">@</span><span class="n">discardableResult</span> <span class="kd">func</span> <span class="nf">goToOrder3Screen</span><span class="p">(</span><span class="n">animated</span><span class="p">:</span> <span class="nb">Bool</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="kc">Self</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">protocol</span> <span class="nc">HomeBaseCoordinated</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">coordinator</span><span class="p">:</span> <span class="n">HomeBaseCoordinator</span><span class="p">?</span> <span class="p">{</span> <span class="kr">get</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">protocol</span> <span class="nc">OrdersBaseCoordinated</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">coordinator</span><span class="p">:</span> <span class="n">OrdersBaseCoordinator</span><span class="p">?</span> <span class="p">{</span> <span class="kr">get</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Usage of protocols is superfluous as the structure is rather rigid: <code>Main</code> is always a parent of <code>Home</code> and <code>Orders</code> and there’s fixed connection between each point. The flow-transferring methods do exactly one thing thus I’m not sure why all of these protocols even exist; there’s nothing I see they help to abstract. The data flows implemented further in the post would work the same without any of these protocols, as there’s fixed connections (using stored properties):</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">class</span> <span class="nc">MainCoordinator</span><span class="p">:</span> <span class="n">MainBaseCoordinator</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">parentCoordinator</span><span class="p">:</span> <span class="n">MainBaseCoordinator</span><span class="p">?</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kr">lazy</span> <span class="kd">var</span> <span class="nv">homeCoordinator</span><span class="p">:</span> <span class="n">HomeBaseCoordinator</span> <span class="p">=</span> <span class="p">...</span> </span></span><span class="line"><span class="cl"> <span class="kr">lazy</span> <span class="kd">var</span> <span class="nv">ordersCoordinator</span><span class="p">:</span> <span class="n">OrdersBaseCoordinator</span> <span class="p">=</span> <span class="p">...</span> </span></span><span class="line"><span class="cl"><span class="p">...</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">class</span> <span class="nc">HomeCoordinator</span><span class="p">:</span> <span class="n">HomeBaseCoordinator</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">parentCoordinator</span><span class="p">:</span> <span class="n">MainBaseCoordinator</span><span class="p">?</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">func</span> <span class="nf">goToDeepViewInFavoriteTab</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">parentCoordinator</span><span class="p">?.</span><span class="n">moveTo</span><span class="p">(</span><span class="n">flow</span><span class="p">:</span> <span class="p">.</span><span class="n">Favorites</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">...</span> </span></span><span class="line"><span class="cl"><span class="p">...</span> </span></span></code></pre></div><p>Further down, view controllers also store fixed connection to specific parent Coordinator, again creating rigid connection:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">class</span> <span class="nc">HomeViewController</span><span class="p">:</span> <span class="n">UIViewController</span><span class="p">,</span> <span class="n">HomeBaseCoordinated</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">coordinator</span><span class="p">:</span> <span class="n">HomeBaseCoordinator</span><span class="p">?</span> </span></span><span class="line"><span class="cl"> <span class="p">...</span> </span></span></code></pre></div><p>I am confused what benefit all this scaffolding brings. If you opt to use fixed connections like this, you don’t need Coordinators nor protocols of any kind. Simply using embedded parent/child UIViewControllers and setting the equivalent properties will achieve the same thing.</p> <p>The method call :</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">var</span> <span class="nv">parentCoordinator</span><span class="p">:</span> <span class="n">MainBaseCoordinator</span><span class="p">?</span> </span></span><span class="line"><span class="cl"><span class="p">...</span> </span></span><span class="line"><span class="cl"><span class="n">parentCoordinator</span><span class="p">?.</span><span class="n">moveTo</span><span class="p">(</span><span class="n">flow</span><span class="p">:</span> <span class="p">.</span><span class="n">orders</span><span class="p">(.</span><span class="n">secondScreen</span><span class="p">))</span> </span></span></code></pre></div><p>would work just the same with hypothetical <code>Main</code> VC:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">var</span> <span class="nv">parentController</span><span class="p">:</span> <span class="n">MainController</span><span class="p">?</span> </span></span><span class="line"><span class="cl"><span class="p">...</span> </span></span><span class="line"><span class="cl"><span class="n">parentController</span><span class="p">?.</span><span class="n">moveTo</span><span class="p">(</span><span class="n">flow</span><span class="p">:</span> <span class="p">.</span><span class="n">orders</span><span class="p">(.</span><span class="n">secondScreen</span><span class="p">))</span> </span></span></code></pre></div><p>The main issue is that, with this approach, content UIViewControllers have direct connection to specific parent objects. It’s the same thing as what we already have:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">target</span> <span class="p">=</span> <span class="n">tabBarController</span><span class="p">.</span><span class="n">viewControllers</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="k">as</span><span class="p">?</span> <span class="n">UINavigationController</span> </span></span><span class="line"><span class="cl"><span class="n">target</span><span class="p">?.</span><span class="n">moveTo</span><span class="p">(</span><span class="n">flow</span><span class="p">:</span> <span class="p">.</span><span class="n">orders</span><span class="p">(.</span><span class="n">secondScreen</span><span class="p">))</span> </span></span></code></pre></div><hr> <p>Content view controllers should not know nor care who created them nor where they are located inside app’s flow/view hierarchy. They should be self-contained, have specific purpose and some exit points. Content UIVC should not know what other object will handle <code>.moveTo(flow:)</code></p> <p>(One of the) benefits of Coordinators — separation of app flow from the screens belonging to the flow — can only be achieved if you completely isolate screens from the coordinators and from other screens.</p> <ul> <li>UIViewController should not know nor care what its parent coordinator is.</li> <li>Child Coordinator should not know nor care what its parent Coordinator is.</li> <li>In both cases, child object should be able to communicate to their “parent” that flow needs to transfer to something else.</li> </ul> <p>So, how to achieve that?</p> <p>The key is to re-purpose the framework’s existing scaffolding. I already explained how <code>show(vc)</code> works its magic. The beautiful part is that we can create our own magic by re-purposing responder chain and adding additional methods.</p> <p>My <a href="https://github.com/radianttap/Coordinator">Coordinator library</a> does this with <a href="https://github.com/radianttap/Coordinator/blob/master/Coordinator/UIKit-CoordinatingExtensions.swift">few tricks</a> which take less than 20 lines of actual code:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">import</span> <span class="nc">UIKit</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1">// Inject parentCoordinator property into all UIViewControllers</span> </span></span><span class="line"><span class="cl"><span class="kd">extension</span> <span class="nc">UIViewController</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">private</span> <span class="kd">class</span> <span class="nc">WeakCoordinatingTrampoline</span><span class="p">:</span> <span class="n">NSObject</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kr">weak</span> <span class="kd">var</span> <span class="nv">coordinating</span><span class="p">:</span> <span class="n">Coordinating</span><span class="p">?</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">private</span> <span class="kd">struct</span> <span class="nc">AssociatedKeys</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">static</span> <span class="kd">var</span> <span class="nv">ParentCoordinator</span> <span class="p">=</span> <span class="s">&#34;ParentCoordinator&#34;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">public</span> <span class="kr">weak</span> <span class="kd">var</span> <span class="nv">parentCoordinator</span><span class="p">:</span> <span class="n">Coordinating</span><span class="p">?</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kr">get</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">trampoline</span> <span class="p">=</span> <span class="n">objc_getAssociatedObject</span><span class="p">(</span><span class="kc">self</span><span class="p">,</span> <span class="p">&amp;</span><span class="n">AssociatedKeys</span><span class="p">.</span><span class="n">ParentCoordinator</span><span class="p">)</span> <span class="k">as</span><span class="p">?</span> <span class="n">WeakCoordinatingTrampoline</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">trampoline</span><span class="p">?.</span><span class="n">coordinating</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="kr">set</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">trampoline</span> <span class="p">=</span> <span class="n">WeakCoordinatingTrampoline</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="n">trampoline</span><span class="p">.</span><span class="n">coordinating</span> <span class="p">=</span> <span class="n">newValue</span> </span></span><span class="line"><span class="cl"> <span class="n">objc_setAssociatedObject</span><span class="p">(</span><span class="kc">self</span><span class="p">,</span> <span class="p">&amp;</span><span class="n">AssociatedKeys</span><span class="p">.</span><span class="n">ParentCoordinator</span><span class="p">,</span> <span class="n">trampoline</span><span class="p">,</span> <span class="p">.</span><span class="n">OBJC_ASSOCIATION_RETAIN</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">extension</span> <span class="nc">UIResponder</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kr">@objc</span> <span class="n">open</span> <span class="kd">var</span> <span class="nv">coordinatingResponder</span><span class="p">:</span> <span class="n">UIResponder</span><span class="p">?</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">next</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">extension</span> <span class="nc">UIViewController</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kr">override</span> <span class="n">open</span> <span class="kd">var</span> <span class="nv">coordinatingResponder</span><span class="p">:</span> <span class="n">UIResponder</span><span class="p">?</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">guard</span> <span class="kd">let</span> <span class="nv">parentCoordinator</span> <span class="p">=</span> <span class="kc">self</span><span class="p">.</span><span class="n">parentCoordinator</span> <span class="k">else</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">guard</span> <span class="kd">let</span> <span class="nv">parentController</span> <span class="p">=</span> <span class="kc">self</span><span class="p">.</span><span class="n">parent</span> <span class="k">else</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">guard</span> <span class="kd">let</span> <span class="nv">presentingController</span> <span class="p">=</span> <span class="kc">self</span><span class="p">.</span><span class="n">presentingViewController</span> <span class="k">else</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">view</span><span class="p">.</span><span class="n">superview</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">presentingController</span> <span class="k">as</span> <span class="n">UIResponder</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">parentController</span> <span class="k">as</span> <span class="n">UIResponder</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">parentCoordinator</span> <span class="k">as</span><span class="p">?</span> <span class="n">UIResponder</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>This code creates new property called <code>coordinatingResponder</code> on <em>all</em> UIKit’s components: anything that’s based on <code>UIResponder</code> which means all views, controls and controllers. With that added, we can re-create our own <code>show(vc)</code> that works with <code>AppFlow</code>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">extension</span> <span class="nc">UIResponder</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kr">@objc</span> <span class="kd">func</span> <span class="nf">openFlow</span><span class="p">(</span><span class="kc">_</span> <span class="n">flow</span><span class="p">:</span> <span class="n">AppFlowBox</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">keepHierarchy</span><span class="p">:</span> <span class="nb">Bool</span> <span class="p">=</span> <span class="kc">false</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">userData</span><span class="p">:</span> <span class="p">[</span><span class="nb">String</span><span class="p">:</span> <span class="nb">Any</span><span class="p">]?</span> <span class="p">=</span> <span class="kc">nil</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">sender</span><span class="p">:</span> <span class="nb">Any</span><span class="p">?</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">coordinatingResponder</span><span class="p">?.</span><span class="n">openFlow</span><span class="p">(</span><span class="n">flow</span><span class="p">,</span> <span class="n">keepHierarchy</span><span class="p">:</span> <span class="n">keepHierarchy</span><span class="p">,</span> <span class="n">userData</span><span class="p">:</span> <span class="n">userData</span><span class="p">,</span> <span class="n">sender</span><span class="p">:</span> <span class="n">sender</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>I <a href="https://github.com/radianttap/CoordinatorTabbbarStudy">have re-factored</a> Leonardo’s <a href="https://github.com/leopug/CoordinatorTabbbarStudy">example</a> project to use my Coordinator library and that special method to transfer flow.</p> <p>Check the contents or any Coordinator or ViewController — there’s 0 references to parent anything. Whenever you need to switch flow to anywhere else, you just call <code>openFlow()</code> and pass the target flow point. Each Coordinator will override the method and perform routing as needed.</p> <p>For example, inside HomeCoordinator:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kr">override</span> <span class="kd">func</span> <span class="nf">openFlow</span><span class="p">(</span><span class="kc">_</span> <span class="n">flowboxed</span><span class="p">:</span> <span class="n">AppFlowBox</span><span class="p">,</span> <span class="n">keepHierarchy</span><span class="p">:</span> <span class="nb">Bool</span> <span class="p">=</span> <span class="kc">false</span><span class="p">,</span> <span class="n">userData</span><span class="p">:</span> <span class="p">[</span><span class="nb">String</span> <span class="p">:</span> <span class="nb">Any</span><span class="p">]?</span> <span class="p">=</span> <span class="kc">nil</span><span class="p">,</span> <span class="n">sender</span><span class="p">:</span> <span class="nb">Any</span><span class="p">?)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">flow</span> <span class="p">=</span> <span class="n">flowboxed</span><span class="p">.</span><span class="n">unbox</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">switch</span> <span class="n">flow</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="p">.</span><span class="n">home</span><span class="p">(</span><span class="kd">let</span> <span class="nv">screen</span><span class="p">):</span> </span></span><span class="line"><span class="cl"> <span class="n">displayScreen</span><span class="p">(</span><span class="n">screen</span><span class="p">,</span> <span class="n">userData</span><span class="p">:</span> <span class="n">userData</span><span class="p">,</span> <span class="n">sender</span><span class="p">:</span> <span class="n">sender</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="p">.</span><span class="n">orders</span><span class="p">:</span> </span></span><span class="line"><span class="cl"> <span class="n">coordinatingResponder</span><span class="p">?.</span><span class="n">openFlow</span><span class="p">(</span><span class="n">flowboxed</span><span class="p">,</span> <span class="n">keepHierarchy</span><span class="p">:</span> <span class="n">keepHierarchy</span><span class="p">,</span> <span class="n">userData</span><span class="p">:</span> <span class="n">userData</span><span class="p">,</span> <span class="n">sender</span><span class="p">:</span> <span class="n">sender</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>If the flow transfer request is from some of its children, it routes to its private method that displays that screen. If the target flow is not its responsibility, it will pass the request to next <code>coordinatingResponder</code>, upwards the responder chain. Note that HomeCoordinator does not know what that next (parent) object is. It simply passes the request through the magic conduit. Someone will handle it. 😇</p> <hr> <p>Responder chain in UIKit is truly wonderful concept. This particular implementation of Coordinator pattern is just one of its many uses.</p> Mac Studio is perfect Mac for app developers https://aplus.rs/2022/mac-studio-for-developers/ Fri, 11 Mar 2022 18:00:00 +0000 https://aplus.rs/2022/mac-studio-for-developers/ <p>Two years ago I <a href="https://aplus.rs/2020/missing-developer-mac/">lamented the gaping hole</a> in the Apple&rsquo;s desktop Mac lineup. I ended that post with:</p> <blockquote> <p>I would be perfectly happy to pay $3,000+ for a machine with that [16-ish core] CPU housed in Apple-designed case and cooling. If only Apple offered it.</p></blockquote> <p>Few days ago, Apple released <em>exactly</em> what I asked for: <a href="https://www.apple.com/newsroom/2022/03/apple-unveils-all-new-mac-studio-and-studio-display/">Mac Studio is a very powerful workstation in 3.6L case</a>. CPU can have up 10 (M1 Max) or 20 (M1 Ultra) cores paired with impressive GPU.</p> <p><img src="https://aplus.rs/images/2022/mac-studio-internals.jpeg" alt=""></p> <p>It&rsquo;s unreal how much power they packed into such a small chassis. I knew they could do this. Apple designed custom PSU of about 370W that can power M1 Ultra and with SoC being the entire machine, 2/3 of the volume is used for whisper-quiet cooling solution. Perfection.</p> <p>In the last two years, we have invested into 3 rather powerful ad-hoc Mac builds. I will wait for the proper ROI on all of them but in about 2 or 3 years our small agency will transition to Mac Studio(s). There is no doubt in my mind – these machines are amazing purchase for application developers and will easily last you 5+ years for any kind of app development you do. <em>Anything</em>.</p> <p>Difference between default machines is significant thus I won&rsquo;t recommend which one to buy — do the one you can afford. Both are more than capable. What I can advise is to not pay extra for SSD. This is desktop machine with multiple Thunderbolt 4 ports which means you can add as much super-fast external storage as you need over time. 40Gbps is more than enough even for 4x external NMVe RAID boxes. As for memory, I am using 32GB in all my machines and am nowhere near to be limited. But with memory being non-upgradable, I would opt for 64GB if possible; not the end of the world if not.</p> <p>Apple is truly back in the pro market.</p> Async Operation for Core Data imports https://aplus.rs/2021/async-operation-coredata-save/ Sat, 30 Oct 2021 10:20:40 +0000 https://aplus.rs/2021/async-operation-coredata-save/ <p>I wrote about my Essentials snippers <a href="https://aplus.rs/2017/swift-essentials/">4 years ago</a>. I am still using them, slightly <a href="https://github.com/radianttap/Swift-Essentials">updated and modernised</a> but they are still one of the first stops to choose what I need when I start a particular UIKit project.</p> <p>One advanced feature in there is <code>AsyncOperation</code> which a subclass of <a href="https://developer.apple.com/documentation/foundation/operation/">Operation</a> which will not set itself to finished until the asynchronous task you give it is done. <a href="https://developer.apple.com/documentation/foundation/operationqueue">OperationQueue</a> and Operation were a staple of my code for a very long time, whenever I needed to be sure things are being done in proper order. It’s super useful and simple to use API in Foundation framework to handle <a href="https://developer.apple.com/documentation/foundation/task_management">Task management</a>.</p> <blockquote> <p><a href="https://developer.apple.com/documentation/swift/swift_standard_library/concurrency">Concurrency</a> of Swift itself has been greatly expanded in recent years, with <code>Task</code> / <code>TaskGroup</code> and especially with async / await pattern. Thus I’m not sure how useful <code>AsyncOperation</code> will really be, going forward. I advise all to read-up on modern concurrency patterns and API and choose what’s most appropriate.</p></blockquote> <p>Apple’s <a href="https://developer.apple.com/documentation/foundation/blockoperation">BlockOperation</a> handles one or more blocks of synchronous code. But what if you have chained tasks where code inside each task is asynchronous? <code>AsyncOperation</code> overrides its internal state and prevents <code>Operation</code> from setting <code>isFinished=true</code> until you do it yourself, manually.</p> <p>So to create true async Operation</p> <ul> <li>we provide our desired block of code and</li> <li>we must make sure that <em>we</em> call <code>markFinished</code> at the true end of the task</li> </ul> <p>Developer is the only one that knows what “task is finished” means. I previously gave an <a href="https://aplus.rs/2018/asynchronous-operation/">example with networking code</a> where <code>markFinished</code> is called inside the <code>dataTask</code>’s closure. I have since changed the <a href="https://github.com/radianttap/Swift-Essentials/blob/master/Sources/Operation/AsyncOperation.swift">internal working of the AsyncOperation</a> but that example usage code still works.</p> <p>Here’s another example.</p> <p>When importing data from various web API endpoints into Core Data, I need to make sure Core Data store operations are done 1-by-1 to avoid data duplications. Doing insert/update/delete from multiples concurrent threads is super dangerous as you could end up in situation where you insert the same object multiple times. Cleaning that up from production devices is not fun at all.</p> <p>Thus I have this simple implementation of <code>SaveOperation</code>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kr">final</span> <span class="kd">class</span> <span class="nc">SaveOperation</span><span class="p">:</span> <span class="n">AsyncOperation</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">typealias</span> <span class="n">ProcessingBlock</span> <span class="p">=</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="nb">Void</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kr">override</span> <span class="kd">func</span> <span class="nf">workItem</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">block</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="n">markFinished</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kr">required</span> <span class="kd">init</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="bp">fatalError</span><span class="p">(</span><span class="s">&#34;Use the `init(name:block:)`&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">private</span> <span class="kd">var</span> <span class="nv">block</span><span class="p">:</span> <span class="n">ProcessingBlock</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">init</span><span class="p">(</span><span class="n">name</span><span class="p">:</span> <span class="nb">String</span> <span class="p">=</span> <span class="s">&#34;&#34;</span><span class="p">,</span> <span class="n">block</span><span class="p">:</span> <span class="p">@</span><span class="n">escaping</span> <span class="n">ProcessingBlock</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kc">self</span><span class="p">.</span><span class="n">block</span> <span class="p">=</span> <span class="n">block</span> </span></span><span class="line"><span class="cl"> <span class="kc">super</span><span class="p">.</span><span class="kd">init</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="kc">self</span><span class="p">.</span><span class="n">name</span> <span class="p">=</span> <span class="n">name</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Basically, you give it a block of code that must complete before the Operation is marked as finished. This code, for Core Data, is always synchronous since all fetches, inserts, deletes are done on same thread, including the saving of the <code>ManagedObjectContext</code> changes at the end of it all.</p> <p>With this, this is how processing of received JSON is being done:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">private</span><span class="p">(</span><span class="kr">set</span><span class="p">)</span> <span class="kd">var</span> <span class="nv">processingQueue</span> <span class="p">=</span> <span class="n">OperationQueue</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="p">...</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">processingQueue</span><span class="p">.</span><span class="n">maxConcurrentOperationCount</span> <span class="p">=</span> <span class="mi">1</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="p">...</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">processingQueue</span><span class="p">.</span><span class="n">addOperation</span><span class="p">(</span> <span class="n">SaveOperation</span><span class="p">(</span><span class="n">name</span><span class="p">:</span> <span class="n">endpoint</span><span class="p">.</span><span class="n">logName</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="p">[</span><span class="kr">unowned</span> <span class="kc">self</span><span class="p">]</span> <span class="k">in</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">moc</span> <span class="p">=</span> <span class="kc">self</span><span class="p">.</span><span class="n">vendManagedObjectContextForImporting</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">moc</span><span class="p">.</span><span class="n">performAndWait</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">do</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="p">[</span><span class="n">PROCESSING</span> <span class="n">HAPPENS</span> <span class="n">HERE</span><span class="p">]</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// eventually save into core data</span> </span></span><span class="line"><span class="cl"> <span class="k">try</span> <span class="n">moc</span><span class="p">.</span><span class="n">save</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="k">catch</span> <span class="kd">let</span> <span class="nv">error</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kc">self</span><span class="p">.</span><span class="n">log</span><span class="p">(</span><span class="n">level</span><span class="p">:</span> <span class="p">.</span><span class="n">warning</span><span class="p">,</span> <span class="n">error</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">})</span> </span></span></code></pre></div><p>It really is that simple. <code>performAndWait</code> make sure that everything is sync inside the <code>block()</code> and thus calling <code>markFinished</code> like this is OK:</p> <pre tabindex="0"><code> override func workItem() { block() markFinished() } </code></pre> Go the F to sleep https://aplus.rs/2021/hackintosh-sleep-wake/ Sat, 23 Oct 2021 13:09:40 +0000 https://aplus.rs/2021/hackintosh-sleep-wake/ <p>In the AMD Hackintosh land, successfully configuring your build to perform sleep / wake is one of the “Holly shit! It works!” moments. <br> Even though Intel and AMD CPUs are largely compatible (with few very notable differences), their corresponding chipsets are different story. Until recently, Apple exclusively used Intel CPUs and their chipsets (to some degree). Thus it’s not surprising that OpenCore builds feature sleep / wake pretty much out of the box <em>if</em> you use the identical components as per your chosen SMBIOS value.</p> <p>Same with many other aspects of computing: in recent Big Sur builds, my <a href="https://aplus.rs/2020/hmac-2020/">Z490+i7-10700K+RX-5500XT build</a> started randomly showing black screen for a second or two before getting back to normal. While harmless in general, it was super annoying and seriously disrupted day to day work of my wife (she’s using that build). Originally I used <code>iMacPro1,1</code> as SMBIOS even though the hardware build was pretty much replica of <code>iMac20,1</code>. Lô and behold — simply changing the SMBIOS to the latter made all these black screen problems disappear.<br> I am sure some other solution could be found with enough poking around the ACPI but as we will see – that’s a long and painstaking road.</p> <p>AMD chipsets are a different story, since they use different device components and their respective configurations and firmwares. Especially with USB controllers: X570 chipset has 3 USB controllers in itself. Coupled with the fact that manufacturers can write significantly different ACPI for the same darn thing — for some reason <a href="https://forum.amd-osx.com/index.php?threads/macos-monterey-patches-released.1892/page-5#post-12660">MSI-made boards can’t boot Monterey</a> beyond beta 3 — and you could find yourself in quite a mess.</p> <p>For almost a year now, <a href="https://github.com/radianttap/EFI-ASRock-X570-ITX-TB3">my own build</a> using AMD 5900X running on ASRock X570 ITX board had sleep disabled. It’s such a glorious CPU, giving you 12 cores / 24 threads running at 3.7GHz (4.9GHz boost) and costing just ~€750 with the motherboard included (try speccing that config with Intel, I dare you). But sleep…sleep was simply “a bridge too far”.</p> <p>Even after I <a href="https://aplus.rs/2020/usb-mapping-why/">mapped my USB</a>, added <a href="https://dortania.github.io/OpenCore-Post-Install/usb/misc/power.html">USB power properties</a>, switched from <code>iMacPro1,1</code> to <code>MacPro7,1</code> SMBIOS&hellip;nah, did not budge. Machine would turn off screen but would never actually enter sleep. What’s worse, it would lock-up so heavily that nothing responded anymore. Not even power button worked so the only way to turn it off would be pull the wall power. After some half-working attempts with <a href="https://github.com/acidanthera/HibernationFixup">HibernationFixup</a> and some random SSDTs I found online, I decided to dig in deep and learn how ACPI sleep actually works.</p> <p><a href="https://uefi.org/specs/ACPI/6.4/02_Definition_of_Terms/Definition_of_Terms.html">ACPI spec is huge</a> and not easy to dig into. First thing is defining the goal: what exactly I am after. In desktop machines, I want to <strong>achieve <a href="https://uefi.org/specs/ACPI/6.4/02_Definition_of_Terms/Definition_of_Terms.html#sleeping-and-soft-off-state-definitions">S3 sleep state</a></strong>, which means “suspend to RAM”. S4 is hibernation or “suspend to disk” which is very useful for laptops but for desktop machines it is not. S0 is fully awake state.</p> <p>After reading through <a href="https://uefi.org/specs/ACPI/6.4/07_Power_and_Performance_Mgmt/Power_and_Performance_Mgmt.html">Power Management section</a> and coupled with knowledge from previous attempts — the key to the sleep was proper implementation of <code>_PRW</code> method. Through implementations of <a href="https://uefi.org/specs/ACPI/6.4/07_Power_and_Performance_Mgmt/device-power-management-objects.html#prw-power-resources-for-wake">Power resources for Wake</a> method, devices tell the OS what sleep states they can be put into while still keeping the ability to wake the system. It also specifies what (if any) other power resources are needed for the device to properly wake up.</p> <p>It’s always useful to look at examples. So here’s typical one taken from actual MacPro <a href="https://uefi.org/specs/ACPI/6.4/05_ACPI_Software_Programming_Model/ACPI_Software_Programming_Model.html#differentiated-system-description-table-dsdt">DSDT table</a>:</p> <pre tabindex="0"><code>Method (_PRW, 0, NotSerialized) { If (OSDW ()) { Return (Package (0x02) { 0x69, 0x03 }) } Else { Return (Package (0x02) { 0x69, 0x04 }) } } </code></pre><p><code>OSDW</code> is ACPI method Apple uses to check if the active OS is Darwin (macOS). So we see that return value from this method is package of two elements: first argument is the same while second is <code>3</code> for macOS, <code>4</code> for others (Windows, Linux etc).</p> <p>ACPI spec tells that Package format from this method means this:</p> <pre tabindex="0"><code>Package { EventInfo // Integer DeepestSleepState // Integer } </code></pre><blockquote> <p>DeepestSleepState is an Integer that contains the deepest power system sleeping state that can be entered while still providing wake functionality.</p></blockquote> <p>Okie, so this device can go into S3 sleep state for macOS and S4 for others. Looking across many other _PRW implementations in MacPro ACPI, the similar pattern is present.</p> <p>OK, let us now look at DSDT for my ASRock X570 motherboard ACPI. It has ~20 <code>_PRW</code> implementations and in all but one case that second argument is <code>0x04</code>. Here’s one:</p> <pre tabindex="0"><code>Method (_PRW, 0, NotSerialized) // _PRW: Power Resources for Wake { Return (GPRW (0x08, 0x04)) } </code></pre><p>Taking cue from Apple’s implementation, we should override this second parameter to be <code>0x03</code>. How to do that in OpenCore?</p> <p>We employ trickery that experienced iOS developers will know as swizzling: replace existing API with our own, providing custom implementation. We need to hot-patch manufacturer-provided ACPI and rename existing <code>GPRW</code> method into something else, say <code>XPRW</code>.</p> <p>(1) In OpenCore’s <code>config.plist</code> add this dictionary under <code>ACPI/Patch</code>:</p> <pre tabindex="0"><code>&lt;dict&gt; &lt;key&gt;Base&lt;/key&gt; &lt;string&gt;&lt;/string&gt; &lt;key&gt;BaseSkip&lt;/key&gt; &lt;integer&gt;0&lt;/integer&gt; &lt;key&gt;Comment&lt;/key&gt; &lt;string&gt;GPRW to XPRW Rename&lt;/string&gt; &lt;key&gt;Count&lt;/key&gt; &lt;integer&gt;0&lt;/integer&gt; &lt;key&gt;Enabled&lt;/key&gt; &lt;true/&gt; &lt;key&gt;Find&lt;/key&gt; &lt;data&gt; R1BSVwI= &lt;/data&gt; &lt;key&gt;Limit&lt;/key&gt; &lt;integer&gt;0&lt;/integer&gt; &lt;key&gt;Mask&lt;/key&gt; &lt;data&gt; &lt;/data&gt; &lt;key&gt;OemTableId&lt;/key&gt; &lt;data&gt; &lt;/data&gt; &lt;key&gt;Replace&lt;/key&gt; &lt;data&gt; WFBSVwI= &lt;/data&gt; &lt;key&gt;ReplaceMask&lt;/key&gt; &lt;data&gt; &lt;/data&gt; &lt;key&gt;Skip&lt;/key&gt; &lt;integer&gt;0&lt;/integer&gt; &lt;key&gt;TableLength&lt;/key&gt; &lt;integer&gt;0&lt;/integer&gt; &lt;key&gt;TableSignature&lt;/key&gt; &lt;data&gt; &lt;/data&gt; &lt;/dict&gt; </code></pre><p>Thus with this, system provided GPRW method is now called XPRW.</p> <p>(2) Create <a href="https://uefi.org/specs/ACPI/6.4/05_ACPI_Software_Programming_Model/ACPI_Software_Programming_Model.html#secondary-system-description-table-ssdt">custom SSDT</a> table with new implementation of GPRW:</p> <pre tabindex="0"><code>DefinitionBlock (&#34;&#34;, &#34;SSDT&#34;, 2, &#34;ATNV&#34;, &#34;GPRW&#34;, 0x00000000) { External (XPRW, MethodObj) // 2 Arguments Method (GPRW, 2, NotSerialized) { If (_OSI (&#34;Darwin&#34;)) { If ((0x04 == Arg1)) { Return (XPRW (Arg0, 0x03)) } } Return (XPRW (Arg0, Arg1)) } } </code></pre><p>In cases where second argument (<code>Arg1</code>) is <code>0x04</code>, replace it with <code>0x03</code> while passing the first argument (<code>Arg0</code>) as it was. In all other cases just pass to original method implementation.</p> <p>I called this <a href="https://github.com/radianttap/EFI-ASRock-X570-ITX-TB3/blob/master/EFI/OC/ACPI/SSDT-GPRW.aml">SSDT-GPRW.aml</a> and included it under <code>ACPI/Add</code> in config.plist.</p> <p>So&hellip;is it working? I am hesitant to claim with 100% certainty but so far — it is. With keyboard directly attached to USB port, with keyboard working as Bluetooth, both across multiple attempts. My USB ports usually have external USB disk, Glorious and Logitech USB mice, Blue Yeti USB microphone and powered USB hub in the LG 27GN950 monitor. Monitor display goes from RX 570 GPU over DisplayPort.</p> <p>Build goes into sleep and wakes up when mouse is moved or keyboard key is pressed. There’s still possibility something could be problematic but I am hopeful. Fingers crossed.</p> Nope, you GTFO. https://aplus.rs/2021/nope-you-gtfo/ Tue, 19 Oct 2021 09:20:00 +0000 https://aplus.rs/2021/nope-you-gtfo/ <blockquote> <p>Radar or GTFO!</p></blockquote> <p>That’s an old joke in Apple developer circles which was meant to be cute but…really wasn’t. It was often remarked you shouldn’t be speaking to Apple developers about some bug if you don’t already have the bug report number at hand. You could still approach them, of course; it would usually end with some nodding on their part with advice to file a radar and let them know. Back when Apple was still an underdog, it was even some point of pride to publish your RDAR number. Like — “see, I did my part” to make it better for everyone.</p> <p>That attitude was delusional back then; it’s downright insulting to ask for it today.</p> <p>Even when Apple was an underdog they were still a multi-billion dollar company. They were losing money like crazy but they were still a huge company that had all that money to lose. This expectation that indie developers should invest their own time (=money) to work for free was ludicrous. But hey – that was the sentiment due to Apple wielding a lot of goodwill in its small developer community.</p> <p>Apple is not an underdog anymore, they are the largest and most profitable company in the world. That goodwill largely evaporated in recent years due to Apple’s attitude and hostile actions towards 3rd-party developers.</p> <p>Most importantly though — <em>Apple never appreciated all that work</em>. Despite all the talk you would hear from developer evangelism team or particular engineer’s occasional proactivity, <em>Apple as entity</em> has never done a concerted effort to appreciate all the free QA work they were getting.</p> <p>That RDAR service was praised by every Apple employee I spoke to, as the best bug tracking service they have used. Maybe internally it was, I’ve never used it as such. From outside though — it was a horribly buggy, crash-prone web UI which was so frustrating to use that multiple people spent time to write Mac apps to workaround its issues. There were at least two public web wrappers / mirrors built around it, so people could publish what they reported so that other don’t waste time chasing the same issues.</p> <p>It was so bad that there was <a href="https://fixradarorgtfo.com">an attempt to nudge</a> Apple to do better, if they expect people to continue filing bug reports. There were many blog posts explaining <a href="https://nshipster.com/bug-reporting/">how to file a bug report</a> so it’s actionable by Apple engineers.<br> Few years ago, Apple did replace RDAR with <a href="https://feedbackassistant.apple.com">Feedback Reporter</a> which wasn’t much better. It took them a year to make it even remotely usable but it’s still slow today, it remains a chore to use.</p> <p>So what happened if you actually persevered and filed a bug report? Usually: <em>nothing</em>. Literally nothing.</p> <p>Open cases would linger unanswered for months or years. Maybe once a year you will get automated response to “re-check if the bug still applies to newer xOS version”. In some cases they would ask you to custom-build a project demonstrating the issue, since apparently description of the problem was not enough. Even if you make the project example, nothing guaranteed response nor resolution of the issue. I believe the number of cases yielding any useful result was abysmal.</p> <p>Apple was and still is expecting people to do <em>free QA work</em> for them <em>without any obligation</em> from their side.</p> <p>But the worst thing (to me, at least) is that every June, after WWDC, I would see breathless “call to arms” from non-Apple people to <a href="https://www.imore.com/radar-or-gtfo">file feedback as soon as you can</a> if you want “your bugs” fixed in time. If you want it done for major .0 release, make sure to spend your June/July filing issues you find. Install betas, test what you can, hurry up!</p> <p>Can we collectively stop with this nonsense? 😖 Please!</p> <p>Apple earned billions from the App Store in the last year alone, more than 99% of 3rd party devs earned <em>combined</em>. It’s <em>their obligation to deliver reliable SDK</em> they expect us to use. Because our apps are improving functionality and increasing desirability of their devices / platform.</p> <p>Apple does not lack the means to do the correct thing.</p> <ul> <li>Apple can and should hire 100s of people to just scour Stack Overflow, their own developer forums, subscribe to developer blogs, make lists on Twitter tracking outspoken developers.</li> <li>These employees should make note of every issue that’s being mentioned.</li> <li>Information should be accepted in any way or form: email, tweet, blog post, whatever.</li> <li>They should hire 1000s or people to attempt to replicate the issues on their own.</li> <li>Those employees should proactively contact people and ask them for more details if they are unable to replicate issues.</li> </ul> <p>An approach like this would very likely make people feel appreciated and potentially willing to make an effort and do all the things that Apple expect for granted: make example projects, collect sys info etc.</p> <p>An approach like this would also give Apple an immense pool of potential future developers and engineers. All such QA interns building projects will learn their tools, learn their APIs and will strive to either work for Apple or build apps for their platforms as 3rd party. There are few nicer initial points on your resume in this particular industry than “Software QA at Apple Inc” thus I have little doubt that Apple would have issues finding people to fill-in these positions. (As long as they don’t insist they live in Bay Area.)</p> <p>This is not rocket science and I don’t propose anything radical. Many client-service-oriented companies do exactly this.</p> <p>Design community learned long ago to say <a href="https://www.nospec.com">NO to spec-work</a>, to not “work for exposure”, to reject various other attempts to extract work for nothing. I really hope that Apple dev community learns that lesson too: you don’t owe Apple anything.</p>