Jekyll2022-10-21T06:54:54+00:00/feed.xmlSamuel Défago’s CornerA cozy place for random babbling about iOS and tvOS developmentDeclarative Reactive Programming with Combine2021-12-13T00:00:00+00:002021-12-13T00:00:00+00:00/declarative_reactive_programming_with_combine<p>Any non-trivial application uses some form of data, whether retrieved from a web service, from a database or generated by user interaction. A well-behaved application properly responds to data changing over time while preserving a delightful uninterrupted user experience. Achieving this result can be a challenge, though, as most applications rely on several data sources whose changes might require a user interface update at any time.</p> <p>Reactive programming offers an elegant solution to dealing with heterogeneous data sources, as it provides a comprehensive toolbox with which their results can be processed and consolidated. Though reactive programming is not new in the Apple ecosystem it historically required integrating third-party libraries like <a href="https://github.com/ReactiveCocoa/ReactiveSwift">ReactiveSwift</a> or <a href="https://github.com/ReactiveX/RxSwift">RXSwift</a>, which could be a tough decision.</p> <p>This is why the fact that Apple officially introduced <a href="https://developer.apple.com/documentation/combine">Combine</a> in 2019 is a huge step forward, as it makes reactive programming immediately available to any project targeting iOS 13, tvOS 13, watchOS 6 or macOS 10.15. Adopting Combine or reactive programming might sometimes be frowned upon, though, as the initial learning curve might be steep. This is true but, as this article will attempt to illustrate, Combine and reactive programming can be invaluable tools to help you manage data sources in a way that is both expressive and scalable.</p> <p>This article assumes some familiarity with Combine and reactive programming in general. If you are new to Combine you should probably watch the <a href="https://developer.apple.com/videos/play/wwdc2019/722">corresponding WWDC talk</a> for a mild introduction first. You can also peek at <a href="https://heckj.github.io/swiftui-notes">Joseph Heck’s excellent <em>Using Combine</em> book</a> at any time to better understand code snippets appearing in this article.</p> <ul id="markdown-toc"> <li><a href="#foreword" id="markdown-toc-foreword">Foreword</a></li> <li><a href="#data-emitters" id="markdown-toc-data-emitters">Data Emitters</a></li> <li><a href="#data-pipelines" id="markdown-toc-data-pipelines">Data Pipelines</a></li> <li><a href="#triggers-and-signals" id="markdown-toc-triggers-and-signals">Triggers and Signals</a></li> <li><a href="#paginated-publishers" id="markdown-toc-paginated-publishers">Paginated Publishers</a></li> <li><a href="#refresh-publishers" id="markdown-toc-refresh-publishers">Refresh Publishers</a></li> <li><a href="#accumulators" id="markdown-toc-accumulators">Accumulators</a></li> <li><a href="#advanced-pipeline-design" id="markdown-toc-advanced-pipeline-design">Advanced Pipeline Design</a></li> <li><a href="#declarative-data-pipelines-and-view-models" id="markdown-toc-declarative-data-pipelines-and-view-models">Declarative Data Pipelines and View Models</a></li> <li><a href="#user-driven-data-emitters" id="markdown-toc-user-driven-data-emitters">User-driven Data Emitters</a></li> <li><a href="#conclusion" id="markdown-toc-conclusion">Conclusion</a></li> <li><a href="#sample-code" id="markdown-toc-sample-code">Sample Code</a></li> </ul> <h4 class="no_toc" id="remark">Remark</h4> <p>For the sake of simplicity code samples appearing in this article often define types and methods at global scope. In practice these should be of course be part of a narrower scope for better encapsulation.</p> <h2 id="foreword">Foreword</h2> <p><a href="https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html">Async/await and actors</a> draw a lot of attention nowadays and Combine did not receive any significant updates since its introduction in 2019, merely a few stability fixes and a couple of API improvements. The question of whether Combine might soon be discontinued entirely in favor of async/await, or even whether Combine is still relevant now that async/await is available with the same version requirements,<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> regularly appears in articles and on Twitter since WWDC 2021.</p> <p>I am pretty confident that Combine is here to stay, especially considered its tight relationship with SwiftUI. The Combine API was already comprehensive in 2019 and I think that the fact that no significant updates were made is a proof of maturity, rather than the worrying sign of a decaying framework.</p> <p>This article might also convince you that async/await and Combine are complementary rather than mutually exclusive. Combine is namely a reactive framework with the concept of <a href="https://developer.apple.com/documentation/combine/processing-published-elements-with-subscribers">back pressure at its heart</a>, while asyc/await and actors provide language constructs for <a href="https://gist.github.com/lattner/31ed37682ef1576b16bca1432ea9f782">structured concurrency</a>. Both approaches might address common concerns like avoiding concurrent access to mutable states or scalability, but in general they serve different purposes.</p> <p>A significant time I spend every week as an app developer involves writing or updating code that aggregates data from several sources. I found reactive programming and Combine to be invaluable tools for such tasks. It would certainly be possible to achieve the same results using other approaches but, as this article will hopefully illustrate, Combine provides a formalism with which the obtained code can be both expressive and scalable, whereas other approaches usually quickly lead to code which is hard to maintain. If I were to implement a database access layer or a cache, though, I would rather use actors instead, since safe access to a shared resource is more relevant in this case than reactiveness or back pressure. Different purposes, different tools.</p> <p>Some people might of course argue that Combine and reactive programming are not easy and that <a href="https://twitter.com/collindonnell/status/1446291171517468675">compiler support is not always great</a>, and they would certainly be right. But async/await and actors, or even GCD, are not easy either. Concurrency in general is a hard problem and it is only natural that approaches dealing with it have some learning curve, advantages and drawbacks. Just stay open-minded about which options are available and where they work best to pick the one that will help you write better code. We never had as many great options as we have nowadays, and it would be a shame not to use the one that works best in a specific case.</p> <h2 id="data-emitters">Data Emitters</h2> <p>It usually does not matter how a data source actually fetches data internally. This might happen through a network request or a database operation, but how the associated network or data access layers are internally implemented is not important. Instead you should treat data sources in your application as opaque <em>data emitters</em> with a well-defined API contract. This contract essentially boils down to what kind of data is emitted, how the process might fail, and whether an emitter delivers data once or several times until possible exhaustion.</p> <p>With Combine any data emitter can be exposed as a publisher, either because this is how its API was designed in the first place or because some asynchronous API call was wrapped into a <code class="language-plaintext highlighter-rouge">Future</code>. The specifics of data delivery are expressed through output and failure generic types, for example:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">profilePublisher</span><span class="p">(</span><span class="nv">forUserId</span><span class="p">:</span> <span class="kt">String</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">AnyPublisher</span><span class="o">&lt;</span><span class="kt">Profile</span><span class="p">,</span> <span class="kt">Error</span><span class="o">&gt;</span> <span class="kd">func</span> <span class="nf">filesPublisher</span><span class="p">(</span><span class="nv">inDirectoryAt</span><span class="p">:</span> <span class="nv">url</span><span class="p">:</span> <span class="kt">URL</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">AnyPublisher</span><span class="o">&lt;</span><span class="p">[</span><span class="kt">File</span><span class="p">],</span> <span class="kt">Error</span><span class="o">&gt;</span> <span class="kd">func</span> <span class="nf">lifecycleEventPublisher</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">AnyPublisher</span><span class="o">&lt;</span><span class="kt">NSNotification</span><span class="p">,</span> <span class="kt">Never</span><span class="o">&gt;</span> </code></pre></div></div> <h2 id="data-pipelines">Data Pipelines</h2> <p>Once you have identified the data emitters you need you can connect them together, ultimately forming a tree-shaped structure describing the entire data delivery process. No matter how complex this tree can be, it can be seen as a <em>data pipeline</em> assembling data from various emitters, transforming and consolidating it along the way, before delivering it to a single endpoint.</p> <p>Combine provides a <a href="https://heckj.github.io/swiftui-notes">rich toolset</a> to build pipelines:</p> <ul> <li>Operators with which you can shape the data, e.g. extracting or formatting a value, or wrapping it into a different type (<code class="language-plaintext highlighter-rouge">map</code>, <code class="language-plaintext highlighter-rouge">filter</code>, etc.).</li> <li>Operators with which you can tweak the delivery process (<code class="language-plaintext highlighter-rouge">throttle</code>, <code class="language-plaintext highlighter-rouge">debounce</code>, etc.).</li> <li>Operators which gather data before delivering it further (<code class="language-plaintext highlighter-rouge">scan</code>, <code class="language-plaintext highlighter-rouge">reduce</code>).</li> <li>Publishers that aggregate other publishers (<code class="language-plaintext highlighter-rouge">Publishers.Merge</code>, <code class="language-plaintext highlighter-rouge">Publishers.Zip</code>, <code class="language-plaintext highlighter-rouge">Publishers.CombineLatest</code>).</li> <li>And many more…</li> </ul> <p>Using publishers like <code class="language-plaintext highlighter-rouge">Publishers.Merge</code> or <code class="language-plaintext highlighter-rouge">Publishers.CombineLatest</code>, as well as combinations of operators like <code class="language-plaintext highlighter-rouge">map</code> and <code class="language-plaintext highlighter-rouge">switchToLatest</code>, a trunk is able to grow branches, which themselves can grow other branches and so forth, allowing you to create abritrarily complex data delivery trees, e.g. to consolidate data associated with a user id:</p> <p><img src="https://docs.google.com/drawings/d/e/2PACX-1vQWbCWEoj4zg_dD6_HnAAk-rVBXiQawmLiohIMdM8_NJ1OsFIp62TAH1U95rjGNTH5dwyzhmhafvuo0/pub?w=837&amp;h=409" alt="Complex Pipeline" /></p> <p>The process of building a pipeline this way is inherently declarative, as you focus on the description of the data flow rather than on the intricacies of its implementation. Moreover, since any pipeline is itself a publisher, arbitrarily complex pipelines can be treated as opaque data emitters and inserted into other pipelines, in a composite fashion. For example the above pipeline reduces to a single user data emitter and can therefore be used in a pipeline fetching several user ids and consolidating the result as a view model state:</p> <p><img src="https://docs.google.com/drawings/d/e/2PACX-1vStHGubMMrDreq1dx3HhGon_PKfwJGF6pnLilGG5z_XKuHkCLI9dh25vAYspbTAZcqXBJtmeR9EDkqV/pub?w=823&amp;h=337" alt="Composite Pipeline" /></p> <h2 id="triggers-and-signals">Triggers and Signals</h2> <p>Some data emitters in a pipeline might deliver data in pages of results. Traditional examples include network or database requests, as usually only partial results can be retrieved at a time, not the whole result set at once. When additional pages of results need to be loaded on-demand (e.g. when the user reaches the bottom of a list), it would be tempting to use a secondary pipeline for retrieving the subsequent pages of results. This approach, often applied in imperative block-based network code, can of course be applied with reactive pipelines, but suffers from similar drawbacks:</p> <ul> <li>A pointer to the next retrievable page must be stored as a mutable state external to the pipelines.</li> <li>The results must be consolidated between the two pipelines, again via an external mutable state.</li> <li>Cancellation must be properly coordinated. In our example, if a reload is triggered for the first pipeline while the secondary pipeline is still retrieving the next page of results, then both pipelines must likely be reset so that the overall state remains consistent.</li> </ul> <p>This approach does not scale well, since more states need to be added and synchronized as the number of paginated emitters increases. To make things worse, and due to the usually concurrent nature of data retrieval, extra care is required when accessing all these shared mutable states.</p> <p>With the help of a few additions, though, reactive programming and Combine make it possible to eliminate these issues entirely. If we namely only keep the first pipeline describing the whole data delivery process (without pagination), we can directly build pagination into this pipeline if we are able to ask any publisher directly for more results when needed. Updated results will then flow through the usual pipeline and deliver a consolidated update consistently.</p> <p>For example, if the document and address publishers in the above example both support pagination, all we need to add pagination support is a way to contact these two data emitters directly when more data is required:</p> <p><img src="https://docs.google.com/drawings/d/e/2PACX-1vSZcfmUXeBE6xt70-DzJPDl-SxbCErCBn3fth5UJFmDnABAJoyWGqq5Xmvi-bpz7o0ww_72jANxGMdN/pub?w=837&amp;h=409" alt="Pagination" /></p> <p>The idea of contacting a publisher directly can be neatly implemented using signals and triggers. A <em>signal</em> is a publisher whose values we are not interested in (<code class="language-plaintext highlighter-rouge">Void</code>), which never fails, and whose only purpose is therefore to signal that something happened, for example:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">networkReachableAgainSignal</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">AnyPublisher</span><span class="o">&lt;</span><span class="kt">Void</span><span class="p">,</span> <span class="kt">Never</span><span class="o">&gt;</span> </code></pre></div></div> <p>A <em>trigger</em>, on the other hand, is a communication device with a set of associated signals, which it can contact on-demand so that they emit a value. Being publishers, signals associated with a trigger can be inserted into any pipeline at design time to define control points which can be activated on-demand later on.</p> <p>Implementing triggers and associated signals is straightforward:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">Trigger</span> <span class="p">{</span> <span class="kd">typealias</span> <span class="kt">Index</span> <span class="o">=</span> <span class="kt">Int</span> <span class="kd">typealias</span> <span class="kt">Signal</span> <span class="o">=</span> <span class="kt">AnyPublisher</span><span class="o">&lt;</span><span class="kt">Void</span><span class="p">,</span> <span class="kt">Never</span><span class="o">&gt;</span> <span class="kd">private</span> <span class="k">let</span> <span class="nv">sender</span> <span class="o">=</span> <span class="kt">PassthroughSubject</span><span class="o">&lt;</span><span class="kt">Index</span><span class="p">,</span> <span class="kt">Never</span><span class="o">&gt;</span><span class="p">()</span> <span class="kd">func</span> <span class="nf">signal</span><span class="p">(</span><span class="n">activatedBy</span> <span class="nv">index</span><span class="p">:</span> <span class="kt">Index</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Signal</span> <span class="p">{</span> <span class="k">return</span> <span class="n">sender</span> <span class="o">.</span><span class="n">filter</span> <span class="p">{</span> <span class="nv">$0</span> <span class="o">==</span> <span class="n">index</span> <span class="p">}</span> <span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">_</span> <span class="k">in</span> <span class="p">}</span> <span class="o">.</span><span class="nf">eraseToAnyPublisher</span><span class="p">()</span> <span class="p">}</span> <span class="kd">func</span> <span class="nf">activate</span><span class="p">(</span><span class="k">for</span> <span class="nv">index</span><span class="p">:</span> <span class="kt">Index</span><span class="p">)</span> <span class="p">{</span> <span class="n">sender</span><span class="o">.</span><span class="nf">send</span><span class="p">(</span><span class="n">index</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>By leveraging publishers to implement communication between triggers and signals we ensure that no state or multithreading issues arise, as all the heavy lifting is entirely managed by Combine itself.</p> <p>Now assume we are equipped with a trigger:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">trigger</span> <span class="o">=</span> <span class="kt">Trigger</span><span class="p">()</span> </code></pre></div></div> <p>We can create an associated signal publisher for some identifier:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">signal</span> <span class="o">=</span> <span class="n">trigger</span><span class="o">.</span><span class="nf">signal</span><span class="p">(</span><span class="nv">activatedBy</span><span class="p">:</span> <span class="mi">1</span><span class="p">)</span> </code></pre></div></div> <p>which will emit a value when activated by the trigger:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">trigger</span><span class="o">.</span><span class="nf">activate</span><span class="p">(</span><span class="nv">for</span><span class="p">:</span> <span class="mi">1</span><span class="p">)</span> </code></pre></div></div> <p>Note that we use integers as identifiers, but we can simply use any hashable type as well with the help of the following extension:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">extension</span> <span class="kt">Trigger</span> <span class="p">{</span> <span class="kd">func</span> <span class="n">signal</span><span class="o">&lt;</span><span class="kt">T</span><span class="o">&gt;</span><span class="p">(</span><span class="n">activatedBy</span> <span class="nv">t</span><span class="p">:</span> <span class="kt">T</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Signal</span> <span class="k">where</span> <span class="kt">T</span><span class="p">:</span> <span class="kt">Hashable</span> <span class="p">{</span> <span class="k">return</span> <span class="nf">signal</span><span class="p">(</span><span class="nv">activatedBy</span><span class="p">:</span> <span class="n">t</span><span class="o">.</span><span class="n">hashValue</span><span class="p">)</span> <span class="p">}</span> <span class="kd">func</span> <span class="n">activate</span><span class="o">&lt;</span><span class="kt">T</span><span class="o">&gt;</span><span class="p">(</span><span class="k">for</span> <span class="nv">t</span><span class="p">:</span> <span class="kt">T</span><span class="p">)</span> <span class="k">where</span> <span class="kt">T</span><span class="p">:</span> <span class="kt">Hashable</span> <span class="p">{</span> <span class="nf">activate</span><span class="p">(</span><span class="nv">for</span><span class="p">:</span> <span class="n">t</span><span class="o">.</span><span class="n">hashValue</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>With triggers and signals now defined, let us see how they can be used to implement publishers supporting pagination.</p> <h2 id="paginated-publishers">Paginated Publishers</h2> <p>Assume we have some paginated publisher which returns items for a specific page, as well as a <code class="language-plaintext highlighter-rouge">next</code> page which can be used to load the next page of results (if any):</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">itemsPublisher</span><span class="p">(</span><span class="n">at</span> <span class="nv">page</span><span class="p">:</span> <span class="kt">Page</span><span class="p">?)</span> <span class="o">-&gt;</span> <span class="kt">AnyPublisher</span><span class="o">&lt;</span><span class="p">(</span><span class="nv">items</span><span class="p">:</span> <span class="p">[</span><span class="kt">Items</span><span class="p">],</span> <span class="nv">next</span><span class="p">:</span> <span class="kt">Page</span><span class="p">?),</span> <span class="kt">Error</span><span class="o">&gt;</span> </code></pre></div></div> <p>The first page of results can be obtained with <code class="language-plaintext highlighter-rouge">page</code> set to <code class="language-plaintext highlighter-rouge">nil</code>, and the <code class="language-plaintext highlighter-rouge">next</code> page is set to <code class="language-plaintext highlighter-rouge">nil</code> when all results have been returned. The publisher might internally retrieve results from a web service or from a database, and the next page might be extracted from the data itself, from a database cursor or from a response HTTP headers, but this is not important to our present discussion.</p> <p>We cannot use this publisher as is in a complex declarative data pipeline, as the page parameter would require some state to be stored externally, as discussed in the previous section. But what if we could have the publisher itself manage this state internally?</p> <p>This is exactly what we can achieve with triggers and signals. Let us namely define a helper publisher which takes a signal bound to a trigger as parameter. Since pagination is not necessarily desired in all cases we define this parameter as optional:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">itemsPublisher</span><span class="p">(</span><span class="n">at</span> <span class="nv">page</span><span class="p">:</span> <span class="kt">Page</span><span class="p">?,</span> <span class="n">paginatedBy</span> <span class="nv">paginator</span><span class="p">:</span> <span class="kt">Trigger</span><span class="o">.</span><span class="kt">Signal</span><span class="p">?)</span> <span class="o">-&gt;</span> <span class="kt">AnyPublisher</span><span class="o">&lt;</span><span class="p">(</span><span class="nv">items</span><span class="p">:</span> <span class="p">[</span><span class="kt">Items</span><span class="p">],</span> <span class="nv">next</span><span class="p">:</span> <span class="kt">Page</span><span class="p">?),</span> <span class="kt">Error</span><span class="o">&gt;</span> </code></pre></div></div> <p>If we could find a way to have this publisher enter a dormant state each time a page of results has been emitted, we could wake it up when a new page of results is desired, so that pagination can be entirely managed by the publisher implementation, eliminating the need for external states. This is actually possible if we introduce a <code class="language-plaintext highlighter-rouge">wait(untilOutputFrom:)</code> operator which, as its name suggests, simply waits for another signal to emit a value:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">extension</span> <span class="kt">Publisher</span> <span class="p">{</span> <span class="kd">func</span> <span class="n">wait</span><span class="o">&lt;</span><span class="kt">S</span><span class="o">&gt;</span><span class="p">(</span><span class="n">untilOutputFrom</span> <span class="nv">signal</span><span class="p">:</span> <span class="kt">S</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">AnyPublisher</span><span class="o">&lt;</span><span class="k">Self</span><span class="o">.</span><span class="kt">Output</span><span class="p">,</span> <span class="k">Self</span><span class="o">.</span><span class="kt">Failure</span><span class="o">&gt;</span> <span class="k">where</span> <span class="kt">S</span><span class="p">:</span> <span class="kt">Publisher</span><span class="p">,</span> <span class="kt">S</span><span class="o">.</span><span class="kt">Failure</span> <span class="o">==</span> <span class="kt">Never</span> <span class="p">{</span> <span class="k">return</span> <span class="nf">prepend</span><span class="p">(</span> <span class="kt">Empty</span><span class="p">(</span><span class="nv">completeImmediately</span><span class="p">:</span> <span class="kc">false</span><span class="p">)</span> <span class="o">.</span><span class="nf">prefix</span><span class="p">(</span><span class="nv">untilOutputFrom</span><span class="p">:</span> <span class="n">signal</span><span class="p">)</span> <span class="p">)</span> <span class="o">.</span><span class="nf">eraseToAnyPublisher</span><span class="p">()</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>Using this new operator we can manage pagination entirely within our helper publisher implementation:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">itemsPublisher</span><span class="p">(</span><span class="n">at</span> <span class="nv">page</span><span class="p">:</span> <span class="kt">Page</span><span class="p">?,</span> <span class="n">paginatedBy</span> <span class="nv">paginator</span><span class="p">:</span> <span class="kt">Trigger</span><span class="o">.</span><span class="kt">Signal</span><span class="p">?)</span> <span class="o">-&gt;</span> <span class="kt">AnyPublisher</span><span class="o">&lt;</span><span class="p">(</span><span class="nv">items</span><span class="p">:</span> <span class="p">[</span><span class="kt">Items</span><span class="p">],</span> <span class="nv">next</span><span class="p">:</span> <span class="kt">Page</span><span class="p">?),</span> <span class="kt">Error</span><span class="o">&gt;</span> <span class="p">{</span> <span class="k">return</span> <span class="nf">itemsPublisher</span><span class="p">(</span><span class="nv">at</span><span class="p">:</span> <span class="n">page</span><span class="p">)</span> <span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">result</span> <span class="o">-&gt;</span> <span class="kt">AnyPublisher</span><span class="o">&lt;</span><span class="p">(</span><span class="nv">items</span><span class="p">:</span> <span class="p">[</span><span class="kt">Items</span><span class="p">],</span> <span class="nv">next</span><span class="p">:</span> <span class="kt">Page</span><span class="p">?),</span> <span class="kt">Error</span><span class="o">&gt;</span> <span class="k">in</span> <span class="k">if</span> <span class="k">let</span> <span class="nv">paginator</span> <span class="o">=</span> <span class="n">paginator</span><span class="p">,</span> <span class="k">let</span> <span class="nv">next</span> <span class="o">=</span> <span class="n">result</span><span class="o">.</span><span class="n">next</span> <span class="p">{</span> <span class="k">return</span> <span class="nf">itemsPublisher</span><span class="p">(</span><span class="nv">at</span><span class="p">:</span> <span class="n">next</span><span class="p">,</span> <span class="nv">paginatedBy</span><span class="p">:</span> <span class="n">paginator</span><span class="p">)</span> <span class="o">.</span><span class="nf">wait</span><span class="p">(</span><span class="nv">untilOutputFrom</span><span class="p">:</span> <span class="n">paginator</span><span class="p">)</span> <span class="o">.</span><span class="nf">retry</span><span class="p">(</span><span class="o">.</span><span class="n">max</span><span class="p">)</span> <span class="o">.</span><span class="nf">prepend</span><span class="p">(</span><span class="n">result</span><span class="p">)</span> <span class="o">.</span><span class="nf">eraseToAnyPublisher</span><span class="p">()</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="kt">Just</span><span class="p">(</span><span class="n">result</span><span class="p">)</span> <span class="o">.</span><span class="nf">setFailureType</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="kt">Error</span><span class="o">.</span><span class="k">self</span><span class="p">)</span> <span class="o">.</span><span class="nf">eraseToAnyPublisher</span><span class="p">()</span> <span class="p">}</span> <span class="p">}</span> <span class="o">.</span><span class="nf">switchToLatest</span><span class="p">()</span> <span class="p">}</span> </code></pre></div></div> <p>This implementation deserves a few words of explanation:</p> <ul> <li>If a paginator has been provided and a next page is available we return the result immediately (<code class="language-plaintext highlighter-rouge">prepend</code>) and recursively prepare the request for the subsequent page of results, whose delivery is controlled by a <code class="language-plaintext highlighter-rouge">wait</code> (the pipeline must be read from the bottom to the top here). We also insert a <code class="language-plaintext highlighter-rouge">retry(.max)</code> so that, in the event a request fails, the trigger can be used to perform new attempts.</li> <li>Otherwise we just return the result we have and the publisher completes.</li> </ul> <p>This helper publisher can finally be used to implement the paginated item publisher we need, which can be inserted into any pipeline and controlled externally when more results are needed:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">itemsPublisher</span><span class="p">(</span><span class="n">paginatedBy</span> <span class="nv">paginator</span><span class="p">:</span> <span class="kt">Trigger</span><span class="o">.</span><span class="kt">Signal</span><span class="p">?</span> <span class="o">=</span> <span class="kc">nil</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">AnyPublisher</span><span class="o">&lt;</span><span class="p">[</span><span class="kt">Item</span><span class="p">],</span> <span class="kt">Error</span><span class="o">&gt;</span> <span class="p">{</span> <span class="k">return</span> <span class="nf">itemsPublisher</span><span class="p">(</span><span class="nv">at</span><span class="p">:</span> <span class="kc">nil</span><span class="p">,</span> <span class="nv">paginatedBy</span><span class="p">:</span> <span class="n">paginator</span><span class="p">)</span> <span class="o">.</span><span class="nf">map</span><span class="p">(\</span><span class="o">.</span><span class="n">items</span><span class="p">)</span> <span class="o">.</span><span class="nf">eraseToAnyPublisher</span><span class="p">()</span> <span class="p">}</span> </code></pre></div></div> <p>Now that have discussed how triggers and signals can be used to insert control points into any declarative pipelines, let us discuss how signals can be also used to refresh an entire pipeline or subset thereof.</p> <h2 id="refresh-publishers">Refresh Publishers</h2> <p>Let us assume we have some pipeline delivering data to a view model the first time its associated view is displayed, and that this process can fail (e.g. because network requests are somehow involved):</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">dataPublisher</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">AnyPublisher</span><span class="o">&lt;</span><span class="kt">Data</span><span class="p">,</span> <span class="kt">Error</span><span class="o">&gt;</span> </code></pre></div></div> <p>An application should in general be able to execute this pipeline again, most notably:</p> <ul> <li>When the user manually reloads the screen (e.g. pull-to-refresh).</li> <li>When the application returns to the foreground, so that data is up to date.</li> <li>When the network is reachable again, so that the application can automatically recover from network failure or ensure data is up to date.</li> </ul> <p>It is tempting to recreate the publisher again in such cases, but there is in fact a better way. If we namely realize that the above events can be translated into corresponding signal publishers, we can namely build these events right into our original reactive pipeline.</p> <p>Assume we have wrapped the above events into publishers with the following signatures:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">reloadSignal</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">AnyPublisher</span><span class="o">&lt;</span><span class="kt">Void</span><span class="p">,</span> <span class="kt">Never</span><span class="o">&gt;</span> <span class="kd">func</span> <span class="nf">foregroundSignal</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">AnyPublisher</span><span class="o">&lt;</span><span class="kt">Void</span><span class="p">,</span> <span class="kt">Never</span><span class="o">&gt;</span> <span class="kd">func</span> <span class="nf">networkReachableAgainSignal</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">AnyPublisher</span><span class="o">&lt;</span><span class="kt">Void</span><span class="p">,</span> <span class="kt">Never</span><span class="o">&gt;</span> </code></pre></div></div> <p>We can use any of these signals to force our pipeline or a subset thereof to be executed or repeated when they emit a value, provided we introduce the following publisher helpers:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">extension</span> <span class="kt">Publishers</span> <span class="p">{</span> <span class="kd">static</span> <span class="kd">func</span> <span class="kt">Publish</span><span class="o">&lt;</span><span class="kt">S</span><span class="p">,</span> <span class="kt">P</span><span class="o">&gt;</span><span class="p">(</span><span class="n">onOutputFrom</span> <span class="nv">signal</span><span class="p">:</span> <span class="kt">S</span><span class="p">,</span> <span class="n">_</span> <span class="nv">publisher</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">P</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">AnyPublisher</span><span class="o">&lt;</span><span class="kt">P</span><span class="o">.</span><span class="kt">Output</span><span class="p">,</span> <span class="kt">P</span><span class="o">.</span><span class="kt">Failure</span><span class="o">&gt;</span> <span class="k">where</span> <span class="kt">S</span><span class="p">:</span> <span class="kt">Publisher</span><span class="p">,</span> <span class="kt">P</span><span class="p">:</span> <span class="kt">Publisher</span><span class="p">,</span> <span class="kt">S</span><span class="o">.</span><span class="kt">Failure</span> <span class="o">==</span> <span class="kt">Never</span> <span class="p">{</span> <span class="k">return</span> <span class="n">signal</span> <span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">_</span> <span class="k">in</span> <span class="p">}</span> <span class="o">.</span><span class="nf">setFailureType</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="kt">P</span><span class="o">.</span><span class="kt">Failure</span><span class="o">.</span><span class="k">self</span><span class="p">)</span> <span class="c1">// Required for iOS 13, can be removed for iOS 14+</span> <span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">_</span> <span class="k">in</span> <span class="k">return</span> <span class="nf">publisher</span><span class="p">()</span> <span class="p">}</span> <span class="o">.</span><span class="nf">switchToLatest</span><span class="p">()</span> <span class="o">.</span><span class="nf">eraseToAnyPublisher</span><span class="p">()</span> <span class="p">}</span> <span class="kd">static</span> <span class="kd">func</span> <span class="kt">PublishAndRepeat</span><span class="o">&lt;</span><span class="kt">S</span><span class="p">,</span> <span class="kt">P</span><span class="o">&gt;</span><span class="p">(</span><span class="n">onOutputFrom</span> <span class="nv">signal</span><span class="p">:</span> <span class="kt">S</span><span class="p">,</span> <span class="n">_</span> <span class="nv">publisher</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">P</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">AnyPublisher</span><span class="o">&lt;</span><span class="kt">P</span><span class="o">.</span><span class="kt">Output</span><span class="p">,</span> <span class="kt">P</span><span class="o">.</span><span class="kt">Failure</span><span class="o">&gt;</span> <span class="k">where</span> <span class="kt">S</span><span class="p">:</span> <span class="kt">Publisher</span><span class="p">,</span> <span class="kt">P</span><span class="p">:</span> <span class="kt">Publisher</span><span class="p">,</span> <span class="kt">S</span><span class="o">.</span><span class="kt">Failure</span> <span class="o">==</span> <span class="kt">Never</span> <span class="p">{</span> <span class="k">return</span> <span class="n">signal</span> <span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">_</span> <span class="k">in</span> <span class="p">}</span> <span class="o">.</span><span class="nf">prepend</span><span class="p">(())</span> <span class="o">.</span><span class="nf">setFailureType</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="kt">P</span><span class="o">.</span><span class="kt">Failure</span><span class="o">.</span><span class="k">self</span><span class="p">)</span> <span class="c1">// Required for iOS 13, can be removed for iOS 14+</span> <span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">_</span> <span class="k">in</span> <span class="k">return</span> <span class="nf">publisher</span><span class="p">()</span> <span class="p">}</span> <span class="o">.</span><span class="nf">switchToLatest</span><span class="p">()</span> <span class="o">.</span><span class="nf">eraseToAnyPublisher</span><span class="p">()</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>The first publisher only executes the wrapped publisher when a signal emits a value, while the second one always executes the wrapped publisher at least once (a value is namely prepended to the pipeline), repeating the process each time a signal emits a value.</p> <p>Equipped with these publishers it is straightforward to have <code class="language-plaintext highlighter-rouge">dataPublisher()</code> execute once and repeat when, for example, a reload is made:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">Publishers</span><span class="o">.</span><span class="kt">PublishAndRepeat</span><span class="p">(</span><span class="nv">onOutputFrom</span><span class="p">:</span> <span class="nf">reloadSignal</span><span class="p">())</span> <span class="p">{</span> <span class="k">return</span> <span class="nf">dataPublisher</span><span class="p">()</span> <span class="p">}</span> </code></pre></div></div> <p>We can even respond to any of the above listed signals by merging them together first:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">consolidatedReloadSignal</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">AnyPublisher</span><span class="o">&lt;</span><span class="kt">Void</span><span class="p">,</span> <span class="kt">Never</span><span class="o">&gt;</span> <span class="p">{</span> <span class="k">return</span> <span class="kt">Publishers</span><span class="o">.</span><span class="kt">MergeMany</span><span class="p">(</span> <span class="nf">reloadSignal</span><span class="p">(),</span> <span class="nf">foregroundSignal</span><span class="p">(),</span> <span class="nf">networkReachableAgainSignal</span><span class="p">()</span> <span class="p">)</span> <span class="o">.</span><span class="nf">eraseToAnyPublisher</span><span class="p">()</span> <span class="p">}</span> </code></pre></div></div> <p>then using the consolidated signal instead:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">Publishers</span><span class="o">.</span><span class="kt">PublishAndRepeat</span><span class="p">(</span><span class="nv">onOutputFrom</span><span class="p">:</span> <span class="nf">consolidatedReloadSignal</span><span class="p">())</span> <span class="p">{</span> <span class="k">return</span> <span class="nf">dataPublisher</span><span class="p">()</span> <span class="p">}</span> </code></pre></div></div> <h2 id="accumulators">Accumulators</h2> <p>Suppose we want to build a Netflix-like homepage whose main structure is made of different topic rows (e.g. Comedy, Drama, Documentaries, etc.), each one presenting a list of associated medias. The topic list and medias for each topic are retrieved from a webservice through associated data emitters:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">topics</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">AnyPublisher</span><span class="o">&lt;</span><span class="p">[</span><span class="kt">Topic</span><span class="p">],</span> <span class="kt">Error</span><span class="o">&gt;</span> <span class="kd">func</span> <span class="nf">medias</span><span class="p">(</span><span class="n">forTopicId</span> <span class="nv">topicId</span><span class="p">:</span> <span class="kt">String</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">AnyPublisher</span><span class="o">&lt;</span><span class="p">[</span><span class="kt">Media</span><span class="p">],</span> <span class="kt">Error</span><span class="o">&gt;</span> </code></pre></div></div> <p>delivering instances of the following types:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">Topic</span> <span class="p">{</span> <span class="k">var</span> <span class="nv">id</span><span class="p">:</span> <span class="kt">String</span> <span class="k">var</span> <span class="nv">title</span><span class="p">:</span> <span class="kt">String</span> <span class="c1">// ...</span> <span class="p">}</span> <span class="kd">struct</span> <span class="kt">Media</span> <span class="p">{</span> <span class="k">var</span> <span class="nv">id</span><span class="p">:</span> <span class="kt">String</span> <span class="k">var</span> <span class="nv">title</span><span class="p">:</span> <span class="kt">String</span> <span class="c1">// ...</span> <span class="p">}</span> </code></pre></div></div> <p>We want to design a pipeline retrieving topics and their associated medias, consolidating them into rows:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">Row</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">topic</span><span class="p">:</span> <span class="kt">Topic</span> <span class="k">let</span> <span class="nv">medias</span><span class="p">:</span> <span class="p">[</span><span class="kt">Media</span><span class="p">]</span> <span class="p">}</span> </code></pre></div></div> <p>Since the number of topics is not known beforehand we cannot directly use <code class="language-plaintext highlighter-rouge">Publishers.CombineLatest</code> to retrieve medias once the topic list is known, as <code class="language-plaintext highlighter-rouge">Publishers.CombineLatest</code> does not support arbitrary lists of publishers (currently only 2, 3 or 4 publishers are supported, though <a href="https://github.com/apple/swift/blob/main/docs/GenericsManifesto.md#variadic-generics">variadic generics</a> might lift this restriction in the future).</p> <p>With simple bissection, though, we can implement a <code class="language-plaintext highlighter-rouge">Publishers.AccumulateLatestMany</code> publisher which supports an arbitrary number of publishers, returning their output in order as an array rather than as a tuple:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">extension</span> <span class="kt">Publishers</span> <span class="p">{</span> <span class="kd">static</span> <span class="kd">func</span> <span class="kt">AccumulateLatestMany</span><span class="o">&lt;</span><span class="kt">Upstream</span><span class="o">&gt;</span><span class="p">(</span><span class="n">_</span> <span class="nv">publishers</span><span class="p">:</span> <span class="kt">Upstream</span><span class="o">...</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">AnyPublisher</span><span class="o">&lt;</span><span class="p">[</span><span class="kt">Upstream</span><span class="o">.</span><span class="kt">Output</span><span class="p">],</span> <span class="kt">Upstream</span><span class="o">.</span><span class="kt">Failure</span><span class="o">&gt;</span> <span class="k">where</span> <span class="kt">Upstream</span><span class="p">:</span> <span class="kt">Publisher</span> <span class="p">{</span> <span class="k">return</span> <span class="kt">AccumulateLatestMany</span><span class="p">(</span><span class="n">publishers</span><span class="p">)</span> <span class="p">}</span> <span class="kd">static</span> <span class="kd">func</span> <span class="kt">AccumulateLatestMany</span><span class="o">&lt;</span><span class="kt">Upstream</span><span class="p">,</span> <span class="kt">S</span><span class="o">&gt;</span><span class="p">(</span><span class="n">_</span> <span class="nv">publishers</span><span class="p">:</span> <span class="kt">S</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">AnyPublisher</span><span class="o">&lt;</span><span class="p">[</span><span class="kt">Upstream</span><span class="o">.</span><span class="kt">Output</span><span class="p">],</span> <span class="kt">Upstream</span><span class="o">.</span><span class="kt">Failure</span><span class="o">&gt;</span> <span class="k">where</span> <span class="kt">Upstream</span><span class="p">:</span> <span class="kt">Publisher</span><span class="p">,</span> <span class="kt">S</span><span class="p">:</span> <span class="kt">Swift</span><span class="o">.</span><span class="kt">Sequence</span><span class="p">,</span> <span class="kt">S</span><span class="o">.</span><span class="kt">Element</span> <span class="o">==</span> <span class="kt">Upstream</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">publishersArray</span> <span class="o">=</span> <span class="kt">Array</span><span class="p">(</span><span class="n">publishers</span><span class="p">)</span> <span class="k">switch</span> <span class="n">publishersArray</span><span class="o">.</span><span class="n">count</span> <span class="p">{</span> <span class="k">case</span> <span class="mi">0</span><span class="p">:</span> <span class="k">return</span> <span class="kt">Just</span><span class="p">([])</span> <span class="o">.</span><span class="nf">setFailureType</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="kt">Upstream</span><span class="o">.</span><span class="kt">Failure</span><span class="o">.</span><span class="k">self</span><span class="p">)</span> <span class="o">.</span><span class="nf">eraseToAnyPublisher</span><span class="p">()</span> <span class="k">case</span> <span class="mi">1</span><span class="p">:</span> <span class="k">return</span> <span class="n">publishersArray</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="p">[</span><span class="nv">$0</span><span class="p">]</span> <span class="p">}</span> <span class="o">.</span><span class="nf">eraseToAnyPublisher</span><span class="p">()</span> <span class="k">case</span> <span class="mi">2</span><span class="p">:</span> <span class="k">return</span> <span class="kt">Publishers</span><span class="o">.</span><span class="kt">CombineLatest</span><span class="p">(</span><span class="n">publishersArray</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">publishersArray</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">t1</span><span class="p">,</span> <span class="n">t2</span> <span class="k">in</span> <span class="k">return</span> <span class="p">[</span><span class="n">t1</span><span class="p">,</span> <span class="n">t2</span><span class="p">]</span> <span class="p">}</span> <span class="o">.</span><span class="nf">eraseToAnyPublisher</span><span class="p">()</span> <span class="k">case</span> <span class="mi">3</span><span class="p">:</span> <span class="k">return</span> <span class="kt">Publishers</span><span class="o">.</span><span class="kt">CombineLatest3</span><span class="p">(</span><span class="n">publishersArray</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">publishersArray</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="n">publishersArray</span><span class="p">[</span><span class="mi">2</span><span class="p">])</span> <span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">t1</span><span class="p">,</span> <span class="n">t2</span><span class="p">,</span> <span class="n">t3</span> <span class="k">in</span> <span class="k">return</span> <span class="p">[</span><span class="n">t1</span><span class="p">,</span> <span class="n">t2</span><span class="p">,</span> <span class="n">t3</span><span class="p">]</span> <span class="p">}</span> <span class="o">.</span><span class="nf">eraseToAnyPublisher</span><span class="p">()</span> <span class="k">default</span><span class="p">:</span> <span class="k">let</span> <span class="nv">half</span> <span class="o">=</span> <span class="n">publishersArray</span><span class="o">.</span><span class="n">count</span> <span class="o">/</span> <span class="mi">2</span> <span class="k">return</span> <span class="kt">Publishers</span><span class="o">.</span><span class="kt">CombineLatest</span><span class="p">(</span> <span class="kt">AccumulateLatestMany</span><span class="p">(</span><span class="kt">Array</span><span class="p">(</span><span class="n">publishersArray</span><span class="p">[</span><span class="mi">0</span><span class="o">..&lt;</span><span class="n">half</span><span class="p">])),</span> <span class="kt">AccumulateLatestMany</span><span class="p">(</span><span class="kt">Array</span><span class="p">(</span><span class="n">publishersArray</span><span class="p">[</span><span class="n">half</span><span class="o">..&lt;</span><span class="n">publishersArray</span><span class="o">.</span><span class="n">count</span><span class="p">]))</span> <span class="p">)</span> <span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">array1</span><span class="p">,</span> <span class="n">array2</span> <span class="k">in</span> <span class="k">return</span> <span class="n">array1</span> <span class="o">+</span> <span class="n">array2</span> <span class="p">}</span> <span class="o">.</span><span class="nf">eraseToAnyPublisher</span><span class="p">()</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>With the help of <code class="language-plaintext highlighter-rouge">Publishers.AccumulateLatestMany</code> we can now write a publisher for our Netflix-like homepage:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">rowsPublisher</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">AnyPublisher</span><span class="o">&lt;</span><span class="p">[</span><span class="kt">Row</span><span class="p">],</span> <span class="kt">Error</span><span class="o">&gt;</span> <span class="p">{</span> <span class="k">return</span> <span class="nf">topics</span><span class="p">()</span> <span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">topics</span> <span class="k">in</span> <span class="k">return</span> <span class="kt">Publishers</span><span class="o">.</span><span class="kt">AccumulateLatestMany</span><span class="p">(</span><span class="n">topics</span><span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">topic</span> <span class="k">in</span> <span class="k">return</span> <span class="nf">medias</span><span class="p">(</span><span class="nv">forTopicId</span><span class="p">:</span> <span class="n">topic</span><span class="o">.</span><span class="n">id</span><span class="p">)</span> <span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="kt">Row</span><span class="p">(</span><span class="nv">topic</span><span class="p">:</span> <span class="n">topic</span><span class="p">,</span> <span class="nv">medias</span><span class="p">:</span> <span class="nv">$0</span><span class="p">)</span> <span class="p">}</span> <span class="p">})</span> <span class="p">}</span> <span class="o">.</span><span class="nf">switchToLatest</span><span class="p">()</span> <span class="o">.</span><span class="nf">eraseToAnyPublisher</span><span class="p">()</span> <span class="p">}</span> </code></pre></div></div> <p>This publisher is kept as simple as possible but could be improved in several ways:</p> <ul> <li>Since <code class="language-plaintext highlighter-rouge">Publishers.AccumulateLatestMany</code> is based on <code class="language-plaintext highlighter-rouge">Publishers.CombineLatest</code>, a result is only delivered by the pipeline once all rows could be loaded. In general, though, we would prefer delivering rows as they are retrieved. This behavior can be implemented by immediately emitting a row before performing the <code class="language-plaintext highlighter-rouge">medias(forTopicId:)</code> request using the <code class="language-plaintext highlighter-rouge">prepend</code> operator.</li> <li>Similarly, if a media request fails for some topic, the entire pipeline fails. Again this can be improved by using <code class="language-plaintext highlighter-rouge">replaceError</code> to emit a row instead.</li> </ul> <p>A good row value to use in both cases is a list of placeholder medias, or a list of previously retrieved medias. Please have a look at the <a href="#sample-code">sample code</a> associated with this article to see how this can be achieved in practice.</p> <h2 id="advanced-pipeline-design">Advanced Pipeline Design</h2> <p>With <a href="#triggers-and-signals">triggers</a>, <a href="#triggers-and-signals">signals</a>, <a href="#refresh-publishers">refresh publishers</a>, <a href="#accumulators">accumulators</a> and a recipe to implement <a href="#paginated-publishers">paginated publishers</a>, we can now write a single declarative pipeline for our Netflix-like homepage which not only retrieves topics and their medias in order, but also supports global reloads and individual pagination per topic.</p> <p>Let us assume that media lists per topic support pagination. We can augment the API contract to support pagination via trigger and signals as described in the <a href="#paginated-publishers">Paginated Publishers</a> section:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">topics</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">AnyPublisher</span><span class="o">&lt;</span><span class="p">[</span><span class="kt">Topic</span><span class="p">],</span> <span class="kt">Error</span><span class="o">&gt;</span> <span class="kd">func</span> <span class="nf">medias</span><span class="p">(</span><span class="n">forTopicId</span> <span class="nv">topicId</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> <span class="n">paginatedBy</span> <span class="nv">paginator</span><span class="p">:</span> <span class="kt">Trigger</span><span class="o">.</span><span class="kt">Signal</span><span class="p">?</span> <span class="o">=</span> <span class="kc">nil</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">AnyPublisher</span><span class="o">&lt;</span><span class="p">[</span><span class="kt">Media</span><span class="p">],</span> <span class="kt">Error</span><span class="o">&gt;</span> </code></pre></div></div> <p>On a Netflix-like homepage, where media lists are displayed as horizontally scrollable rows for each topic, we want to be able to load more content when the user scrolls to the end of a row. Moreover, we want a pull-to-refresh to be available so that the user can reload the entire screen when desired. Let us introduce a hashable enum describing these use cases:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">enum</span> <span class="kt">TriggerId</span><span class="p">:</span> <span class="kt">Hashable</span> <span class="p">{</span> <span class="k">case</span> <span class="n">reload</span> <span class="k">case</span> <span class="nf">loadMore</span><span class="p">(</span><span class="nv">topicId</span><span class="p">:</span> <span class="kt">String</span><span class="p">)</span> <span class="p">}</span> </code></pre></div></div> <p>Also assume we have a trigger stored somewhere:<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup></p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">trigger</span> <span class="o">=</span> <span class="kt">Trigger</span><span class="p">()</span> </code></pre></div></div> <p>We can now enhance our <code class="language-plaintext highlighter-rouge">rowsPublisher()</code> pipeline to add support for global reloads and pagination in each section:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">rowsPublisher</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">AnyPublisher</span><span class="o">&lt;</span><span class="p">[</span><span class="kt">Row</span><span class="p">],</span> <span class="kt">Error</span><span class="o">&gt;</span> <span class="p">{</span> <span class="k">return</span> <span class="kt">Publishers</span><span class="o">.</span><span class="kt">PublishAndRepeat</span><span class="p">(</span><span class="nv">onOutputFrom</span><span class="p">:</span> <span class="n">trigger</span><span class="o">.</span><span class="nf">signal</span><span class="p">(</span><span class="nv">activatedBy</span><span class="p">:</span> <span class="kt">TriggerId</span><span class="o">.</span><span class="n">reload</span><span class="p">))</span> <span class="p">{</span> <span class="p">[</span><span class="n">trigger</span><span class="p">]</span> <span class="k">in</span> <span class="k">return</span> <span class="nf">topics</span><span class="p">()</span> <span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">topics</span> <span class="k">in</span> <span class="k">return</span> <span class="kt">Publishers</span><span class="o">.</span><span class="kt">AccumulateLatestMany</span><span class="p">(</span><span class="n">topics</span><span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">topic</span> <span class="k">in</span> <span class="k">return</span> <span class="nf">medias</span><span class="p">(</span><span class="nv">forTopicId</span><span class="p">:</span> <span class="n">topic</span><span class="o">.</span><span class="n">id</span><span class="p">,</span> <span class="nv">paginatedBy</span><span class="p">:</span> <span class="kt">TriggerId</span><span class="o">.</span><span class="nf">loadMore</span><span class="p">(</span><span class="nv">topicId</span><span class="p">:</span> <span class="n">topic</span><span class="o">.</span><span class="n">id</span><span class="p">))</span> <span class="o">.</span><span class="nf">scan</span><span class="p">([])</span> <span class="p">{</span> <span class="nv">$0</span> <span class="o">+</span> <span class="nv">$1</span> <span class="p">}</span> <span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="kt">Row</span><span class="p">(</span><span class="nv">topic</span><span class="p">:</span> <span class="n">topic</span><span class="p">,</span> <span class="nv">medias</span><span class="p">:</span> <span class="nv">$0</span><span class="p">)</span> <span class="p">}</span> <span class="p">})</span> <span class="p">}</span> <span class="o">.</span><span class="nf">switchToLatest</span><span class="p">()</span> <span class="o">.</span><span class="nf">eraseToAnyPublisher</span><span class="p">()</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>Only three changes were required in comparison to the pipeline we obtained at the end of the <a href="#accumulators">Accumulators</a> section:</p> <ul> <li><code class="language-plaintext highlighter-rouge">Publishers.PublishAndRepeat</code> ensures the pipeline is executed once and repeated when a reload is triggered.</li> <li>A paginator signal is provided to <code class="language-plaintext highlighter-rouge">medias(forTopicId:paginatedBy:)</code> so that more medias can be loaded for any topic, independently and on-demand.</li> <li><code class="language-plaintext highlighter-rouge">scan</code> is used to gather pages of medias returned by the paginated <code class="language-plaintext highlighter-rouge">medias(forTopicId:paginatedBy:)</code> publisher.</li> </ul> <p>We proceeded here like you would in practice, namely by starting with a basic pipeline implementing the nominal case, then inserting signals where appropriate to deliver more data or repeat part or the entirety of the pipeline. This approach is especially nice, as it allows a pipeline to be gradually enhanced with surgical code changes. Think about the code you would have written if you had to do the same with block-based APIs and you will probably better understand the advantages of this approach.</p> <p>Even better, you can very easily throttle data delivery (e.g. to avoid signals triggering too many unnecessary reloads in part of the pipeline) or use debouncing to add some delay (e.g. if a signal is bound to keyboard input), with only a few additional operator insertions. This is an area where reactive programming really shines in comparison to more imperative approaches which would require much more convoluted code to be written.</p> <h2 id="declarative-data-pipelines-and-view-models">Declarative Data Pipelines and View Models</h2> <p>Declarative data pipelines are especially useful when writing view models conforming to <code class="language-plaintext highlighter-rouge">ObservableObject</code>. Since pipelines deliver consolidated results they can be immediately wired to a published <code class="language-plaintext highlighter-rouge">state</code> property so that the view automatically reflects its changes.</p> <p>For example our Netflix-like homepage could be driven by the following view model, which is based on our <code class="language-plaintext highlighter-rouge">rowsPublisher()</code> from the previous section, whose result is wrapped into a <code class="language-plaintext highlighter-rouge">State</code>:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">final</span> <span class="kd">class</span> <span class="kt">HomepageViewModel</span><span class="p">:</span> <span class="kt">ObservableObject</span> <span class="p">{</span> <span class="kd">enum</span> <span class="kt">State</span> <span class="p">{</span> <span class="k">case</span> <span class="n">loading</span> <span class="k">case</span> <span class="nf">loaded</span><span class="p">(</span><span class="nv">rows</span><span class="p">:</span> <span class="p">[</span><span class="kt">Row</span><span class="p">])</span> <span class="k">case</span> <span class="nf">failure</span><span class="p">(</span><span class="nv">error</span><span class="p">:</span> <span class="kt">Error</span><span class="p">)</span> <span class="p">}</span> <span class="kd">@Published</span> <span class="kd">private(set)</span> <span class="k">var</span> <span class="nv">state</span><span class="p">:</span> <span class="kt">State</span> <span class="o">=</span> <span class="o">.</span><span class="n">loading</span> <span class="kd">private</span> <span class="k">let</span> <span class="nv">trigger</span> <span class="o">=</span> <span class="kt">Trigger</span><span class="p">()</span> <span class="nf">init</span><span class="p">()</span> <span class="p">{</span> <span class="kt">Publishers</span><span class="o">.</span><span class="kt">PublishAndRepeat</span><span class="p">(</span><span class="nv">onOutputFrom</span><span class="p">:</span> <span class="nf">consolidatedReloadSignal</span><span class="p">())</span> <span class="p">{</span> <span class="p">[</span><span class="n">trigger</span><span class="p">]</span> <span class="k">in</span> <span class="k">return</span> <span class="nf">topics</span><span class="p">()</span> <span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">topics</span> <span class="k">in</span> <span class="k">return</span> <span class="kt">Publishers</span><span class="o">.</span><span class="kt">AccumulateLatestMany</span><span class="p">(</span><span class="n">topics</span><span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">topic</span> <span class="k">in</span> <span class="k">return</span> <span class="nf">medias</span><span class="p">(</span><span class="nv">forTopicId</span><span class="p">:</span> <span class="n">topic</span><span class="o">.</span><span class="n">id</span><span class="p">,</span> <span class="nv">paginatedBy</span><span class="p">:</span> <span class="kt">TriggerId</span><span class="o">.</span><span class="nf">loadMore</span><span class="p">(</span><span class="nv">topicId</span><span class="p">:</span> <span class="n">topic</span><span class="o">.</span><span class="n">id</span><span class="p">))</span> <span class="o">.</span><span class="nf">scan</span><span class="p">([])</span> <span class="p">{</span> <span class="nv">$0</span> <span class="o">+</span> <span class="nv">$1</span> <span class="p">}</span> <span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="kt">Row</span><span class="p">(</span><span class="nv">topic</span><span class="p">:</span> <span class="n">topic</span><span class="p">,</span> <span class="nv">medias</span><span class="p">:</span> <span class="nv">$0</span><span class="p">)</span> <span class="p">}</span> <span class="p">})</span> <span class="p">}</span> <span class="o">.</span><span class="nf">switchToLatest</span><span class="p">()</span> <span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="kt">State</span><span class="o">.</span><span class="nf">loaded</span><span class="p">(</span><span class="nv">rows</span><span class="p">:</span> <span class="nv">$0</span><span class="p">)</span> <span class="p">}</span> <span class="o">.</span><span class="k">catch</span> <span class="p">{</span> <span class="n">error</span> <span class="k">in</span> <span class="k">return</span> <span class="kt">Just</span><span class="p">(</span><span class="kt">State</span><span class="o">.</span><span class="nf">failure</span><span class="p">(</span><span class="nv">error</span><span class="p">:</span> <span class="n">error</span><span class="p">))</span> <span class="p">}</span> <span class="o">.</span><span class="nf">eraseToAnyPublisher</span><span class="p">()</span> <span class="p">}</span> <span class="o">.</span><span class="nf">receive</span><span class="p">(</span><span class="nv">on</span><span class="p">:</span> <span class="kt">DispatchQueue</span><span class="o">.</span><span class="n">main</span><span class="p">)</span> <span class="o">.</span><span class="nf">assign</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="o">&amp;</span><span class="err">$</span><span class="n">state</span><span class="p">)</span> <span class="p">}</span> <span class="kd">func</span> <span class="nf">reload</span><span class="p">()</span> <span class="p">{</span> <span class="n">trigger</span><span class="o">.</span><span class="nf">activate</span><span class="p">(</span><span class="nv">for</span><span class="p">:</span> <span class="kt">TriggerId</span><span class="o">.</span><span class="n">reload</span><span class="p">)</span> <span class="p">}</span> <span class="kd">func</span> <span class="nf">loadMore</span><span class="p">(</span><span class="k">for</span> <span class="nv">topic</span><span class="p">:</span> <span class="kt">Topic</span><span class="p">)</span> <span class="p">{</span> <span class="n">trigger</span><span class="o">.</span><span class="nf">activate</span><span class="p">(</span><span class="nv">for</span><span class="p">:</span> <span class="kt">TriggerId</span><span class="o">.</span><span class="nf">loadMore</span><span class="p">(</span><span class="nv">topicId</span><span class="p">:</span> <span class="n">topic</span><span class="o">.</span><span class="n">id</span><span class="p">))</span> <span class="p">}</span> <span class="kd">private</span> <span class="kd">func</span> <span class="nf">consolidatedReloadSignal</span><span class="p">()</span> <span class="p">{</span> <span class="k">return</span> <span class="kt">Publishers</span><span class="o">.</span><span class="kt">Merge</span><span class="p">(</span> <span class="n">trigger</span><span class="o">.</span><span class="nf">signal</span><span class="p">(</span><span class="nv">activatedBy</span><span class="p">:</span> <span class="kt">TriggerId</span><span class="o">.</span><span class="n">reload</span><span class="p">),</span> <span class="nf">networkReachableAgainSignal</span><span class="p">(),</span> <span class="nf">foregroundSignal</span><span class="p">()</span> <span class="p">)</span> <span class="o">.</span><span class="nf">eraseToAnyPublisher</span><span class="p">()</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>By assigning the publisher to the <code class="language-plaintext highlighter-rouge">state</code> property publisher using the <code class="language-plaintext highlighter-rouge">assign(to:)</code> <a href="https://developer.apple.com/documentation/combine/fail/assign(to:)">operator</a>, we bind the lifetime of our pipeline to the lifetime of the <code class="language-plaintext highlighter-rouge">state</code> property and therefore to the <code class="language-plaintext highlighter-rouge">HomepageViewModel</code> instance itself. This ensures correct resource management without the need for explicit <a href="https://developer.apple.com/documentation/combine/anycancellable">cancellables</a>. We also catch errors and replace them with a new publisher <a href="https://heckj.github.io/swiftui-notes/#patterns-continual-error-handling">so that the pipeline never finishes</a>, even in case of failure.</p> <p>Here is a visual representation of the pipeline, with reloads and pagination indicated:</p> <p><img src="https://docs.google.com/drawings/d/e/2PACX-1vRNq4ErM5DzV1rCbhaEVjVr7vwvq94rsqppE6TofemGR5KnrI1OtUfERXSrgi9k9_UnsL6gWXVgNmVy/pub?w=1099&amp;h=628" alt="Homepage Pipeline" /></p> <p>Just take a deep breath and consider which features the above view model provides in ~50 lines of code:</p> <ul> <li>The model delivers state updates in a reactive way, with loading and failure states, as well as a loaded state with the relevant content attached.</li> <li>It performs a request for topics and, for each one, performs another request to gather medias associated with it.</li> <li>It provides support for reloading, either in response to user interaction (e.g. pull-to-refresh) or in response to application environment changes.</li> <li>More medias can be individually loaded for each topic (as the user scrolls, for example).</li> <li>If a reload occurs while more medias are still being loaded for one or several rows, all pipelines are properly cancelled accordingly.</li> </ul> <h4 class="no_toc" id="remark-1">Remark</h4> <p>There is no way to cancel subscriptions made with the <code class="language-plaintext highlighter-rouge">assign(to:)</code> operator since no <code class="language-plaintext highlighter-rouge">AnyCancellable</code> is returned. This means that associated subscriptions remain valid until the parent <code class="language-plaintext highlighter-rouge">ObservableObject</code> is deinitialized.</p> <p>Also note that calling <code class="language-plaintext highlighter-rouge">assign(to:)</code> several times on the same published property does not replace existing subscriptions. New subscriptions will pile up instead, which is why you should avoid code that calls <code class="language-plaintext highlighter-rouge">assign(to:)</code> unnecessarily.</p> <p>For these reasons declaring a reactive pipeline from a designated intializer is a good practice, especially when this pipeline is wired to a published property using <code class="language-plaintext highlighter-rouge">assign(to:)</code>.</p> <p>Sometimes a pipeline might depend on parameters supplied by the user, though, for example a search criterium or a username and password pair. Such parameters are not known at initialization time and you might wonder how their updated values can be provided to an existing pipeline. The next section will show you how this can be achieved.</p> <h2 id="user-driven-data-emitters">User-driven Data Emitters</h2> <p>Assume our example webservice provides an additional endpoint to search medias using some query and additional settings. We introduce a corresponding data emitter:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">medias</span><span class="p">(</span><span class="n">matchingQuery</span> <span class="nv">query</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> <span class="nv">settings</span><span class="p">:</span> <span class="kt">Settings</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">AnyPublisher</span><span class="o">&lt;</span><span class="p">[</span><span class="kt">Media</span><span class="p">],</span> <span class="kt">Error</span><span class="o">&gt;</span> </code></pre></div></div> <p>with settings letting the user pick a date range or a sort order, for example:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">Setting</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">dateRange</span><span class="p">:</span> <span class="kt">DateRange</span><span class="p">?</span> <span class="k">let</span> <span class="nv">ascending</span><span class="p">:</span> <span class="kt">Bool</span> <span class="c1">// ...</span> <span class="p">}</span> </code></pre></div></div> <p>In most scenarios the above emitter would likely support pagination as described in the <a href="#paginated-publishers">Paginated Publishers</a> section, but this is left as an exercise for the reader. As usual the details of the data emitter implementation are not important, only its signature is.</p> <p>Similar to what we did in the <a href="#declarative-data-pipelines-and-view-models">previous section</a> we can now implement the view model of a basic search screen. This view model must support query and setting updates made by the user. We declare a pipeline in the view model initializer, starting our implementation with constant query and settings:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">final</span> <span class="kd">class</span> <span class="kt">SearchViewModel</span><span class="p">:</span> <span class="kt">ObservableObject</span> <span class="p">{</span> <span class="kd">enum</span> <span class="kt">State</span> <span class="p">{</span> <span class="k">case</span> <span class="n">loading</span> <span class="k">case</span> <span class="nf">loaded</span><span class="p">(</span><span class="nv">medias</span><span class="p">:</span> <span class="p">[</span><span class="kt">Media</span><span class="p">])</span> <span class="k">case</span> <span class="nf">failure</span><span class="p">(</span><span class="nv">error</span><span class="p">:</span> <span class="kt">Error</span><span class="p">)</span> <span class="p">}</span> <span class="k">let</span> <span class="nv">query</span> <span class="o">=</span> <span class="s">""</span> <span class="k">let</span> <span class="nv">settings</span> <span class="o">=</span> <span class="kt">Settings</span><span class="p">()</span> <span class="kd">@Published</span> <span class="kd">private(set)</span> <span class="k">var</span> <span class="nv">state</span><span class="p">:</span> <span class="kt">State</span> <span class="o">=</span> <span class="o">.</span><span class="n">loading</span> <span class="nf">init</span><span class="p">()</span> <span class="p">{</span> <span class="kt">Publishers</span><span class="o">.</span><span class="kt">PublishAndRepeat</span><span class="p">(</span><span class="nv">onOutputFrom</span><span class="p">:</span> <span class="nf">consolidatedReloadSignal</span><span class="p">())</span> <span class="p">{</span> <span class="p">[</span><span class="n">query</span><span class="p">,</span> <span class="n">settings</span><span class="p">]</span> <span class="k">in</span> <span class="k">return</span> <span class="nf">medias</span><span class="p">(</span><span class="nv">matchingQuery</span><span class="p">:</span> <span class="n">query</span><span class="p">,</span> <span class="nv">settings</span><span class="p">:</span> <span class="n">settings</span><span class="p">)</span> <span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="kt">State</span><span class="o">.</span><span class="nf">loaded</span><span class="p">(</span><span class="nv">medias</span><span class="p">:</span> <span class="nv">$0</span><span class="p">)</span> <span class="p">}</span> <span class="o">.</span><span class="k">catch</span> <span class="p">{</span> <span class="n">error</span> <span class="k">in</span> <span class="k">return</span> <span class="kt">Just</span><span class="p">(</span><span class="kt">State</span><span class="o">.</span><span class="nf">failure</span><span class="p">(</span><span class="nv">error</span><span class="p">:</span> <span class="n">error</span><span class="p">))</span> <span class="p">}</span> <span class="o">.</span><span class="nf">eraseToAnyPublisher</span><span class="p">()</span> <span class="p">}</span> <span class="o">.</span><span class="nf">receive</span><span class="p">(</span><span class="nv">on</span><span class="p">:</span> <span class="kt">DispatchQueue</span><span class="o">.</span><span class="n">main</span><span class="p">)</span> <span class="o">.</span><span class="nf">assign</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="o">&amp;</span><span class="err">$</span><span class="n">state</span><span class="p">)</span> <span class="p">}</span> <span class="c1">// ...</span> <span class="p">}</span> </code></pre></div></div> <p>We implement pull-to-refresh and respond to the application being woken up by using the same <code class="language-plaintext highlighter-rouge">consolidatedReloadSignal()</code> signal introduced in the previous section (details are therefore omitted here). Note that the use of <code class="language-plaintext highlighter-rouge">Publishers.PublishAndRepeat(onOutputFrom:)</code> requires the query and settings to be accessible within the associated closure, which is here achieved through a capture list.</p> <p>The query and settings must support updates made by the user, but simply turning associated properties into <code class="language-plaintext highlighter-rouge">var</code>s does not work:</p> <ul> <li>Since <code class="language-plaintext highlighter-rouge">String</code> and <code class="language-plaintext highlighter-rouge">Settings</code> are value types, their <em>initial</em> value is captured. Updates will never be visible in the closure.</li> <li>There is no way to reload the pipeline in response to these values being <em>changed</em>.</li> </ul> <p>The need to respond to change is a hint about how we can solve both problems. Instead of simple mutable properties, let us namely introduce <code class="language-plaintext highlighter-rouge">@Published</code> properties:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">final</span> <span class="kd">class</span> <span class="kt">SearchViewModel</span><span class="p">:</span> <span class="kt">ObservableObject</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="kd">@Published</span> <span class="k">var</span> <span class="nv">query</span> <span class="o">=</span> <span class="s">""</span> <span class="kd">@Published</span> <span class="k">var</span> <span class="nv">settings</span> <span class="o">=</span> <span class="kt">Settings</span><span class="p">()</span> <span class="c1">// ...</span> <span class="p">}</span> </code></pre></div></div> <p>The publishers associated with these properties can now be captured and inserted into the pipeline so that search results are updated when the query or settings change:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">final</span> <span class="kd">class</span> <span class="kt">SearchViewModel</span><span class="p">:</span> <span class="kt">ObservableObject</span> <span class="p">{</span> <span class="kd">enum</span> <span class="kt">State</span> <span class="p">{</span> <span class="k">case</span> <span class="n">loading</span> <span class="k">case</span> <span class="nf">loaded</span><span class="p">(</span><span class="nv">medias</span><span class="p">:</span> <span class="p">[</span><span class="kt">Media</span><span class="p">])</span> <span class="k">case</span> <span class="nf">failure</span><span class="p">(</span><span class="nv">error</span><span class="p">:</span> <span class="kt">Error</span><span class="p">)</span> <span class="p">}</span> <span class="kd">@Published</span> <span class="k">var</span> <span class="nv">query</span> <span class="o">=</span> <span class="s">""</span> <span class="kd">@Published</span> <span class="k">var</span> <span class="nv">settings</span> <span class="o">=</span> <span class="kt">Settings</span><span class="p">()</span> <span class="kd">@Published</span> <span class="kd">private(set)</span> <span class="k">var</span> <span class="nv">state</span><span class="p">:</span> <span class="kt">State</span> <span class="o">=</span> <span class="o">.</span><span class="n">loading</span> <span class="nf">init</span><span class="p">()</span> <span class="p">{</span> <span class="kt">Publishers</span><span class="o">.</span><span class="kt">PublishAndRepeat</span><span class="p">(</span><span class="nv">onOutputFrom</span><span class="p">:</span> <span class="nf">consolidatedReloadSignal</span><span class="p">())</span> <span class="p">{</span> <span class="p">[</span><span class="err">$</span><span class="n">query</span><span class="p">,</span> <span class="err">$</span><span class="n">settings</span><span class="p">]</span> <span class="k">in</span> <span class="kt">Publishers</span><span class="o">.</span><span class="kt">CombineLatest</span><span class="p">(</span><span class="err">$</span><span class="n">query</span><span class="p">,</span> <span class="err">$</span><span class="n">settings</span><span class="p">)</span> <span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">query</span><span class="p">,</span> <span class="n">settings</span> <span class="k">in</span> <span class="k">return</span> <span class="nf">medias</span><span class="p">(</span><span class="nv">matchingQuery</span><span class="p">:</span> <span class="n">query</span><span class="p">,</span> <span class="nv">settings</span><span class="p">:</span> <span class="n">settings</span><span class="p">)</span> <span class="p">}</span> <span class="o">.</span><span class="nf">switchToLatest</span><span class="p">()</span> <span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="kt">State</span><span class="o">.</span><span class="nf">loaded</span><span class="p">(</span><span class="nv">medias</span><span class="p">:</span> <span class="nv">$0</span><span class="p">)</span> <span class="p">}</span> <span class="o">.</span><span class="nf">prepend</span><span class="p">(</span><span class="kt">State</span><span class="o">.</span><span class="n">loading</span><span class="p">)</span> <span class="o">.</span><span class="k">catch</span> <span class="p">{</span> <span class="n">error</span> <span class="k">in</span> <span class="k">return</span> <span class="kt">Just</span><span class="p">(</span><span class="kt">State</span><span class="o">.</span><span class="nf">failure</span><span class="p">(</span><span class="nv">error</span><span class="p">:</span> <span class="n">error</span><span class="p">))</span> <span class="p">}</span> <span class="o">.</span><span class="nf">eraseToAnyPublisher</span><span class="p">()</span> <span class="p">}</span> <span class="o">.</span><span class="nf">receive</span><span class="p">(</span><span class="nv">on</span><span class="p">:</span> <span class="kt">DispatchQueue</span><span class="o">.</span><span class="n">main</span><span class="p">)</span> <span class="o">.</span><span class="nf">assign</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="o">&amp;</span><span class="err">$</span><span class="n">state</span><span class="p">)</span> <span class="p">}</span> <span class="c1">// ...</span> <span class="p">}</span> </code></pre></div></div> <p>The above code deserves a brief explanation:</p> <ul> <li>Query and setting publishers are assembled using <code class="language-plaintext highlighter-rouge">Publishers.CombineLatest</code> so that any update received from them triggers a new search. Note that <code class="language-plaintext highlighter-rouge">Publishers.CombineLatest</code> <a href="https://developer.apple.com/documentation/combine/just/combinelatest(_:)">requires each involved publisher to emit a value before emitting its first value</a>, which is here guaranteed since <code class="language-plaintext highlighter-rouge">@Published</code> publishers provide their initial value automatically upon subscription. The pipeline therefore always executes once.</li> <li>A usual <code class="language-plaintext highlighter-rouge">map</code> and <code class="language-plaintext highlighter-rouge">switchToLatest</code> <a href="https://heckj.github.io/swiftui-notes/#reference-switchtolatest">combination</a> is used to produce a single request matching the latest search parameters, ensuring prior requests are properly discarded.</li> <li>The <code class="language-plaintext highlighter-rouge">prepend</code> operator is used to reset the state to <code class="language-plaintext highlighter-rouge">State.loading</code> before each new search attempt.</li> </ul> <p>As usual we can easily tweak this pipeline further, for example to avoid performing a new request if the query did not change, or to ensure requests are not immediately sent while the user is still typing or updating search settings:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">final</span> <span class="kd">class</span> <span class="kt">SearchViewModel</span><span class="p">:</span> <span class="kt">ObservableObject</span> <span class="p">{</span> <span class="kd">enum</span> <span class="kt">State</span> <span class="p">{</span> <span class="k">case</span> <span class="n">loading</span> <span class="k">case</span> <span class="nf">loaded</span><span class="p">(</span><span class="nv">medias</span><span class="p">:</span> <span class="p">[</span><span class="kt">Media</span><span class="p">])</span> <span class="k">case</span> <span class="nf">failure</span><span class="p">(</span><span class="nv">error</span><span class="p">:</span> <span class="kt">Error</span><span class="p">)</span> <span class="p">}</span> <span class="kd">@Published</span> <span class="k">var</span> <span class="nv">query</span> <span class="o">=</span> <span class="s">""</span> <span class="kd">@Published</span> <span class="k">var</span> <span class="nv">settings</span> <span class="o">=</span> <span class="kt">Settings</span><span class="p">()</span> <span class="kd">@Published</span> <span class="kd">private(set)</span> <span class="k">var</span> <span class="nv">state</span><span class="p">:</span> <span class="kt">State</span> <span class="o">=</span> <span class="o">.</span><span class="n">loading</span> <span class="nf">init</span><span class="p">()</span> <span class="p">{</span> <span class="kt">Publishers</span><span class="o">.</span><span class="kt">PublishAndRepeat</span><span class="p">(</span><span class="nv">onOutputFrom</span><span class="p">:</span> <span class="nf">consolidatedReloadSignal</span><span class="p">())</span> <span class="p">{</span> <span class="p">[</span><span class="err">$</span><span class="n">query</span><span class="p">,</span> <span class="err">$</span><span class="n">settings</span><span class="p">]</span> <span class="k">in</span> <span class="kt">Publishers</span><span class="o">.</span><span class="kt">CombineLatest</span><span class="p">(</span><span class="err">$</span><span class="n">query</span><span class="o">.</span><span class="nf">removeDuplicates</span><span class="p">(),</span> <span class="err">$</span><span class="n">settings</span><span class="p">)</span> <span class="o">.</span><span class="nf">debounce</span><span class="p">(</span><span class="nv">for</span><span class="p">:</span> <span class="mf">0.3</span><span class="p">,</span> <span class="nv">scheduler</span><span class="p">:</span> <span class="kt">DispatchQueue</span><span class="o">.</span><span class="n">main</span><span class="p">)</span> <span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">query</span><span class="p">,</span> <span class="n">settings</span> <span class="k">in</span> <span class="k">return</span> <span class="nf">medias</span><span class="p">(</span><span class="nv">matchingQuery</span><span class="p">:</span> <span class="n">query</span><span class="p">,</span> <span class="nv">settings</span><span class="p">:</span> <span class="n">settings</span><span class="p">)</span> <span class="p">}</span> <span class="o">.</span><span class="nf">switchToLatest</span><span class="p">()</span> <span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="kt">State</span><span class="o">.</span><span class="nf">loaded</span><span class="p">(</span><span class="nv">medias</span><span class="p">:</span> <span class="nv">$0</span><span class="p">)</span> <span class="p">}</span> <span class="o">.</span><span class="nf">prepend</span><span class="p">(</span><span class="kt">State</span><span class="o">.</span><span class="n">loading</span><span class="p">)</span> <span class="o">.</span><span class="k">catch</span> <span class="p">{</span> <span class="n">error</span> <span class="k">in</span> <span class="k">return</span> <span class="kt">Just</span><span class="p">(</span><span class="kt">State</span><span class="o">.</span><span class="nf">failure</span><span class="p">(</span><span class="nv">error</span><span class="p">:</span> <span class="n">error</span><span class="p">))</span> <span class="p">}</span> <span class="o">.</span><span class="nf">eraseToAnyPublisher</span><span class="p">()</span> <span class="p">}</span> <span class="o">.</span><span class="nf">receive</span><span class="p">(</span><span class="nv">on</span><span class="p">:</span> <span class="kt">DispatchQueue</span><span class="o">.</span><span class="n">main</span><span class="p">)</span> <span class="o">.</span><span class="nf">assign</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="o">&amp;</span><span class="err">$</span><span class="n">state</span><span class="p">)</span> <span class="p">}</span> <span class="c1">// ...</span> <span class="p">}</span> </code></pre></div></div> <h2 id="conclusion">Conclusion</h2> <p>In this article we illustrated how reactive programming and Combine can be used to build data delivery pipelines in a declarative way. The strategies we elaborated are not only scalable, but also eliminate challenges associated with shared mutable states. This neatly avoid issues commonly encountered when aggregating several asynchronous data sources using imperative block-based or async/await-based approaches.</p> <p>Key concepts we introduced are signals and triggers, which can be inserted as control points into any pipeline, as well as a <code class="language-plaintext highlighter-rouge">wait(untilOutputFrom:)</code> operator which can be used to implement publishers natively supporting signal-based pagination. We also introduced <code class="language-plaintext highlighter-rouge">Publishers.AccumulateLatestMany</code> to solve limitations of <code class="language-plaintext highlighter-rouge">Publishers.CombineLatest</code>, especially when an arbitrary number of publishers must deliver their results in a specific order.</p> <p>Finally, we applied this declarative formalism to view model building, opening the door to implementations where view and view models are built declaratively.<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">3</a></sup> Exactly like a SwiftUI <code class="language-plaintext highlighter-rouge">View</code> is a view recipe, focusing on the desired result rather than on its actual implementation, a declarative pipeline is namely a data delivery recipe and as such is a perfect fit for view models.</p> <p>The approach discussed in this article has of course a few drawbacks. Combine and reactive programming have a steep learning curve, and compiler messages might be cryptic, though experience definitely helps. Pipeline design also requires special care, as it can be quite surgical. There is definitely an entry fee involved, but hopefully this article helped you realize the return might be worth the investment.</p> <p>Finally, and though this discussion was focused on reactive programming with Combine, all the concepts introduced in this article could likely be transposed to any other declarative framework. For this reason I hope this article can be insightful outside Apple or Swift development communities as well, or useful to people using reactive frameworks like ReactiveSwift or RxSwift.</p> <h2 id="sample-code">Sample Code</h2> <p>Sample code is provided <a href="https://github.com/defagos/DeclarativeCombine">on GitHub</a>. The Combine toolset built in this article is provided as a Swift package associated with the project, which implements a basic Netflix-like homepage as described in this article, and built using SwiftUI. The underlying view model includes a few improvements in comparison to the one discussed in this article, mostly to display placeholders while loading content or if a media list cannot be loaded for some reason.</p> <div class="footnotes" role="doc-endnotes"> <ol> <li id="fn:1" role="doc-endnote"> <p>As of Xcode 13.2 async/await are backward compatible with the same minimum OS versions as Combine. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p> </li> <li id="fn:2" role="doc-endnote"> <p>Recall that we use a global scope for simplicity, but in this case trigger and row publishers would likely be stored in a class (therefore the capture list used to make <code class="language-plaintext highlighter-rouge">trigger</code> accessible within the block). <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p> </li> <li id="fn:4" role="doc-endnote"> <p>SwiftUI or UIKit diffable data sources and compositional layouts, for example, nicely support this philosophy. <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p> </li> </ol> </div>Any non-trivial application uses some form of data, whether retrieved from a web service, from a database or generated by user interaction. A well-behaved application properly responds to data changing over time while preserving a delightful uninterrupted user experience. Achieving this result can be a challenge, though, as most applications rely on several data sources whose changes might require a user interface update at any time.Understanding SwiftUI Layout Behaviors2021-06-03T00:00:00+00:002021-06-03T00:00:00+00:00/understanding_swiftui_layout_behaviors<p>The SwiftUI layout system is more predictable and easier to understand than UIKit layout system. But this does not mean how it works is entirely straightforward.</p> <p>For newcomers with no preconception of how layout historically worked on Apple platforms, official documentation about the SwiftUI layout system might namely be incomplete or <a href="https://developer.apple.com/documentation/SwiftUI/View/frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:)">obscure</a>. The number of views and modifiers, as well as their various behaviors, can be quite overwhelming. Even for seasoned UIKit developers it can be difficult to figure out how SwiftUI layout system works, as its core principles are quite different from UIKit well-known concepts of Auto Layout constraints, springs and struts.</p> <p>This article explores the essential rules and behaviors of the SwiftUI layout system and explains how you should reason about it. It also introduces a formalism that helps characterize views and their sizing behaviors in general. It finally provides a list of the sizing behaviors for most SwiftUI built-in views.</p> <ul id="markdown-toc"> <li><a href="#view-categories" id="markdown-toc-view-categories">View Categories</a></li> <li><a href="#the-swiftui-layout-process" id="markdown-toc-the-swiftui-layout-process">The SwiftUI Layout Process</a></li> <li><a href="#sizing-behaviors" id="markdown-toc-sizing-behaviors">Sizing Behaviors</a></li> <li><a href="#decorator-views-and-modifiers" id="markdown-toc-decorator-views-and-modifiers">Decorator Views and Modifiers</a></li> <li><a href="#determining-the-intrinsic-sizing-behavior-of-a-view" id="markdown-toc-determining-the-intrinsic-sizing-behavior-of-a-view">Determining the Intrinsic Sizing Behavior of a View</a></li> <li><a href="#ambiguous-layouts" id="markdown-toc-ambiguous-layouts">Ambiguous Layouts</a></li> <li><a href="#sizing-behaviors-in-view-hierarchies" id="markdown-toc-sizing-behaviors-in-view-hierarchies">Sizing Behaviors in View Hierarchies</a></li> <li><a href="#altering-sizing-behaviors" id="markdown-toc-altering-sizing-behaviors">Altering Sizing Behaviors</a></li> <li><a href="#layout-priorities" id="markdown-toc-layout-priorities">Layout Priorities</a></li> <li><a href="#sizing-swiftui-views" id="markdown-toc-sizing-swiftui-views">Sizing SwiftUI Views</a></li> <li><a href="#uiviewrepresentable-and-uiviewcontrollerrepresentable" id="markdown-toc-uiviewrepresentable-and-uiviewcontrollerrepresentable">UIViewRepresentable and UIViewControllerRepresentable</a></li> <li><a href="#layout-advice" id="markdown-toc-layout-advice">Layout Advice</a></li> <li><a href="#tldr" id="markdown-toc-tldr">TL;DR</a></li> <li><a href="#view-taxonomy" id="markdown-toc-view-taxonomy">View Taxonomy</a></li> </ul> <h2 id="view-categories">View Categories</h2> <p>SwiftUI views all belong to either one of the following categories:</p> <ul> <li><strong>Simple views</strong> which do not take another view (or view builder) as parameter, like <code class="language-plaintext highlighter-rouge">Text</code> or <code class="language-plaintext highlighter-rouge">Image</code>.</li> <li><strong>Composed views</strong> which take another view or a view builder as parameter, like <code class="language-plaintext highlighter-rouge">VStack</code> or <code class="language-plaintext highlighter-rouge">Toggle</code>.</li> </ul> <p>Layouts themselves are simply created by assembling simple views and composed views in arbitrarily complex hierarchies.</p> <h2 id="the-swiftui-layout-process">The SwiftUI Layout Process</h2> <p>When a parent must lay out one of its child views it proceeds in three steps, very well explained in articles from <a href="https://kean.blog/post/swiftui-layout-system">Alex Grebenyuk</a> and <a href="https://www.hackingwithswift.com/books/ios-swiftui/how-layout-works-in-swiftui">Paul Hudson</a>:</p> <ol> <li>The parent offers some size to the child view.</li> <li>The child view decides the size it requires, eventualy taking into account the parent size offer (a hint which the child is free to ignore entirely). It then returns the size it requires to its parent.</li> <li>The parent lays out the child somewhere, strictly respecting the size that the child requested.</li> </ol> <h3 class="no_toc" id="examples">Examples</h3> <p>Size offers and child view placements vary depending on the expected result, for example:</p> <ul> <li>A <code class="language-plaintext highlighter-rouge">Painting</code> might draw some ostentatious painting frame around its edges, and offers the rest to a single child view centered in it.</li> <li>A <code class="language-plaintext highlighter-rouge">Border</code> might offer its entire size to a single child view and adds a 1-pixel border on top of it.</li> <li>A <code class="language-plaintext highlighter-rouge">Plane</code> might be made of two coordinate axes, equally offering a fourth of its size to four child views, each one drawn in a quadrant. Child views are positioned in their respective quadrant with different alignments (bottom left for the 1st, bottom right for the 2nd, top right for the 3rd and top left for the 4th), so that one of their corners coincides with the origin.</li> </ul> <h2 id="sizing-behaviors">Sizing Behaviors</h2> <p>During step 2 of the layout process children must decide the size they need before communicating it to their parent. We call <em>sizing behaviors</em><sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> the various possibilities with which a view can decide the size it needs. Note that views might have different sizing behaviors in the horizontal and vertical directions. Knowing which sizing behavior is adopted by a view and how it affects the layout process is crucial in understanding how SwiftUI assigns sizes and positions to views.</p> <p>Usually a view exhibits one of the two following concrete opposite sizing behaviors:</p> <ul> <li><strong>Expanding</strong> (exp): The view strives to match the size offered by its parent.</li> <li><strong>Hugging</strong> (hug): The view chooses the best size to fit its content without consulting the size offered by its parent.</li> </ul> <p>If a view is expanding in a single direction it must match the size offered by its parent in this direction so that it can scale when the parent does. But if a view expands in horizontal and vertical directions at the same time it must only fulfill the size offered by its parent in at least one direction.<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup> Child views are namely ultimately responsible of deciding alone which size they want. If they are expanding in all directions they must still match the size offered by their parent in at least one direction (so that they can scale when the parent does), but they remain free to choose the size in the other direction if they do not want to stretch.<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup></p> <p>A third abstract behavior must be introduced for composed views, whose behavior depend on their children behavior:</p> <ul> <li><strong>Neutral</strong> (neu): The view adjusts its sizing behavior based on the behaviors of its children, adopting hugging behavior if and only if all its children have hugging behavior, otherwise expanding behavior.</li> </ul> <p>These three sizing behaviors describe <em>intrinsic</em> properties of views, which means they apply to views considered in isolation. In concrete situations, though, views are part of a layout hierarchy. When a view is part of a hierarchy only expanding and hugging behaviors ultimately apply. Neutral behavior must therefore be seen a behavioral placeholder for expanding or hugging behaviors, with no existence in concrete hierarchies.</p> <p>In the following we might sometimes use h-exp, v-exp, h-hug, v-hug, h-neu and v-neu as shorthands for all possible intrinsic behaviors in horizontal (h) and vertical (v) directions respectively.</p> <h2 id="decorator-views-and-modifiers">Decorator Views and Modifiers</h2> <p>Composed views containing a single child are special. They namely behave like decorators, possibly altering or preserving the decorated view behavior:</p> <ul> <li>A composed view with hugging behavior in some direction provides its child with a fixed content proposal in this direction.</li> <li>A composed view with expanding behavior in some direction provides its child with the maximal content proposal it can afford in this direction.</li> <li>A composed view with neutral behavior in some direction transparently adopts the behavior of its child in the same direction.</li> </ul> <p>SwiftUI makes extensive use of decorators for defining modifiers. Each modifier has an associated private composed view wrapper, returned as an opaque type from the view modifier. Modifiers are therefore mostly syntactic sugar and include iconic examples like <code class="language-plaintext highlighter-rouge">View/frame(width:height:)</code> or <code class="language-plaintext highlighter-rouge">View/aspectRatio(_:contentMode:)</code>.</p> <h2 id="determining-the-intrinsic-sizing-behavior-of-a-view">Determining the Intrinsic Sizing Behavior of a View</h2> <p>You can probe the sizing behavior of a view to determine its intrinsic sizing behavior, even if you don’t have access to its implementation. The procedure to follow depends on the category the view belongs to.</p> <h3 class="no_toc" id="probing-intrinsic-sizing-behavior-for-simple-views">Probing Intrinsic Sizing Behavior for Simple Views</h3> <p>To determine the sizing behavior of a simple view use a sufficiently large canvas, attach a border to the simple view, and observe where the border is displayed:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">SimpleView_Previews</span><span class="p">:</span> <span class="kt">PreviewProvider</span> <span class="p">{</span> <span class="kd">static</span> <span class="k">var</span> <span class="nv">previews</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">SimpleView</span><span class="p">(</span><span class="o">...</span><span class="p">)</span> <span class="o">.</span><span class="nf">border</span><span class="p">(</span><span class="kt">Color</span><span class="o">.</span><span class="n">blue</span><span class="p">,</span> <span class="nv">width</span><span class="p">:</span> <span class="mi">3</span><span class="p">)</span> <span class="o">.</span><span class="nf">previewLayout</span><span class="p">(</span><span class="o">.</span><span class="nf">fixed</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="mi">1000</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="mi">1000</span><span class="p">))</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>If the border is close to the simple view for some direction it has hugging behavior in this direction, otherwise expanding behavior.</p> <p>With this method it can be verified that <code class="language-plaintext highlighter-rouge">Text</code> has hugging behavior in all directions, while <code class="language-plaintext highlighter-rouge">Color</code> has expanding behavior in all directions:</p> <p><img src="/images/swiftui_layout_simple_view.jpg" alt="Simple view" /></p> <h3 class="no_toc" id="probing-intrinsic-sizing-behavior-for-composed-views">Probing Intrinsic Sizing Behavior for Composed Views</h3> <p>To determine the behavior of a composed view use a sufficiently large canvas, attach a border to the composed view, and observe where the border is displayed when the composed view wraps an expanding child view, respectively a hugging child view. <code class="language-plaintext highlighter-rouge">Text</code> and <code class="language-plaintext highlighter-rouge">Color</code> are ideal child candidates as they let us probe both horizontal and vertical behaviors at the same time:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">ComposedView_Previews</span><span class="p">:</span> <span class="kt">PreviewProvider</span> <span class="p">{</span> <span class="kd">static</span> <span class="k">var</span> <span class="nv">previews</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">Group</span> <span class="p">{</span> <span class="kt">ComposedView</span><span class="p">(</span><span class="o">...</span><span class="p">)</span> <span class="p">{</span> <span class="kt">Color</span><span class="o">.</span><span class="n">red</span> <span class="o">.</span><span class="nf">border</span><span class="p">(</span><span class="kt">Color</span><span class="o">.</span><span class="n">green</span><span class="p">,</span> <span class="nv">width</span><span class="p">:</span> <span class="mi">3</span><span class="p">)</span> <span class="p">}</span> <span class="kt">ComposedView</span><span class="p">(</span><span class="o">...</span><span class="p">)</span> <span class="p">{</span> <span class="kt">Text</span><span class="p">(</span><span class="s">"Test"</span><span class="p">)</span> <span class="o">.</span><span class="nf">border</span><span class="p">(</span><span class="kt">Color</span><span class="o">.</span><span class="n">green</span><span class="p">,</span> <span class="nv">width</span><span class="p">:</span> <span class="mi">3</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="o">.</span><span class="nf">border</span><span class="p">(</span><span class="kt">Color</span><span class="o">.</span><span class="n">blue</span><span class="p">,</span> <span class="nv">width</span><span class="p">:</span> <span class="mi">3</span><span class="p">)</span> <span class="o">.</span><span class="nf">previewLayout</span><span class="p">(</span><span class="o">.</span><span class="nf">fixed</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="mi">1000</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="mi">1000</span><span class="p">))</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>If expanding, respectively hugging behavior is observed for the composed view when its child is expanding, respectively hugging in some direction, this means the composed view has neutral behavior in this direction, as it adopts the behavior of its child.</p> <p>If on the other hand the composed view ignores its child behavior for some direction, then it must either have expanding or hugging behavior in this direction. Simply apply the procedure for simple views to determine the intrinsic composed view behavior in this case.</p> <p>With this method it can be verified that a <code class="language-plaintext highlighter-rouge">Toggle</code> wrapping some <code class="language-plaintext highlighter-rouge">View</code> label:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">ComposedView_Previews</span><span class="p">:</span> <span class="kt">PreviewProvider</span> <span class="p">{</span> <span class="kd">static</span> <span class="k">var</span> <span class="nv">previews</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">Group</span> <span class="p">{</span> <span class="kt">Toggle</span><span class="p">(</span><span class="nv">isOn</span><span class="p">:</span> <span class="o">.</span><span class="nf">constant</span><span class="p">(</span><span class="kc">true</span><span class="p">))</span> <span class="p">{</span> <span class="kt">Color</span><span class="o">.</span><span class="n">red</span> <span class="o">.</span><span class="nf">border</span><span class="p">(</span><span class="kt">Color</span><span class="o">.</span><span class="n">green</span><span class="p">,</span> <span class="nv">width</span><span class="p">:</span> <span class="mi">3</span><span class="p">)</span> <span class="p">}</span> <span class="kt">Toggle</span><span class="p">(</span><span class="nv">isOn</span><span class="p">:</span> <span class="o">.</span><span class="nf">constant</span><span class="p">(</span><span class="kc">true</span><span class="p">))</span> <span class="p">{</span> <span class="kt">Text</span><span class="p">(</span><span class="s">"Option"</span><span class="p">)</span> <span class="o">.</span><span class="nf">border</span><span class="p">(</span><span class="kt">Color</span><span class="o">.</span><span class="n">green</span><span class="p">,</span> <span class="nv">width</span><span class="p">:</span> <span class="mi">3</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="o">.</span><span class="nf">border</span><span class="p">(</span><span class="kt">Color</span><span class="o">.</span><span class="n">blue</span><span class="p">,</span> <span class="nv">width</span><span class="p">:</span> <span class="mi">3</span><span class="p">)</span> <span class="o">.</span><span class="nf">previewLayout</span><span class="p">(</span><span class="o">.</span><span class="nf">fixed</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="mi">1000</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="mi">1000</span><span class="p">))</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>has expanding behavior horizontally, but neutral behavior vertically:</p> <p><img src="/images/swiftui_layout_composed_view.jpg" alt="Composed view" /></p> <h3 class="no_toc" id="probing-modifiers">Probing Modifiers</h3> <p>Modifiers return opaque composed views (as they are meant to augment the view they are applied on). Probing a modifier is therefore achieved in the same way as for composed views:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">Modifier_Previews</span><span class="p">:</span> <span class="kt">PreviewProvider</span> <span class="p">{</span> <span class="kd">static</span> <span class="k">var</span> <span class="nv">previews</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">Group</span> <span class="p">{</span> <span class="kt">Color</span><span class="o">.</span><span class="n">red</span> <span class="kt">Text</span><span class="p">(</span><span class="s">"Test"</span><span class="p">)</span> <span class="p">}</span> <span class="o">.</span><span class="nf">border</span><span class="p">(</span><span class="kt">Color</span><span class="o">.</span><span class="n">green</span><span class="p">,</span> <span class="nv">width</span><span class="p">:</span> <span class="mi">3</span><span class="p">)</span> <span class="o">.</span><span class="nf">modifier</span><span class="p">(</span><span class="o">...</span><span class="p">)</span> <span class="o">.</span><span class="nf">border</span><span class="p">(</span><span class="kt">Color</span><span class="o">.</span><span class="n">blue</span><span class="p">,</span> <span class="nv">width</span><span class="p">:</span> <span class="mi">3</span><span class="p">)</span> <span class="o">.</span><span class="nf">previewLayout</span><span class="p">(</span><span class="o">.</span><span class="nf">fixed</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="mi">1000</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="mi">1000</span><span class="p">))</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>With this method it can be verified that applying the <code class="language-plaintext highlighter-rouge">frame(maxWidth: .infinity)</code> modifier creates an expanding horizontal frame with neutral vertical behavior:</p> <p><img src="/images/swiftui_layout_frame_modifier.jpg" alt="Frame modifier" /></p> <p>Thoroughly probing the <code class="language-plaintext highlighter-rouge">View/frame(minWidth:idealWidth:maxWidth:minWeight:idealHeight:maxHeight:alignment:)</code> modifier is achieved by probing its behavior for other values of <code class="language-plaintext highlighter-rouge">maxWidth</code> and <code class="language-plaintext highlighter-rouge">maxHeight</code>. The observed behavior is characterized in the <a href="#view-taxonomy">View Taxonomy</a> section.</p> <h2 id="ambiguous-layouts">Ambiguous Layouts</h2> <p>One of the biggest advantages of SwiftUI over Auto Layout is the fact that layouts can never break. Every seasoned UIKit developer has experienced Auto Layout failing when layouts are over- or underdetermined, yielding unpredictable results and logging horrendous messages to the console. This never occurs with SwiftUI.</p> <p>This does not mean that ambiguities do not exist in SwiftUI layouts, though. When the SwiftUI layout engine encounters an ambiguity it simply assigns a magical value of 10 to view dimensions it could not properly determine. The layout succeeds and no issues are reported, though of course the result obtained is not the expected one.</p> <p>Such situations usually occur when a parent view wants to size itself based on the size of its children, but those children exhibit expanding behavior and thus want to match the parent size. This creates a chicken-and-egg problem which SwiftUI solves by replacing undetermined sizes with 10, as can be seen with the simple following code:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">Undetermined_Size_Previews</span><span class="p">:</span> <span class="kt">PreviewProvider</span> <span class="p">{</span> <span class="kd">static</span> <span class="k">var</span> <span class="nv">previews</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">Color</span><span class="o">.</span><span class="n">red</span> <span class="o">.</span><span class="nf">fixedSize</span><span class="p">()</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>Since <code class="language-plaintext highlighter-rouge">Color</code> has h-exp and v-exp behavior, the <code class="language-plaintext highlighter-rouge">View/fixedSize()</code> modifier cannot figure out the intrinsic size it needs to apply, using 10 as fallback in both directions.</p> <p>Therefore, when you see some size of 10 popping somewhere in your layout for unknown reasons, this usually means that a similar chicken-and-egg problem exists with the involved view and its parent. Nothing will break or throw an exception, but you should still have a look at why the problem exists in the first place and lift the associated ambiguity, for example by applying a frame modifier which will provide the child with a well-defined size.</p> <h2 id="sizing-behaviors-in-view-hierarchies">Sizing Behaviors in View Hierarchies</h2> <p>Layouts are created by assembling simple and composed views with various sizing behaviors together. Associated with composed views only, the neutral sizing behavior is a placeholder for expanding or hugging behavior, though. Before you can understand how some layout works in practice, you therefore must determine which behavior some neutral behavior translates into, depending on the behavior of its children.</p> <p>Finding the true nature of a neutral behavior is typically achieved in a top-bottom / bottom-up fashion. Starting from a composed view with undetermined neutral behavior you consider the behavior of its children, recursively applying the same strategy when you encounter another view with composed behavior. Once the behavior of all children contained in a composed view is known the behavior of the parent view itself can be determined.</p> <p>This process might seem cumbersome but is thankfully theoretical in most cases. As SwiftUI views are usually small reusable units for which the behavior is known or can be quickly determined, the process above should in practice only involve a brief look at the children of some composed view to guess its overall behavior.</p> <p>To speed up the process of identifying neutral behaviors, it might still be useful to document custom views in your code so that their behavior can be quickly guessed from their documentation, for example:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">/// Intrinsic sizing behavior: h-exp, v-exp</span> <span class="kd">struct</span> <span class="kt">CalendarView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="p">}</span> <span class="c1">/// Intrinsic sizing behavior: h-exp, v-hug</span> <span class="kd">struct</span> <span class="kt">SegmentedControl</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="p">}</span> <span class="c1">// Tag is a "Dual-Category View", see corresponding paragraph in the View Taxonomy section</span> <span class="kd">struct</span> <span class="kt">Tag</span><span class="o">&lt;</span><span class="kt">Label</span><span class="p">:</span> <span class="kt">View</span><span class="o">&gt;</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span> <span class="c1">/// Intrinsic sizing behavior: h-neu, v-neu</span> <span class="nf">init</span><span class="p">(</span><span class="kd">@ViewBuilder</span> <span class="nv">label</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">Label</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="p">}</span> <span class="c1">/// Intrinsic sizing behavior: h-hug, v-hug</span> <span class="nf">init</span><span class="p">(</span><span class="n">_</span> <span class="nv">titleKey</span><span class="p">:</span> <span class="kt">LocalizedStringKey</span><span class="p">)</span> <span class="k">where</span> <span class="kt">Label</span> <span class="o">==</span> <span class="kt">Text</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="p">}</span> <span class="p">}</span> <span class="kd">extension</span> <span class="kt">View</span> <span class="p">{</span> <span class="c1">/// Intrinsic sizing behavior: h-neu, v-neu</span> <span class="kd">func</span> <span class="nf">ornatePictureFrame</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <h2 id="altering-sizing-behaviors">Altering Sizing Behaviors</h2> <p>When the behavior of a SwiftUI view is not the one you want you cannot alter its properties directly, as views themselves are value types and thus immutable. Instead you wrap the view with unsatisfying behavior into another one to obtain the desired behavior. This is usually achieved using some public composed view (e.g. a stack) or by applying a modifier.</p> <p>You can refer to the <a href="#view-taxonomy">View Taxonomy</a> section to help you decide which modifier can be helpful to achieve the desired behavior.</p> <h2 id="layout-priorities">Layout Priorities</h2> <p>Layout priorities do not change the sizing behavior of a view. They merely are used by composed parent views to decide which child they should propose a size first.</p> <p>For this reason this article will not further discuss layout priorities. You can read the <a href="https://www.swiftbysundell.com/articles/swiftui-layout-system-guide-part-3/">dedicated article from John Sundell</a> to learn more about this topic.</p> <h2 id="sizing-swiftui-views">Sizing SwiftUI Views</h2> <p>If you are wrapping SwiftUI views within UIKit you might be interested to know which size a SwiftUI view requires, depending on the space it can be provided. This can be helpful if the view contains a variable amount of text and you want to provide a matching size to a collection layout in advance, for example.</p> <p>Fortunately <code class="language-plaintext highlighter-rouge">UIHostingController</code> provides a <code class="language-plaintext highlighter-rouge">sizeThatFits(in:)</code> method to calculate the intrinsic size of a view, which in facts apply the <code class="language-plaintext highlighter-rouge">UIView/sizeThatFits(:)</code> method to its associated view.</p> <p>Note that if your SwiftUI layout depends on size classes you should inject the size class into the environment when calculating the size, so that the view you probe adopts the correct behavior:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">extension</span> <span class="kt">View</span> <span class="p">{</span> <span class="kd">func</span> <span class="nf">adaptiveSizeThatFits</span><span class="p">(</span><span class="k">in</span> <span class="nv">size</span><span class="p">:</span> <span class="kt">CGSize</span><span class="p">,</span> <span class="k">for</span> <span class="nv">horizontalSizeClass</span><span class="p">:</span> <span class="kt">UIUserInterfaceSizeClass</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">CGSize</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">hostController</span> <span class="o">=</span> <span class="kt">UIHostingController</span><span class="p">(</span><span class="nv">rootView</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="nf">environment</span><span class="p">(\</span><span class="o">.</span><span class="n">horizontalSizeClass</span><span class="p">,</span> <span class="kt">UserInterfaceSizeClass</span><span class="p">(</span><span class="n">horizontalSizeClass</span><span class="p">)))</span> <span class="k">return</span> <span class="n">hostController</span><span class="o">.</span><span class="nf">sizeThatFits</span><span class="p">(</span><span class="nv">in</span><span class="p">:</span> <span class="n">size</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>You can use <code class="language-plaintext highlighter-rouge">UIView.layoutFittingExpandedSize</code> to calculate the size required by some SwiftUI view. For example here is how you would calculate the height of <code class="language-plaintext highlighter-rouge">SomeView</code> constrained to 800px horizontally, for the regular size class:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">fittingSize</span> <span class="o">=</span> <span class="kt">CGSize</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="mi">800</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="kt">UIView</span><span class="o">.</span><span class="n">layoutFittingExpandedSize</span><span class="o">.</span><span class="n">height</span><span class="p">)</span> <span class="k">let</span> <span class="nv">height</span> <span class="o">=</span> <span class="kt">SomeView</span><span class="p">()</span><span class="o">.</span><span class="nf">adaptiveSizeThatFits</span><span class="p">(</span><span class="nv">in</span><span class="p">:</span> <span class="n">fittingSize</span><span class="p">,</span> <span class="nv">for</span><span class="p">:</span> <span class="o">.</span><span class="n">regular</span><span class="p">)</span><span class="o">.</span><span class="n">height</span> </code></pre></div></div> <p>If <code class="language-plaintext highlighter-rouge">SomeView</code> has hugging behavior the matching height is returned. If the view has expanding behavior, though, the returned height will be equal to the size offer <code class="language-plaintext highlighter-rouge">UIView.layoutFittingExpandedSize.height</code> instead.</p> <h4 class="no_toc" id="remark">Remark</h4> <p>Instead of the visual approach described <a href="determining-the-intrinsic-sizing-behavior-of-a-biew">above</a> you can use <code class="language-plaintext highlighter-rouge">UIHostingController</code> and <code class="language-plaintext highlighter-rouge">sizeThatFits(in:)</code> to probe view sizing behavior. I like the visual approach better but here is a rough idea how you can check that a <code class="language-plaintext highlighter-rouge">VStack</code> has neutral behavior in all directions, using a Swift playground:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">ComposedViewExpandingTest</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span> <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">VStack</span> <span class="p">{</span> <span class="kt">Color</span><span class="o">.</span><span class="n">red</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="kd">struct</span> <span class="kt">ComposedViewHuggingTest</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span> <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">VStack</span> <span class="p">{</span> <span class="kt">Text</span><span class="p">(</span><span class="s">"Test"</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="kd">extension</span> <span class="kt">View</span> <span class="p">{</span> <span class="kd">func</span> <span class="nf">probedSize</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">CGSize</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">hostController</span> <span class="o">=</span> <span class="kt">UIHostingController</span><span class="p">(</span><span class="nv">rootView</span><span class="p">:</span> <span class="k">self</span><span class="p">)</span> <span class="k">return</span> <span class="n">hostController</span><span class="o">.</span><span class="nf">sizeThatFits</span><span class="p">(</span><span class="nv">in</span><span class="p">:</span> <span class="kt">UIView</span><span class="o">.</span><span class="n">layoutFittingExpandedSize</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="k">let</span> <span class="nv">expandingSize</span> <span class="o">=</span> <span class="kt">ComposedViewExpandingTest</span><span class="p">()</span><span class="o">.</span><span class="nf">probedSize</span><span class="p">()</span> <span class="k">let</span> <span class="nv">huggingSize</span> <span class="o">=</span> <span class="kt">ComposedViewHuggingTest</span><span class="p">()</span><span class="o">.</span><span class="nf">probedSize</span><span class="p">()</span> <span class="k">let</span> <span class="nv">hNeutral</span> <span class="o">=</span> <span class="n">huggingSize</span><span class="o">.</span><span class="n">width</span> <span class="o">!=</span> <span class="kt">UIView</span><span class="o">.</span><span class="n">layoutFittingExpandedSize</span><span class="o">.</span><span class="n">width</span> <span class="o">&amp;&amp;</span> <span class="n">expandingSize</span><span class="o">.</span><span class="n">width</span> <span class="o">==</span> <span class="kt">UIView</span><span class="o">.</span><span class="n">layoutFittingExpandedSize</span><span class="o">.</span><span class="n">width</span> <span class="k">let</span> <span class="nv">vNeutral</span> <span class="o">=</span> <span class="n">huggingSize</span><span class="o">.</span><span class="n">height</span> <span class="o">!=</span> <span class="kt">UIView</span><span class="o">.</span><span class="n">layoutFittingExpandedSize</span><span class="o">.</span><span class="n">height</span> <span class="o">&amp;&amp;</span> <span class="n">expandingSize</span><span class="o">.</span><span class="n">height</span> <span class="o">==</span> <span class="kt">UIView</span><span class="o">.</span><span class="n">layoutFittingExpandedSize</span><span class="o">.</span><span class="n">height</span> </code></pre></div></div> <h2 id="uiviewrepresentable-and-uiviewcontrollerrepresentable">UIViewRepresentable and UIViewControllerRepresentable</h2> <p>Special considerations are required when considering the sizing behavior of views implemented with <code class="language-plaintext highlighter-rouge">UIViewRepresentable</code> or <code class="language-plaintext highlighter-rouge">UIViewControllerRepresentable</code>. Such considerations are outside the scope of this article and will be discussed in a separate article.</p> <h2 id="layout-advice">Layout Advice</h2> <p>When creating layouts you should think about the sizing behavior of the involved views and alter them as required, for example using frame modifiers. You can refer to the <a href="#view-taxonomy">View Taxonomy</a> at the end of this article to guess the resulting behavior beforehand.</p> <p>To avoid messy layouts I recommend to:</p> <ul> <li>Factor out view hierarchies into smaller views with documented sizing behaviors.</li> <li>Avoid geometry readers, which should be considered only if there are no other possibilities to achieve what you want. View positioning can in general be made in a much cleaner way using frame modifiers.</li> <li>Avoid fixed <code class="language-plaintext highlighter-rouge">Spacer</code>s to insert spacings in stacks. In general you can use paddings and nested stacks with proper spacing settings to achieve a better result.</li> </ul> <h2 id="tldr">TL;DR</h2> <p>SwiftUI introduces a robust layout system relying on size negociation between parent and children views. Children ultimately choose the size they want based on three possible intrisic sizing behaviors, expanding, hugging and neutral, possibly different in horizontal and vertical directions.</p> <p>Views involved in a hierarchy effectively only exhibit expanding or hugging behaviors, though. It is the responsibility of the layout system, or yours when you create or inspect layouts, to identify how neutral behaviors ultimately translate into expanding or hugging behaviors in a hierarchy.</p> <p>To quicker analyze view hiearchies and understand how adding a view to an existing hierarchy might affect the overall result, it might prove helpful to document custom views so that their intrinsic behavior can be quickly read. While this process must be done for custom views, it can be done <a href="#view-taxonomy">for SwiftUI built-in views</a> to offer an overview of their respective behaviors.</p> <h2 id="view-taxonomy">View Taxonomy</h2> <p>The following taxonomy lists the intrinsic sizing behaviors of most SwiftUI built-in views, and can be used as a reference when inspecting or building layouts.</p> <p>Each view was probed using one of the procedures outlined in this article. Results were consolidated and presented in several tables, grouping views with similar purposes.</p> <p>Note that tables not only list behaviors of public view types like <code class="language-plaintext highlighter-rouge">Text</code>, but also of opaque types returned by modifiers like <code class="language-plaintext highlighter-rouge">Image/resizable(capInsets:resizingMode:)</code> or <code class="language-plaintext highlighter-rouge">View/frame(width:height:)</code>.</p> <h3 class="no_toc" id="dual-category-views">Dual-Category Views</h3> <p>Some views can either be simple views or composed views depending on how they are instantiated. You should be especially careful when using such views, as simply adding a trailing closure to them might change a simple view into a composed view with entirely different layout behaviors (usually switching from hugging to neutral behavior).</p> <p>Dual-category views include most notably <code class="language-plaintext highlighter-rouge">Label</code>, <code class="language-plaintext highlighter-rouge">Link</code>, <code class="language-plaintext highlighter-rouge">ProgressView</code>, <code class="language-plaintext highlighter-rouge">Slider</code>, <code class="language-plaintext highlighter-rouge">Stepper</code> and <code class="language-plaintext highlighter-rouge">Toggle</code>.</p> <h3 class="no_toc" id="simple-building-blocks">Simple Building Blocks</h3> <p>The following views are commonly used when building any kind of layout.</p> <table> <thead> <tr> <th style="text-align: left">Type</th> <th style="text-align: center">Category</th> <th style="text-align: center">Horizontal</th> <th style="text-align: center">Vertical</th> </tr> </thead> <tbody> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Color</code></td> <td style="text-align: center">Simple</td> <td style="text-align: center">exp</td> <td style="text-align: center">exp</td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Divider</code></td> <td style="text-align: center">Simple</td> <td style="text-align: center">exp</td> <td style="text-align: center">hug</td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Image</code></td> <td style="text-align: center">Simple</td> <td style="text-align: center">hug</td> <td style="text-align: center">hug</td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Image</code> from <code class="language-plaintext highlighter-rouge">resizable(capInsets:resizingMode:)</code> modifier</td> <td style="text-align: center">Simple</td> <td style="text-align: center">exp</td> <td style="text-align: center">exp</td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">SecureField</code></td> <td style="text-align: center">Simple</td> <td style="text-align: center">exp</td> <td style="text-align: center">hug</td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Text</code></td> <td style="text-align: center">Simple</td> <td style="text-align: center">hug</td> <td style="text-align: center">hug</td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">TextEditor</code></td> <td style="text-align: center">Simple</td> <td style="text-align: center">exp</td> <td style="text-align: center">exp</td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">TextField</code></td> <td style="text-align: center">Simple</td> <td style="text-align: center">exp</td> <td style="text-align: center">hug</td> </tr> </tbody> </table> <h3 class="no_toc" id="buttons">Buttons</h3> <p>Button style can be controlled with the <code class="language-plaintext highlighter-rouge">Button/buttonStyle(_:)</code> modifier, resulting in different behaviors.</p> <table> <thead> <tr> <th style="text-align: left">Type</th> <th style="text-align: center">Category</th> <th style="text-align: center">Horizontal</th> <th style="text-align: center">Vertical</th> <th style="text-align: left">Remarks</th> </tr> </thead> <tbody> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Button</code> (default)</td> <td style="text-align: center">Composed</td> <td style="text-align: center">neu</td> <td style="text-align: center">neu</td> <td style="text-align: left">No style or <code class="language-plaintext highlighter-rouge">DefaultButtonStyle</code>. Corresponds to <code class="language-plaintext highlighter-rouge">PlainButtonStyle</code> on iOS and to <code class="language-plaintext highlighter-rouge">BorderedButtonStyle</code> on tvOS</td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Button</code> (plain)</td> <td style="text-align: center">Composed</td> <td style="text-align: center">neu</td> <td style="text-align: center">neu</td> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">PlainButtonStyle</code>. No content insets are applied on tvOS</td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Button</code> (bordered, tvOS only)</td> <td style="text-align: center">Composed</td> <td style="text-align: center">neu</td> <td style="text-align: center">neu</td> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">BorderedButtonStyle</code>. Some content insets are applied</td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Button</code> (card, tvOS only)</td> <td style="text-align: center">Composed</td> <td style="text-align: center">hug</td> <td style="text-align: center">hug</td> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">CardButtonStyle</code>. ⚠️ This button calls <code class="language-plaintext highlighter-rouge">View/fixedSize(horizontal:vertical:)</code> on its content</td> </tr> </tbody> </table> <h3 class="no_toc" id="links">Links</h3> <p>Links can be created with a title <code class="language-plaintext highlighter-rouge">String</code> or with a custom label view.</p> <table> <thead> <tr> <th style="text-align: left">Type</th> <th style="text-align: center">Category</th> <th style="text-align: center">Horizontal</th> <th style="text-align: center">Vertical</th> </tr> </thead> <tbody> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Link</code></td> <td style="text-align: center">Simple</td> <td style="text-align: center">hug</td> <td style="text-align: center">hug</td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Link</code> with <code class="language-plaintext highlighter-rouge">Label: View</code></td> <td style="text-align: center">Composed</td> <td style="text-align: center">neu</td> <td style="text-align: center">neu</td> </tr> </tbody> </table> <h3 class="no_toc" id="labels">Labels</h3> <p>Labels can be created with a title <code class="language-plaintext highlighter-rouge">String</code> and an image / system image, or with two custom views for the title and the icon.</p> <table> <thead> <tr> <th style="text-align: left">Type</th> <th style="text-align: center">Category</th> <th style="text-align: center">Horizontal</th> <th style="text-align: center">Vertical</th> </tr> </thead> <tbody> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Label</code></td> <td style="text-align: center">Simple</td> <td style="text-align: center">hug</td> <td style="text-align: center">hug</td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Label</code> with <code class="language-plaintext highlighter-rouge">Title: View</code> and <code class="language-plaintext highlighter-rouge">Icon: View</code></td> <td style="text-align: center">Composed</td> <td style="text-align: center">neu</td> <td style="text-align: center">neu</td> </tr> </tbody> </table> <h3 class="no_toc" id="stacks">Stacks</h3> <p>Stacks are essential components for horizontally and vertically arranging views.</p> <table> <thead> <tr> <th style="text-align: left">Type</th> <th style="text-align: center">Category</th> <th style="text-align: center">Horizontal</th> <th style="text-align: center">Vertical</th> </tr> </thead> <tbody> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">HStack</code></td> <td style="text-align: center">Composed</td> <td style="text-align: center">neu</td> <td style="text-align: center">neu</td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">VStack</code></td> <td style="text-align: center">Composed</td> <td style="text-align: center">neu</td> <td style="text-align: center">neu</td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">ZStack</code></td> <td style="text-align: center">Composed</td> <td style="text-align: center">neu</td> <td style="text-align: center">neu</td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">LazyHStack</code></td> <td style="text-align: center">Composed</td> <td style="text-align: center">hug</td> <td style="text-align: center">exp</td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">LazyVStack</code></td> <td style="text-align: center">Composed</td> <td style="text-align: center">exp</td> <td style="text-align: center">hug</td> </tr> </tbody> </table> <p>Note that lazy stacks have very different sizing behaviors from their standard counterparts. You should therefore be especially careful when you repalce a stack with its lazy counterpart, as this will change its sizing behavior and will likely require some layout adjustments.</p> <h3 class="no_toc" id="sliders">Sliders</h3> <p>Sliders can be created with or without associated custom label view.</p> <table> <thead> <tr> <th style="text-align: left">Type</th> <th style="text-align: center">Category</th> <th style="text-align: center">Horizontal</th> <th style="text-align: center">Vertical</th> <th style="text-align: left">Remarks</th> </tr> </thead> <tbody> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Slider</code></td> <td style="text-align: center">Simple</td> <td style="text-align: center">exp</td> <td style="text-align: center">hug</td> <td style="text-align: left"> </td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Slider</code> with <code class="language-plaintext highlighter-rouge">Label: View</code></td> <td style="text-align: center">Composed</td> <td style="text-align: center">exp</td> <td style="text-align: center">hug</td> <td style="text-align: left">Label used only for accessibility; does not participate in the layout</td> </tr> </tbody> </table> <h3 class="no_toc" id="progress-views">Progress Views</h3> <p>Progress views can be created with or without associated custom label view. Their style can be controlled with the <code class="language-plaintext highlighter-rouge">View/progressViewStyle(_:)</code> modifier, resulting in different behaviors.</p> <table> <thead> <tr> <th style="text-align: left">Type</th> <th style="text-align: center">Category</th> <th style="text-align: center">Horizontal</th> <th style="text-align: center">Vertical</th> <th style="text-align: left">Remarks</th> </tr> </thead> <tbody> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">ProgressView</code> (linear)</td> <td style="text-align: center">Simple</td> <td style="text-align: center">exp</td> <td style="text-align: center">hug</td> <td style="text-align: left">No style, <code class="language-plaintext highlighter-rouge">DefaultProgressViewStyle</code> or <code class="language-plaintext highlighter-rouge">LinearProgressViewStyle</code></td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">ProgressView</code> (linear) with <code class="language-plaintext highlighter-rouge">Label: View</code></td> <td style="text-align: center">Composed</td> <td style="text-align: center">exp</td> <td style="text-align: center">hug</td> <td style="text-align: left">No style, <code class="language-plaintext highlighter-rouge">DefaultProgressViewStyle</code> or <code class="language-plaintext highlighter-rouge">LinearProgressViewStyle</code>. Label used only for accessibility; does not participate in the layout</td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">ProgressView</code> (circular)</td> <td style="text-align: center">Simple</td> <td style="text-align: center">hug</td> <td style="text-align: center">hug</td> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">CircularProgressViewStyle</code></td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">ProgressView</code> (circular) with <code class="language-plaintext highlighter-rouge">Label: View</code></td> <td style="text-align: center">Composed</td> <td style="text-align: center">neu</td> <td style="text-align: center">neu</td> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">CircularProgressViewStyle</code>. Label displayed underneath</td> </tr> </tbody> </table> <h3 class="no_toc" id="steppers">Steppers</h3> <p>Steppers can be created with a title <code class="language-plaintext highlighter-rouge">String</code>, or with a custom view for the title.</p> <table> <thead> <tr> <th style="text-align: left">Type</th> <th style="text-align: center">Category</th> <th style="text-align: center">Horizontal</th> <th style="text-align: center">Vertical</th> </tr> </thead> <tbody> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Stepper</code></td> <td style="text-align: center">Simple</td> <td style="text-align: center">exp</td> <td style="text-align: center">hug</td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Stepper</code> with <code class="language-plaintext highlighter-rouge">Label: View</code></td> <td style="text-align: center">Composed</td> <td style="text-align: center">exp</td> <td style="text-align: center">neu</td> </tr> </tbody> </table> <h3 class="no_toc" id="toggles">Toggles</h3> <p>Toggles can be created with a title <code class="language-plaintext highlighter-rouge">String</code>, or with a custom view for the title.</p> <table> <thead> <tr> <th style="text-align: left">Type</th> <th style="text-align: center">Category</th> <th style="text-align: center">Horizontal</th> <th style="text-align: center">Vertical</th> </tr> </thead> <tbody> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Toggle</code></td> <td style="text-align: center">Simple</td> <td style="text-align: center">exp</td> <td style="text-align: center">hug</td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Toggle</code> with <code class="language-plaintext highlighter-rouge">Label: View</code></td> <td style="text-align: center">Composed</td> <td style="text-align: center">exp</td> <td style="text-align: center">neu</td> </tr> </tbody> </table> <h3 class="no_toc" id="shapes">Shapes</h3> <p>Several built-in shapes are available. Custom shapes can be created by implementing the <code class="language-plaintext highlighter-rouge">Shape</code> protocol. All have expanding behaviors in all directions.</p> <table> <thead> <tr> <th style="text-align: left">Type</th> <th style="text-align: center">Category</th> <th style="text-align: center">Horizontal</th> <th style="text-align: center">Vertical</th> </tr> </thead> <tbody> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Capsule</code></td> <td style="text-align: center">Simple</td> <td style="text-align: center">exp</td> <td style="text-align: center">exp</td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Circle</code></td> <td style="text-align: center">Simple</td> <td style="text-align: center">exp</td> <td style="text-align: center">exp</td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Ellipse</code></td> <td style="text-align: center">Simple</td> <td style="text-align: center">exp</td> <td style="text-align: center">exp</td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Rectangle</code></td> <td style="text-align: center">Simple</td> <td style="text-align: center">exp</td> <td style="text-align: center">exp</td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">RoundedRectangle</code></td> <td style="text-align: center">Simple</td> <td style="text-align: center">exp</td> <td style="text-align: center">exp</td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Shape</code> (custom)</td> <td style="text-align: center">Simple</td> <td style="text-align: center">exp</td> <td style="text-align: center">exp</td> </tr> </tbody> </table> <h3 class="no_toc" id="spacers">Spacers</h3> <p>Spacers are always flexible and work only within stacks. You can create a fixed size spacer with the <code class="language-plaintext highlighter-rouge">View/frame(width:height:)</code> modifier, though this is best <a href="#layout-advice">avoided</a> in general.</p> <table> <thead> <tr> <th style="text-align: left">Type</th> <th style="text-align: center">Category</th> <th style="text-align: center">Horizontal</th> <th style="text-align: center">Vertical</th> </tr> </thead> <tbody> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Spacer</code></td> <td style="text-align: center">Simple</td> <td style="text-align: center">exp</td> <td style="text-align: center">exp</td> </tr> </tbody> </table> <h3 class="no_toc" id="simple-frame">Simple Frame</h3> <p>The <code class="language-plaintext highlighter-rouge">View/frame(width:height:alignment:)</code> modifier is used to constrain space provided to its receiver in any or all directions.</p> <table> <thead> <tr> <th style="text-align: left"><code class="language-plaintext highlighter-rouge">width</code> / <code class="language-plaintext highlighter-rouge">height</code> argument</th> <th style="text-align: center">Obtained behavior in the horizontal / vertical direction</th> </tr> </thead> <tbody> <tr> <td style="text-align: left">Omitted</td> <td style="text-align: center">neu</td> </tr> <tr> <td style="text-align: left">Finite value</td> <td style="text-align: center">hug</td> </tr> </tbody> </table> <p>If an argument is omitted for some direction the frame wrapper transparently adopts the same behavior as the receiver in this direction.</p> <h3 class="no_toc" id="advanced-frame">Advanced Frame</h3> <p>The <code class="language-plaintext highlighter-rouge">View/frame(minWidth:idealWidth:maxWidth:minWeight:idealHeight:maxHeight:alignment:)</code> modifier is used to constrain space or create an invisible largest frame in some direction, letting various alignments be applied for views drawn in it.</p> <table> <thead> <tr> <th style="text-align: center"><code class="language-plaintext highlighter-rouge">maxWidth</code> / <code class="language-plaintext highlighter-rouge">maxHeight</code> argument</th> <th style="text-align: center">Obtained behavior in the horizontal / vertical direction</th> </tr> </thead> <tbody> <tr> <td style="text-align: center">Omitted</td> <td style="text-align: center">neu</td> </tr> <tr> <td style="text-align: center">Finite value</td> <td style="text-align: center">hug</td> </tr> <tr> <td style="text-align: center"><code class="language-plaintext highlighter-rouge">.infinity</code></td> <td style="text-align: center">exp</td> </tr> </tbody> </table> <p>If an argument is omitted for some direction the frame wrapper transparently adopts the same behavior as the receiver in this direction.</p> <p>Note that only maximum arguments alter the sizing behavior of the frame. Minimal and ideal arguments are only considered when the frame has hugging behavior (finite maximum argument) to choose the best possible size.</p> <h3 class="no_toc" id="aspect-ratio">Aspect Ratio</h3> <p>A view can be forced to a given aspect ratio with the <code class="language-plaintext highlighter-rouge">View/aspectRatio(_:contentMode:)</code> modifier. This modifier does not change the sizing behavior of the receiver, but if the receiver is expanding in all directions it guarantees that it fit or fills the parent view while the other direction is adjusted to satisfy the desired aspect ratio and content mode.</p> <table> <thead> <tr> <th style="text-align: left">Type</th> <th style="text-align: center">Category</th> <th style="text-align: center">Horizontal</th> <th style="text-align: center">Vertical</th> </tr> </thead> <tbody> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">View/aspectRatio(_:contentMode:)</code></td> <td style="text-align: center">Composed</td> <td style="text-align: center">neu</td> <td style="text-align: center">neu</td> </tr> </tbody> </table> <p>The aspect ratio is an optional parameter. If omitted the intrinsic aspect ratio of the receiver is used.</p> <h4 class="no_toc" id="remark-1">Remark</h4> <p>The intrinsic aspect ratio of the receiver is calculated from its intrinsic dimensions. As discussed in the <a href="#ambiguous-layouts">Ambiguous Layout</a> section, if some intrinsic dimension of the receiver cannot be determined SwiftUI will replace it with the magic value 10.</p> <h3 class="no_toc" id="fixed-size">Fixed Size</h3> <p>A view can be forced to its intrinsic size with the <code class="language-plaintext highlighter-rouge">View/fixedSize(horizontal:vertical:)</code> modifier in any direction, adopting hugging behavior. If not the view behavior is preserved in the corresponding direction.</p> <table> <thead> <tr> <th style="text-align: left"><code class="language-plaintext highlighter-rouge">horizontal</code> / <code class="language-plaintext highlighter-rouge">vertical</code> argument</th> <th style="text-align: center">Obtained behavior in the horizontal / vertical direction</th> </tr> </thead> <tbody> <tr> <td style="text-align: left">true</td> <td style="text-align: center">hug</td> </tr> <tr> <td style="text-align: left">false</td> <td style="text-align: center">neu</td> </tr> </tbody> </table> <p>As discussed in the <a href="#ambiguous-layouts">Ambiguous Layout</a> section, if some intrinsic dimension of the receiver cannot be determined SwiftUI will replace it with the magic value 10.</p> <h3 class="no_toc" id="common-decorators">Common Decorators</h3> <p>These views adopt the behavior of the receiver in all directions and decorate it.</p> <table> <thead> <tr> <th style="text-align: left">Type</th> <th style="text-align: center">Category</th> <th style="text-align: center">Horizontal</th> <th style="text-align: center">Vertical</th> </tr> </thead> <tbody> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">View/border(_:width:)</code></td> <td style="text-align: center">Composed</td> <td style="text-align: center">neu</td> <td style="text-align: center">neu</td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">View/background(_:alignment:)</code></td> <td style="text-align: center">Composed</td> <td style="text-align: center">neu</td> <td style="text-align: center">neu</td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">View/overlay(_:alignment:)</code></td> <td style="text-align: center">Composed</td> <td style="text-align: center">neu</td> <td style="text-align: center">neu</td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">View/offset(_:)</code> and <code class="language-plaintext highlighter-rouge">View/offset(x:y:)</code></td> <td style="text-align: center">Composed</td> <td style="text-align: center">neu</td> <td style="text-align: center">neu</td> </tr> </tbody> </table> <h3 class="no_toc" id="special-views">Special Views</h3> <p>The following are special views for positioning and grouping.</p> <table> <thead> <tr> <th style="text-align: left">Type</th> <th style="text-align: center">Category</th> <th style="text-align: center">Horizontal</th> <th style="text-align: center">Vertical</th> <th style="text-align: left">Remarks</th> </tr> </thead> <tbody> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">GeometryReader</code></td> <td style="text-align: center">Composed</td> <td style="text-align: center">exp</td> <td style="text-align: center">exp</td> <td style="text-align: left">Takes the whole size offered by its parent. Its <code class="language-plaintext highlighter-rouge">geometryProxy</code> parameter can be used for precise children placement within the associated region. If a child is not provided with a frame the geometry reader simply places it at the top left</td> </tr> <tr> <td style="text-align: left"><code class="language-plaintext highlighter-rouge">Group</code></td> <td style="text-align: center">Composed</td> <td style="text-align: center">neu</td> <td style="text-align: center">neu</td> <td style="text-align: left"> </td> </tr> </tbody> </table> <div class="footnotes" role="doc-endnotes"> <ol> <li id="fn:1" role="doc-endnote"> <p>The term <em>sizing behavior</em> is informally encountered in <a href="https://developer.apple.com/documentation/SwiftUI/View/frame(minWidth:idealWidth:maxWidth:minHeight:idealHeight:maxHeight:alignment:)">Apple frame documentation</a>. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p> </li> <li id="fn:2" role="doc-endnote"> <p>This is why the definition of expanding behavior mentions that the view <em>strives to match</em>, not that it <em>exactly matches</em> the size offered by its parent. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p> </li> <li id="fn:3" role="doc-endnote"> <p>This is for example how the aspect ratio modifier works. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p> </li> </ol> </div>The SwiftUI layout system is more predictable and easier to understand than UIKit layout system. But this does not mean how it works is entirely straightforward.Building a Collection For SwiftUI (Part 3) - Fixes and Focus Management2020-09-15T00:00:00+00:002020-09-15T00:00:00+00:00/swiftui_collection_part3<p>In <a href="/swiftui_collection_part2">part 2 of this article series</a> we implemented a first working collection view in SwiftUI, powered by <code class="language-plaintext highlighter-rouge">UICollectionView</code>. This implementation is promising but still has a few issues and shortcomings that we will address in this final article.</p> <p><em>This article is part 3 in the <a href="/swiftui_collection_intro">Building a Collection For SwiftUI</a> series</em>.</p> <ul id="markdown-toc"> <li><a href="#fixing-cell-frames" id="markdown-toc-fixing-cell-frames">Fixing Cell Frames</a></li> <li><a href="#supporting-focus-on-tvos" id="markdown-toc-supporting-focus-on-tvos">Supporting Focus on tvOS</a></li> <li><a href="#supporting-supplementary-views" id="markdown-toc-supporting-supplementary-views">Supporting Supplementary Views</a></li> <li><a href="#additional-considerations" id="markdown-toc-additional-considerations">Additional Considerations</a></li> <li><a href="#source-code" id="markdown-toc-source-code">Source Code</a></li> <li><a href="#wrapping-up" id="markdown-toc-wrapping-up">Wrapping Up</a></li> </ul> <h2 id="fixing-cell-frames">Fixing Cell Frames</h2> <p>When running our shelf example, cells initially on screen have correct frames, while cells emerging from screen edges do not:</p> <p><img src="/images/first_swiftui_collection.jpg" alt="SwiftUI CollectionView" /></p> <p>When inspected in the view debugger, <code class="language-plaintext highlighter-rouge">UICollectionViewCell</code> and <code class="language-plaintext highlighter-rouge">UIHostingController</code> view frames are fine, so the problem must be related to how SwiftUI assigns a frame to views contained in a <code class="language-plaintext highlighter-rouge">UIHostingController</code>. In fact, closer inspection of the applied frames reveals that the reduction in size is due to safe area insets being somehow applied.</p> <p>A <a href="https://stackoverflow.com/questions/61552497/uitableviewheaderfooterview-with-swiftui-content-getting-automatic-safe-area-ins">Stack Overflow thread</a> proposes a workaround for this issue until <code class="language-plaintext highlighter-rouge">UIHostingController</code> itself provides an official API to disable this behavior. This workaround applies swizzling to all hosting view instances indiscriminately, disabling safe area inset support entirely for all hosted SwiftUI views.</p> <p>This approach is too greedy and affects hosting controllers for which this behavior is actually legitimate and desired, for example a <code class="language-plaintext highlighter-rouge">UIHostingController</code> hosting the SwiftUI root of a <code class="language-plaintext highlighter-rouge">UIApplication</code>. A more surgical approach than method swizzling is to use <a href="https://funwithobjc.tumblr.com/post/1482787069/dynamic-subclassing">dynamic subclassing</a>, the runtime wizardry applied by key-value observing.</p> <p>To briefly sketch the theory behind dynamic subclassing, consider you have an object of some class whose behavior you want to tweak:</p> <ul> <li>First register a new subclass of this class at runtime.</li> <li>Then add methods to the subclass (possibly calling a parent implementation if this makes sense).</li> <li>Finally change the class of the object to the subclass.</li> </ul> <p>In our case, we can provide the missing opt-in behavior we need as an extension on <code class="language-plaintext highlighter-rouge">UIHostingController</code>, applied by dynamically changing the hosting view class to a subclass ignoring safe area insets:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">extension</span> <span class="kt">UIHostingController</span> <span class="p">{</span> <span class="kd">convenience</span> <span class="kd">public</span> <span class="nf">init</span><span class="p">(</span><span class="nv">rootView</span><span class="p">:</span> <span class="kt">Content</span><span class="p">,</span> <span class="nv">ignoreSafeArea</span><span class="p">:</span> <span class="kt">Bool</span><span class="p">)</span> <span class="p">{</span> <span class="k">self</span><span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="nv">rootView</span><span class="p">:</span> <span class="n">rootView</span><span class="p">)</span> <span class="k">if</span> <span class="n">ignoreSafeArea</span> <span class="p">{</span> <span class="nf">disableSafeArea</span><span class="p">()</span> <span class="p">}</span> <span class="p">}</span> <span class="kd">func</span> <span class="nf">disableSafeArea</span><span class="p">()</span> <span class="p">{</span> <span class="k">guard</span> <span class="k">let</span> <span class="nv">viewClass</span> <span class="o">=</span> <span class="nf">object_getClass</span><span class="p">(</span><span class="n">view</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span> <span class="k">let</span> <span class="nv">viewSubclassName</span> <span class="o">=</span> <span class="kt">String</span><span class="p">(</span><span class="nv">cString</span><span class="p">:</span> <span class="nf">class_getName</span><span class="p">(</span><span class="n">viewClass</span><span class="p">))</span><span class="o">.</span><span class="nf">appending</span><span class="p">(</span><span class="s">"_IgnoreSafeArea"</span><span class="p">)</span> <span class="k">if</span> <span class="k">let</span> <span class="nv">viewSubclass</span> <span class="o">=</span> <span class="kt">NSClassFromString</span><span class="p">(</span><span class="n">viewSubclassName</span><span class="p">)</span> <span class="p">{</span> <span class="nf">object_setClass</span><span class="p">(</span><span class="n">view</span><span class="p">,</span> <span class="n">viewSubclass</span><span class="p">)</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="k">guard</span> <span class="k">let</span> <span class="nv">viewClassNameUtf8</span> <span class="o">=</span> <span class="p">(</span><span class="n">viewSubclassName</span> <span class="k">as</span> <span class="kt">NSString</span><span class="p">)</span><span class="o">.</span><span class="n">utf8String</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span> <span class="k">guard</span> <span class="k">let</span> <span class="nv">viewSubclass</span> <span class="o">=</span> <span class="nf">objc_allocateClassPair</span><span class="p">(</span><span class="n">viewClass</span><span class="p">,</span> <span class="n">viewClassNameUtf8</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span> <span class="k">if</span> <span class="k">let</span> <span class="nv">method</span> <span class="o">=</span> <span class="nf">class_getInstanceMethod</span><span class="p">(</span><span class="kt">UIView</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="kd">#selector(</span><span class="nf">getter: UIView.safeAreaInsets</span><span class="kd">)</span><span class="p">)</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">safeAreaInsets</span><span class="p">:</span> <span class="kd">@convention</span><span class="p">(</span><span class="n">block</span><span class="p">)</span> <span class="p">(</span><span class="kt">AnyObject</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">UIEdgeInsets</span> <span class="o">=</span> <span class="p">{</span> <span class="n">_</span> <span class="k">in</span> <span class="k">return</span> <span class="o">.</span><span class="n">zero</span> <span class="p">}</span> <span class="nf">class_addMethod</span><span class="p">(</span><span class="n">viewSubclass</span><span class="p">,</span> <span class="kd">#selector(</span><span class="nf">getter: UIView.safeAreaInsets</span><span class="kd">)</span><span class="p">,</span> <span class="nf">imp_implementationWithBlock</span><span class="p">(</span><span class="n">safeAreaInsets</span><span class="p">),</span> <span class="nf">method_getTypeEncoding</span><span class="p">(</span><span class="n">method</span><span class="p">))</span> <span class="p">}</span> <span class="nf">objc_registerClassPair</span><span class="p">(</span><span class="n">viewSubclass</span><span class="p">)</span> <span class="nf">object_setClass</span><span class="p">(</span><span class="n">view</span><span class="p">,</span> <span class="n">viewSubclass</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>This way we only apply a change of behavior to those <code class="language-plaintext highlighter-rouge">UIHostingController</code> instances for which safe area insets must not be taken into account, for example in our <code class="language-plaintext highlighter-rouge">HostCell</code> implementation:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">hostController</span> <span class="o">=</span> <span class="kt">UIHostingController</span><span class="p">(</span><span class="nv">rootView</span><span class="p">:</span> <span class="n">view</span><span class="p">,</span> <span class="nv">ignoreSafeArea</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span> </code></pre></div></div> <p>With this simple trick cell frames are now correct:</p> <p><img src="/images/collection_fixed_cell_size.jpg" alt="Fixed frame" /></p> <h2 id="supporting-focus-on-tvos">Supporting Focus on tvOS</h2> <p>In the shelf example discussed at the end of <a href="/swiftui_collection_part2">part 2 of this article series</a>, using the new tvOS 14 <code class="language-plaintext highlighter-rouge">CardButtonStyle</code>, the usual focused appearance is not applied when running the application. This is actually expected behavior, as the cell itself is focusable and the button style <a href="https://developer.apple.com/videos/play/wwdc2020/10042">is not meant to receive focus in such cases</a>.</p> <p>In fact, SwiftUI buttons wrapped in focusable cells cannot be triggered at all. As a good SwiftUI citizen, our <code class="language-plaintext highlighter-rouge">CollectionView</code> must not prevent buttons from being used in cells, therefore host cells must not be focusable themselves. This also means we will not respond to collection cell standard selection delegate either, but rather let buttons handle actions.</p> <p>One way to disable focus for a cell is by implementing <code class="language-plaintext highlighter-rouge">canBecomeFocused</code> on the cell class itself and return <code class="language-plaintext highlighter-rouge">false</code>. This seems to work well in general, but this strategy breaks when data source changes are applied with animations. In such cases the focus often spins out of control on tvOS. We therefore need a better approach.</p> <h3 class="no_toc" id="uicollectionview-and-animated-reloads">UICollectionView and Animated Reloads</h3> <p>To understand this buggy behavior we need to figure the expected behavior of a <code class="language-plaintext highlighter-rouge">UICollectionView</code> when an animated reload occurs. To find the answer I simply implemented a basic collection in UIKit, with a simple data source and focusable <code class="language-plaintext highlighter-rouge">UICollectionViewCell</code>s:</p> <ul> <li>On tvOS I could observe that the focused item is followed when data source changes are animated. The focus never spins out of control during reloads. There is still a minor issue with the focused appearance being lost after the animation ends (likely a <code class="language-plaintext highlighter-rouge">UICollectionView</code> bug), but it suffices to swipe the remote again to have a nearby item focused.</li> <li>On iOS the content offset is simply preserved, as there is no currently focus concept.</li> </ul> <p>This user experience sets our goal for our SwiftUI <code class="language-plaintext highlighter-rouge">CollectionView</code> focus behavior.</p> <h3 class="no_toc" id="disabling-and-enabling-focus">Disabling and Enabling Focus</h3> <p>Disabling focus on cell classes directly does not work, but a similar result can be achieved thanks to <code class="language-plaintext highlighter-rouge">UICollectionViewDelegate</code>, which provides a dedicated delegate method to decide at any time whether a cell must be focusable or not. We therefore make our <code class="language-plaintext highlighter-rouge">Coordinator</code> conform to this protocol and introduce an internal flag to enable or disable focus for all cells when we want:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">struct</span> <span class="kt">CollectionView</span><span class="o">&lt;</span><span class="kt">Section</span><span class="p">:</span> <span class="kt">Hashable</span><span class="p">,</span> <span class="kt">Item</span><span class="p">:</span> <span class="kt">Hashable</span><span class="p">,</span> <span class="kt">Cell</span><span class="p">:</span> <span class="kt">View</span><span class="o">&gt;</span><span class="p">:</span> <span class="kt">UIViewRepresentable</span> <span class="p">{</span> <span class="kd">public</span> <span class="kd">class</span> <span class="kt">Coordinator</span><span class="p">:</span> <span class="kt">NSObject</span><span class="p">,</span> <span class="kt">UICollectionViewDelegate</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="kd">fileprivate</span> <span class="k">var</span> <span class="nv">isFocusable</span><span class="p">:</span> <span class="kt">Bool</span> <span class="o">=</span> <span class="kc">false</span> <span class="kd">public</span> <span class="kd">func</span> <span class="nf">collectionView</span><span class="p">(</span><span class="n">_</span> <span class="nv">collectionView</span><span class="p">:</span> <span class="kt">UICollectionView</span><span class="p">,</span> <span class="n">canFocusItemAt</span> <span class="nv">indexPath</span><span class="p">:</span> <span class="kt">IndexPath</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Bool</span> <span class="p">{</span> <span class="k">return</span> <span class="n">isFocusable</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>We can now enable <code class="language-plaintext highlighter-rouge">UICollectionView</code> cell focus during reloads, letting the collection view correctly follow the currently focused item. The rest of the time cells must not be focusable. We strive to enable focus for as little time as possible, and forcing a focus update before restting the flag to its nominal value is sufficient to achieve proper behavior:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">struct</span> <span class="kt">CollectionView</span><span class="o">&lt;</span><span class="kt">Section</span><span class="p">:</span> <span class="kt">Hashable</span><span class="p">,</span> <span class="kt">Item</span><span class="p">:</span> <span class="kt">Hashable</span><span class="p">,</span> <span class="kt">Cell</span><span class="p">:</span> <span class="kt">View</span><span class="o">&gt;</span><span class="p">:</span> <span class="kt">UIViewRepresentable</span> <span class="p">{</span> <span class="kd">private</span> <span class="kd">func</span> <span class="nf">reloadData</span><span class="p">(</span><span class="k">in</span> <span class="nv">collectionView</span><span class="p">:</span> <span class="kt">UICollectionView</span><span class="p">,</span> <span class="nv">context</span><span class="p">:</span> <span class="kt">Context</span><span class="p">,</span> <span class="nv">animated</span><span class="p">:</span> <span class="kt">Bool</span> <span class="o">=</span> <span class="kc">false</span><span class="p">)</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">coordinator</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">coordinator</span> <span class="n">coordinator</span><span class="o">.</span><span class="n">sectionLayoutProvider</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="n">sectionLayoutProvider</span> <span class="k">guard</span> <span class="k">let</span> <span class="nv">dataSource</span> <span class="o">=</span> <span class="n">coordinator</span><span class="o">.</span><span class="n">dataSource</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span> <span class="k">let</span> <span class="nv">rowsHash</span> <span class="o">=</span> <span class="n">rows</span><span class="o">.</span><span class="n">hashValue</span> <span class="k">if</span> <span class="n">coordinator</span><span class="o">.</span><span class="n">rowsHash</span> <span class="o">!=</span> <span class="n">rowsHash</span> <span class="p">{</span> <span class="n">dataSource</span><span class="o">.</span><span class="nf">apply</span><span class="p">(</span><span class="nf">snapshot</span><span class="p">(),</span> <span class="nv">animatingDifferences</span><span class="p">:</span> <span class="n">animated</span><span class="p">)</span> <span class="p">{</span> <span class="n">coordinator</span><span class="o">.</span><span class="n">isFocusable</span> <span class="o">=</span> <span class="kc">true</span> <span class="n">collectionView</span><span class="o">.</span><span class="nf">setNeedsFocusUpdate</span><span class="p">()</span> <span class="n">collectionView</span><span class="o">.</span><span class="nf">updateFocusIfNeeded</span><span class="p">()</span> <span class="n">coordinator</span><span class="o">.</span><span class="n">isFocusable</span> <span class="o">=</span> <span class="kc">false</span> <span class="p">}</span> <span class="n">coordinator</span><span class="o">.</span><span class="n">rowsHash</span> <span class="o">=</span> <span class="n">rowsHash</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>Note that this requires the <code class="language-plaintext highlighter-rouge">UICollectionView</code> to be provided as additional parameter to our reload method.</p> <p>With these changes the focus now appears as expected and the focused item is followed when the data source is updated:</p> <p><img src="/images/collection_fixed_focus.jpg" alt="Fixed focus" /></p> <h3 class="no_toc" id="remark">Remark</h3> <p>During my investigations I worked with focusable cells for a while until I realized this was a bad idea. Here are a few more findings if you are interested.</p> <p>If we keep cells focusable <code class="language-plaintext highlighter-rouge">Button</code>s are basically useless, as said above. Moreover, focused appearance must be implemented manually by tweaking view properties. Scaling is easy but tilting is another matter, and the native tvOS look &amp; feel is not easy to reproduce accurately.</p> <p>Finally, the pressed appearance (obtained for free with <code class="language-plaintext highlighter-rouge">CardButtonStyle</code>) requires catching cell interactions and transferring them up the SwiftUI view hierarchy, for example through the <code class="language-plaintext highlighter-rouge">@Environment</code> using a custom <code class="language-plaintext highlighter-rouge">EnvironmentKey</code>. Again this requires the appearance (scaling) to be adjusted manually.</p> <p>For all these reasons I recommend avoiding focusable cells if you intend to wrap SwiftUI views within them.</p> <h2 id="supporting-supplementary-views">Supporting Supplementary Views</h2> <p>Supporting supplementary views is very similar to supporting cells. We simply introduce a dedicated view builder and a host view. As for cells, type inference requires the addition of a new <code class="language-plaintext highlighter-rouge">SupplementaryView </code>type parameter to the generic <code class="language-plaintext highlighter-rouge">CollectionView</code> type:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">CollectionView</span><span class="o">&lt;</span><span class="kt">Section</span><span class="p">:</span> <span class="kt">Hashable</span><span class="p">,</span> <span class="kt">Item</span><span class="p">:</span> <span class="kt">Hashable</span><span class="p">,</span> <span class="kt">Cell</span><span class="p">:</span> <span class="kt">View</span><span class="p">,</span> <span class="kt">SupplementaryView</span><span class="p">:</span> <span class="kt">View</span><span class="o">&gt;</span><span class="p">:</span> <span class="kt">UIViewRepresentable</span> <span class="p">{</span> <span class="kd">private</span> <span class="kd">class</span> <span class="kt">HostSupplementaryView</span><span class="p">:</span> <span class="kt">UICollectionReusableView</span> <span class="p">{</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">hostController</span><span class="p">:</span> <span class="kt">UIHostingController</span><span class="o">&lt;</span><span class="kt">SupplementaryView</span><span class="o">&gt;</span><span class="p">?</span> <span class="k">override</span> <span class="kd">func</span> <span class="nf">prepareForReuse</span><span class="p">()</span> <span class="p">{</span> <span class="k">if</span> <span class="k">let</span> <span class="nv">hostView</span> <span class="o">=</span> <span class="n">hostController</span><span class="p">?</span><span class="o">.</span><span class="n">view</span> <span class="p">{</span> <span class="n">hostView</span><span class="o">.</span><span class="nf">removeFromSuperview</span><span class="p">()</span> <span class="p">}</span> <span class="n">hostController</span> <span class="o">=</span> <span class="kc">nil</span> <span class="p">}</span> <span class="k">var</span> <span class="nv">hostedSupplementaryView</span><span class="p">:</span> <span class="kt">SupplementaryView</span><span class="p">?</span> <span class="p">{</span> <span class="k">willSet</span> <span class="p">{</span> <span class="k">guard</span> <span class="k">let</span> <span class="nv">view</span> <span class="o">=</span> <span class="n">newValue</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span> <span class="n">hostController</span> <span class="o">=</span> <span class="kt">UIHostingController</span><span class="p">(</span><span class="nv">rootView</span><span class="p">:</span> <span class="n">view</span><span class="p">,</span> <span class="nv">ignoreSafeArea</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span> <span class="k">if</span> <span class="k">let</span> <span class="nv">hostView</span> <span class="o">=</span> <span class="n">hostController</span><span class="p">?</span><span class="o">.</span><span class="n">view</span> <span class="p">{</span> <span class="n">hostView</span><span class="o">.</span><span class="n">frame</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="n">bounds</span> <span class="n">hostView</span><span class="o">.</span><span class="n">autoresizingMask</span> <span class="o">=</span> <span class="p">[</span><span class="o">.</span><span class="n">flexibleWidth</span><span class="p">,</span> <span class="o">.</span><span class="n">flexibleHeight</span><span class="p">]</span> <span class="nf">addSubview</span><span class="p">(</span><span class="n">hostView</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="c1">// ...</span> <span class="k">let</span> <span class="nv">supplementaryView</span><span class="p">:</span> <span class="p">(</span><span class="kt">String</span><span class="p">,</span> <span class="kt">IndexPath</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">SupplementaryView</span> <span class="nf">init</span><span class="p">(</span><span class="nv">rows</span><span class="p">:</span> <span class="p">[</span><span class="kt">CollectionRow</span><span class="o">&lt;</span><span class="kt">Section</span><span class="p">,</span> <span class="kt">Item</span><span class="o">&gt;</span><span class="p">],</span> <span class="nv">sectionLayoutProvider</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="kt">Int</span><span class="p">,</span> <span class="kt">NSCollectionLayoutEnvironment</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">NSCollectionLayoutSection</span><span class="p">,</span> <span class="kd">@ViewBuilder</span> <span class="nv">cell</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="kt">IndexPath</span><span class="p">,</span> <span class="kt">Item</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Cell</span><span class="p">,</span> <span class="kd">@ViewBuilder</span> <span class="nv">supplementaryView</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="kt">String</span><span class="p">,</span> <span class="kt">IndexPath</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">SupplementaryView</span><span class="p">)</span> <span class="p">{</span> <span class="k">self</span><span class="o">.</span><span class="n">rows</span> <span class="o">=</span> <span class="n">rows</span> <span class="k">self</span><span class="o">.</span><span class="n">sectionLayoutProvider</span> <span class="o">=</span> <span class="n">sectionLayoutProvider</span> <span class="k">self</span><span class="o">.</span><span class="n">cell</span> <span class="o">=</span> <span class="n">cell</span> <span class="k">self</span><span class="o">.</span><span class="n">supplementaryView</span> <span class="o">=</span> <span class="n">supplementaryView</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>Supplementary views are registered for a single reuse identifier (for the same reason a single identifier is required for cells) but specific kinds, e.g. header, footer or custom. We store known kinds in our coordinator as they are registered so that each required registration is made at most once:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">CollectionView</span><span class="o">&lt;</span><span class="kt">Section</span><span class="p">:</span> <span class="kt">Hashable</span><span class="p">,</span> <span class="kt">Item</span><span class="p">:</span> <span class="kt">Hashable</span><span class="p">,</span> <span class="kt">Cell</span><span class="p">:</span> <span class="kt">View</span><span class="p">,</span> <span class="kt">SupplementaryView</span><span class="p">:</span> <span class="kt">View</span><span class="o">&gt;</span><span class="p">:</span> <span class="kt">UIViewRepresentable</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="kd">class</span> <span class="kt">Coordinator</span><span class="p">:</span> <span class="kt">NSObject</span><span class="p">,</span> <span class="kt">UICollectionViewDelegate</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="kd">fileprivate</span> <span class="k">var</span> <span class="nv">registeredSupplementaryViewKinds</span><span class="p">:</span> <span class="p">[</span><span class="kt">String</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span> <span class="p">}</span> <span class="kd">func</span> <span class="nf">makeUIView</span><span class="p">(</span><span class="nv">context</span><span class="p">:</span> <span class="kt">Context</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">UICollectionView</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">cellIdentifier</span> <span class="o">=</span> <span class="s">"hostCell"</span> <span class="k">let</span> <span class="nv">supplementaryViewIdentifier</span> <span class="o">=</span> <span class="s">"hostSupplementaryView"</span> <span class="k">let</span> <span class="nv">collectionView</span> <span class="o">=</span> <span class="kt">UICollectionView</span><span class="p">(</span><span class="nv">frame</span><span class="p">:</span> <span class="o">.</span><span class="n">zero</span><span class="p">,</span> <span class="nv">collectionViewLayout</span><span class="p">:</span> <span class="nf">layout</span><span class="p">(</span><span class="nv">context</span><span class="p">:</span> <span class="n">context</span><span class="p">))</span> <span class="n">collectionView</span><span class="o">.</span><span class="nf">register</span><span class="p">(</span><span class="kt">HostCell</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forCellWithReuseIdentifier</span><span class="p">:</span> <span class="n">cellIdentifier</span><span class="p">)</span> <span class="k">let</span> <span class="nv">dataSource</span> <span class="o">=</span> <span class="kt">Coordinator</span><span class="o">.</span><span class="kt">DataSource</span><span class="p">(</span><span class="nv">collectionView</span><span class="p">:</span> <span class="n">collectionView</span><span class="p">)</span> <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">item</span> <span class="k">in</span> <span class="k">let</span> <span class="nv">hostCell</span> <span class="o">=</span> <span class="n">collectionView</span><span class="o">.</span><span class="nf">dequeueReusableCell</span><span class="p">(</span><span class="nv">withReuseIdentifier</span><span class="p">:</span> <span class="n">cellIdentifier</span><span class="p">,</span> <span class="nv">for</span><span class="p">:</span> <span class="n">indexPath</span><span class="p">)</span> <span class="k">as?</span> <span class="kt">HostCell</span> <span class="n">hostCell</span><span class="p">?</span><span class="o">.</span><span class="n">hostedCell</span> <span class="o">=</span> <span class="nf">cell</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="k">return</span> <span class="n">hostCell</span> <span class="p">}</span> <span class="n">context</span><span class="o">.</span><span class="n">coordinator</span><span class="o">.</span><span class="n">dataSource</span> <span class="o">=</span> <span class="n">dataSource</span> <span class="n">dataSource</span><span class="o">.</span><span class="n">supplementaryViewProvider</span> <span class="o">=</span> <span class="p">{</span> <span class="n">collectionView</span><span class="p">,</span> <span class="n">kind</span><span class="p">,</span> <span class="n">indexPath</span> <span class="k">in</span> <span class="k">let</span> <span class="nv">coordinator</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">coordinator</span> <span class="k">if</span> <span class="o">!</span><span class="n">coordinator</span><span class="o">.</span><span class="n">registeredSupplementaryViewKinds</span><span class="o">.</span><span class="nf">contains</span><span class="p">(</span><span class="n">kind</span><span class="p">)</span> <span class="p">{</span> <span class="n">collectionView</span><span class="o">.</span><span class="nf">register</span><span class="p">(</span><span class="kt">HostSupplementaryView</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forSupplementaryViewOfKind</span><span class="p">:</span> <span class="n">kind</span><span class="p">,</span> <span class="nv">withReuseIdentifier</span><span class="p">:</span> <span class="n">supplementaryViewIdentifier</span><span class="p">)</span> <span class="n">coordinator</span><span class="o">.</span><span class="n">registeredSupplementaryViewKinds</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="n">kind</span><span class="p">)</span> <span class="p">}</span> <span class="k">guard</span> <span class="k">let</span> <span class="nv">view</span> <span class="o">=</span> <span class="n">collectionView</span><span class="o">.</span><span class="nf">dequeueReusableSupplementaryView</span><span class="p">(</span><span class="nv">ofKind</span><span class="p">:</span> <span class="n">kind</span><span class="p">,</span> <span class="nv">withReuseIdentifier</span><span class="p">:</span> <span class="n">supplementaryViewIdentifier</span><span class="p">,</span> <span class="nv">for</span><span class="p">:</span> <span class="n">indexPath</span><span class="p">)</span> <span class="k">as?</span> <span class="kt">HostSupplementaryView</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">nil</span> <span class="p">}</span> <span class="n">view</span><span class="o">.</span><span class="n">hostedSupplementaryView</span> <span class="o">=</span> <span class="nf">supplementaryView</span><span class="p">(</span><span class="n">kind</span><span class="p">,</span> <span class="n">indexPath</span><span class="p">)</span> <span class="k">return</span> <span class="n">view</span> <span class="p">}</span> <span class="nf">reloadData</span><span class="p">(</span><span class="nv">in</span><span class="p">:</span><span class="n">collectionView</span><span class="p">,</span> <span class="nv">context</span><span class="p">:</span> <span class="n">context</span><span class="p">)</span> <span class="k">return</span> <span class="n">collectionView</span> <span class="p">}</span> <span class="c1">// ...</span> <span class="p">}</span> </code></pre></div></div> <p>Supplementary views must be added to your layout and defined within the corresponding view builder. Refer to the source code (link at the end of this article) for an example of use.</p> <h2 id="additional-considerations">Additional Considerations</h2> <p>Before we wrap things up, I just wanted to mention a few important behaviors related to this implementation:</p> <ul> <li>Since cells are reused any <code class="language-plaintext highlighter-rouge">@State</code> they store is temporary. Persistent state should be stored in your model instead.</li> <li>Diffable data sources need each item to have a unique identifier, no matter the row it appears in, otherwise errors will be reported. This is because items can be moved between sections in general. Several sections can display the same item, but in order to do so you need to make the item truly unique for each section, for example by wrapping it into a <code class="language-plaintext highlighter-rouge">struct</code> and associating it with its section.</li> <li>Changes are detected based on hashes. If a displayed item changes internally (e.g. some title changes) but its hash does not change, no update will be triggered.</li> </ul> <p>Further improvements could be made and are left as exercise for the reader:</p> <ul> <li>Make supplementary view support optional.</li> <li>Make tabs optionally scroll with the content on tvOS (Hint: <code class="language-plaintext highlighter-rouge">tabBarObservedScrollView</code>).</li> <li>Use decoration views to add focus guides for navigation between rows of different length on tvOS.</li> <li>Port this collection to macOS and <code class="language-plaintext highlighter-rouge">NSCollectionView</code>. I read somewhere that nested stacks and scroll view performance was poor on macOS as well, probably because macOS has focus support for keyboard navigation. It is therefore likely that the approach discussed for tvOS can be applied to macOS in a similar way.</li> </ul> <h2 id="source-code">Source Code</h2> <p>The <a href="https://github.com/defagos/SwiftUICollection">code for the collection view</a> (&lt; 200 LOC) and an example of use is available on GitHub. I even added a SPM manifest so that you can quickly test the collection in a project of your own. This does not mean the code is intended to be used as a library yet (it should be applied to a broader set of cases first), but feel free to use it any way you want.</p> <h2 id="wrapping-up">Wrapping Up</h2> <p>This article is the last one in this series. I hope you enjoyed the ride and learned a few things!</p> <p>You can also return to the <a href="/swiftui_collection_intro">introduction</a>.</p>In part 2 of this article series we implemented a first working collection view in SwiftUI, powered by UICollectionView. This implementation is promising but still has a few issues and shortcomings that we will address in this final article.Building a Collection For SwiftUI (Part 2) - SwiftUI Collection Implementation2020-09-14T00:00:00+00:002020-09-14T00:00:00+00:00/swiftui_collection_part2<p>Faced with the inability to reach an acceptable level of performance with grid layouts made of simple SwiftUI building blocks, I decided to roll my own solution.</p> <p><em>This article is part 2 in the <a href="/swiftui_collection_intro">Building a Collection For SwiftUI</a> series</em>.</p> <ul id="markdown-toc"> <li><a href="#implementation-strategies-and-requirements" id="markdown-toc-implementation-strategies-and-requirements">Implementation Strategies and Requirements</a></li> <li><a href="#wrapping-uikit-collection-view-into-swiftui" id="markdown-toc-wrapping-uikit-collection-view-into-swiftui">Wrapping UIKit Collection View Into SwiftUI</a></li> <li><a href="#providing-a-collection-view-layout" id="markdown-toc-providing-a-collection-view-layout">Providing a Collection View Layout</a></li> <li><a href="#loading-data-into-the-collection" id="markdown-toc-loading-data-into-the-collection">Loading Data Into the Collection</a></li> <li><a href="#data-source-and-coordinator" id="markdown-toc-data-source-and-coordinator">Data Source and Coordinator</a></li> <li><a href="#data-source-snapshots" id="markdown-toc-data-source-snapshots">Data Source Snapshots</a></li> <li><a href="#solving-performance-issues" id="markdown-toc-solving-performance-issues">Solving Performance Issues</a></li> <li><a href="#collection-layout-support" id="markdown-toc-collection-layout-support">Collection Layout Support</a></li> <li><a href="#cell-layout" id="markdown-toc-cell-layout">Cell Layout</a></li> <li><a href="#cell-display" id="markdown-toc-cell-display">Cell Display</a></li> <li><a href="#example-of-a-grid-layout" id="markdown-toc-example-of-a-grid-layout">Example of a Grid Layout</a></li> <li><a href="#wrapping-up" id="markdown-toc-wrapping-up">Wrapping Up</a></li> </ul> <h2 id="implementation-strategies-and-requirements">Implementation Strategies and Requirements</h2> <p>Two possible implementation strategies can be considered when implementing a SwiftUI collection:</p> <ul> <li>Reproducing <code class="language-plaintext highlighter-rouge">UICollectionView</code> entirely in SwiftUI, including cell reuse and decoration views, not to mention flexible layout support.</li> <li>Wrapping <code class="language-plaintext highlighter-rouge">UICollectionView</code> into a SwifUI <code class="language-plaintext highlighter-rouge">UIViewRepresentable</code>.</li> </ul> <p>Since <code class="language-plaintext highlighter-rouge">UICollectionView</code> is already mature and has been greatly enhanced lately, the second option is obviously more manageable.</p> <p>I added a few implementation constraints to ensure the custom collection view feels like a native SwiftUI component:</p> <ul> <li>It must nicely integrate with SwiftUI declarative formalism.</li> <li>Cells and supplementary views must be built in SwiftUI, not in UIKit, and certainly not with xibs or constraint-based layouts.</li> <li>Cells and supplementary views must be lazily instantiated and reused. Put another way, scrolling performance must be similar to what is usually expected from a <code class="language-plaintext highlighter-rouge">UICollectionView</code>.</li> <li>The collection view must support arbitrary layouts and nicely update when SwiftUI detects a change to a source of truth.</li> <li>Focus must be correctly managed on tvOS, in particular it should be possible to implement the standard user experience expected on Apple TV according to the <a href="https://developer.apple.com/design/human-interface-guidelines/tvos/overview/focus-and-parallax">Human Interface Guidelines</a>.</li> </ul> <p>Let us start by wrapping a UIKit collection into a SwiftUI view.</p> <h2 id="wrapping-uikit-collection-view-into-swiftui">Wrapping UIKit Collection View Into SwiftUI</h2> <p>A great feature of SwiftUI is the ease with which you can embed UIKit views in SwiftUI and conversely. This makes it easy to adopt SwiftUI where you can in your app, while still using good old UIKit where SwiftUI is lagging behind in functionality or performance.</p> <p>SwiftUI provides the <code class="language-plaintext highlighter-rouge">UIViewRepresentable</code> protocol to <a href="https://developer.apple.com/documentation/swiftui/uiviewrepresentable">wrap a UIKit view for use in SwiftUI</a>. We can also wrap a view controller into a <code class="language-plaintext highlighter-rouge">UIViewControllerRepresentable</code>, but I here prefer the former approach as there is here no real competitive advantage in wrapping <code class="language-plaintext highlighter-rouge">UICollectionViewController</code> instead of <code class="language-plaintext highlighter-rouge">UICollectionView</code> directly.</p> <p>If you are not familiar with SwiftUI, just recall that a <code class="language-plaintext highlighter-rouge">View</code> in SwiftUI can be seen as a short-lived <em>layout snapshot</em>, unlike a <code class="language-plaintext highlighter-rouge">UIView</code> in UIKit which is actually <em>what is drawn on screen</em>. To create and update content on screen in SwiftUI you therefore provide an up-to-date snapshot of your layout which then gets applied by the SwiftUI layout engine depending on what has changed. Snapshots are then discarded until new ones are required by further updates.</p> <p>When interfacing UIKit with SwiftUI this important distiction translates into two protocol methods required by <code class="language-plaintext highlighter-rouge">UIViewRepresentable</code>, so that <code class="language-plaintext highlighter-rouge">View</code> snapshots can create or update the matching <code class="language-plaintext highlighter-rouge">UIView</code> on screen:</p> <ul> <li><code class="language-plaintext highlighter-rouge">makeUIView(context:)</code> is called when the <code class="language-plaintext highlighter-rouge">UIView</code> described by a <code class="language-plaintext highlighter-rouge">View</code> needs to be created.</li> <li><code class="language-plaintext highlighter-rouge">updateUIView(_:context:)</code> is called when the available <code class="language-plaintext highlighter-rouge">UIView</code> described by a <code class="language-plaintext highlighter-rouge">View</code> needs to be updated.</li> </ul> <p>We therefore start our implementation by conforming our new <code class="language-plaintext highlighter-rouge">CollectionView</code> type to <code class="language-plaintext highlighter-rouge">UIViewRepresentable</code> and implementing its two required methods:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">CollectionView</span><span class="p">:</span> <span class="kt">UIViewRepresentable</span> <span class="p">{</span> <span class="kd">func</span> <span class="nf">makeUIView</span><span class="p">(</span><span class="nv">context</span><span class="p">:</span> <span class="kt">Context</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">UICollectionView</span> <span class="p">{</span> <span class="c1">// Create the collection view for the first time</span> <span class="p">}</span> <span class="kd">func</span> <span class="nf">updateUIView</span><span class="p">(</span><span class="n">_</span> <span class="nv">uiView</span><span class="p">:</span> <span class="kt">UICollectionView</span><span class="p">,</span> <span class="nv">context</span><span class="p">:</span> <span class="kt">Context</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// Update the existing collection view</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>In our case the actual <code class="language-plaintext highlighter-rouge">UICollectionView</code> instance must be created in <code class="language-plaintext highlighter-rouge">makeUIView(context:)</code>, but this requires a collection view layout to be provided at initialization time first.</p> <h2 id="providing-a-collection-view-layout">Providing a Collection View Layout</h2> <p>With iOS 13 and tvOS 13 collection view layouts can be provided in a general declarative way through <a href="https://developer.apple.com/videos/play/wwdc2019/215">compositional layouts</a>. The obtained layout code is expressive and supports advanced features like <a href="https://developer.apple.com/documentation/uikit/uicollectionlayoutsectionorthogonalscrollingbehavior">independently scrollable sections</a>.</p> <p>Because SwiftUI was introduced with iOS and tvOS 13, compositional layouts are the obvious choice for our implementation:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">CollectionView</span><span class="p">:</span> <span class="kt">UIViewRepresentable</span> <span class="p">{</span> <span class="kd">private</span> <span class="kd">func</span> <span class="nf">layout</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">UICollectionViewLayout</span> <span class="p">{</span> <span class="k">return</span> <span class="kt">UICollectionViewCompositionalLayout</span> <span class="p">{</span> <span class="n">sectionIndex</span><span class="p">,</span> <span class="n">layoutEnvironment</span> <span class="k">in</span> <span class="c1">// Return section layout</span> <span class="p">}</span> <span class="p">}</span> <span class="kd">func</span> <span class="nf">makeUIView</span><span class="p">(</span><span class="nv">context</span><span class="p">:</span> <span class="kt">Context</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">UICollectionView</span> <span class="p">{</span> <span class="k">return</span> <span class="kt">UICollectionView</span><span class="p">(</span><span class="nv">frame</span><span class="p">:</span> <span class="o">.</span><span class="n">zero</span><span class="p">,</span> <span class="nv">collectionViewLayout</span><span class="p">:</span> <span class="nf">layout</span><span class="p">())</span> <span class="p">}</span> <span class="kd">func</span> <span class="nf">updateUIView</span><span class="p">(</span><span class="n">_</span> <span class="nv">uiView</span><span class="p">:</span> <span class="kt">UICollectionView</span><span class="p">,</span> <span class="nv">context</span><span class="p">:</span> <span class="kt">Context</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// Update the existing collection view</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>Note that we provide <code class="language-plaintext highlighter-rouge">.zero</code> as frame since SwiftUI is responsible of applying a suitable frame when the <code class="language-plaintext highlighter-rouge">UICollectionView</code> is actually drawn on screen.</p> <p>Compositional layouts are defined using sections, each section containing groups of items with supplementary or decoration views, and possibly independent orthogonal scrolling behavior. How and where this layout definition should be provided is yet still unclear, we just know it must ultimately be delivered by a <code class="language-plaintext highlighter-rouge">layout()</code> method.</p> <p>Before we proceed with finding how to actually provide section layout definitions, let us first discuss how data will be loaded into the collection.</p> <h2 id="loading-data-into-the-collection">Loading Data Into the Collection</h2> <p>Since iOS 13 and tvOS 13, and in addition to compositional layouts, UIKit provides <a href="https://developer.apple.com/videos/play/wwdc2019/220">diffable data sources</a> to incrementally update data associated with a collection and animate changes. Such data sources ensure that cells and underlying data stay consistent, avoiding crashes ususally associated with mismatches between data and layout.</p> <p>When a data change needs to be made, a corresponding snapshot must be created and applied to the data source, which then takes care of the updating the associated collection view. This approach is quite similar to how SwiftUI reacts to data changes in general: When a change is detected a new layout description is provided to represent the new state and SwiftUI takes care of applying it to update the content visible on screen. Diffable data sources therefore seem quite appropriate in achieving what we need.</p> <p>Let us first briefly consider from a client perspective. A parent content view which displays some <code class="language-plaintext highlighter-rouge">Model</code> conforming to <code class="language-plaintext highlighter-rouge">ObservableObject</code> into our <code class="language-plaintext highlighter-rouge">CollectionView</code> would roughly be implemented as follows:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">ContentView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span> <span class="kd">@ObservedObject</span> <span class="k">var</span> <span class="nv">model</span><span class="p">:</span> <span class="kt">Model</span> <span class="kd">private</span> <span class="kd">func</span> <span class="nf">data</span><span class="p">(</span><span class="n">from</span> <span class="nv">model</span><span class="p">:</span> <span class="kt">Model</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">CollectionData</span> <span class="p">{</span> <span class="c1">// Build data as expected by the CollectionView</span> <span class="p">}</span> <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">CollectionView</span><span class="p">(</span><span class="nv">data</span><span class="p">:</span> <span class="nf">data</span><span class="p">(</span><span class="nv">from</span><span class="p">:</span> <span class="n">model</span><span class="p">))</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>When a model update is published by the observed object the view body is recalculated. An internal method takes care of creating a new data snapshot in some format understood by the <code class="language-plaintext highlighter-rouge">CollectionView</code>, yet to be determined. The SwiftUI layout engine then either creates or updates the underlying <code class="language-plaintext highlighter-rouge">UICollectionView</code> using this new view snapshot.</p> <p>What kind of data format should we use, then? Using <code class="language-plaintext highlighter-rouge">UICollectionViewDiffableDataSource</code> internally is appropriate for the reasons outlined above. Since this type is generic and parametrized with two types, <code class="language-plaintext highlighter-rouge">SectionIdentifierType</code> and <code class="language-plaintext highlighter-rouge">ItemIdentifierType</code> (both required to be <code class="language-plaintext highlighter-rouge">Hashable</code>), our <code class="language-plaintext highlighter-rouge">CollectionView</code> type needs to be generic as well.</p> <p>For compatibility with compositional layouts which display rows of items, it is natural to model the data displayed by a <code class="language-plaintext highlighter-rouge">CollectionView</code> as an array of rows, each row being described by the section it corresponds to and the items it contains:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">CollectionRow</span><span class="o">&lt;</span><span class="kt">Section</span><span class="p">:</span> <span class="kt">Hashable</span><span class="p">,</span> <span class="kt">Item</span><span class="p">:</span> <span class="kt">Hashable</span><span class="o">&gt;</span><span class="p">:</span> <span class="kt">Hashable</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">section</span><span class="p">:</span> <span class="kt">Section</span> <span class="k">let</span> <span class="nv">items</span><span class="p">:</span> <span class="p">[</span><span class="kt">Item</span><span class="p">]</span> <span class="p">}</span> </code></pre></div></div> <p>Since <code class="language-plaintext highlighter-rouge">Section</code> and <code class="language-plaintext highlighter-rouge">Item</code> are both <code class="language-plaintext highlighter-rouge">Hashable</code>, making <code class="language-plaintext highlighter-rouge">CollectionViewRow</code> itself hashable only requires an explicit protocol conformance. Having a row itself hashable is useful to quickly check whether it changed, but more on that later.</p> <p>Since <code class="language-plaintext highlighter-rouge">CollectionRow</code> is a generic type, the collection view itself must at least be parametrized with the same <code class="language-plaintext highlighter-rouge">Section</code> and <code class="language-plaintext highlighter-rouge">Item</code> types:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">CollectionView</span><span class="o">&lt;</span><span class="kt">Section</span><span class="p">:</span> <span class="kt">Hashable</span><span class="p">,</span> <span class="kt">Item</span><span class="p">:</span> <span class="kt">Hashable</span><span class="o">&gt;</span><span class="p">:</span> <span class="kt">UIViewRepresentable</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">rows</span><span class="p">:</span> <span class="p">[</span><span class="kt">CollectionRow</span><span class="o">&lt;</span><span class="kt">Section</span><span class="p">,</span> <span class="kt">Item</span><span class="o">&gt;</span><span class="p">]</span> <span class="c1">// ...</span> <span class="p">}</span> </code></pre></div></div> <p>Types for the generic parameters will be automatically inferred by the Swift compiler when assigning rows to the collection. Still these rows are received by <code class="language-plaintext highlighter-rouge">CollectionView</code> and must be represented by a <code class="language-plaintext highlighter-rouge">UICollectionView</code>. Now is therefore the time to actually implement the diffable data source we need.</p> <h2 id="data-source-and-coordinator">Data Source and Coordinator</h2> <p>A <code class="language-plaintext highlighter-rouge">UICollectionView</code> requires a data source to provide the data it displays. Instantiating a diffable data source containing <code class="language-plaintext highlighter-rouge">Item</code>s in <code class="language-plaintext highlighter-rouge">Section</code>s and displaying them in a <code class="language-plaintext highlighter-rouge">collectionView</code> is simple:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">dataSource</span> <span class="o">=</span> <span class="kt">UICollectionViewDiffableDataSource</span><span class="o">&lt;</span><span class="kt">Section</span><span class="p">,</span> <span class="kt">Item</span><span class="o">&gt;</span><span class="p">(</span><span class="nv">collectionView</span><span class="p">:</span> <span class="n">collectionView</span><span class="p">)</span> <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">item</span> <span class="k">in</span> <span class="c1">// Return UICollectionViewCell for the item</span> <span class="p">}</span> </code></pre></div></div> <p>This data source must be retained, as the collection view only keeps a weak reference to it. But where should we store a strong reference to the data source to keep it alive, since <code class="language-plaintext highlighter-rouge">View</code>s in SwiftUI are merely short-lived snapshots which get destroyed once they have been applied?</p> <p>Fortunately SwiftUI provides an answer in the form of coordinators. The <code class="language-plaintext highlighter-rouge">UIViewRepresentable</code> protocol namely lets you optionally implement a <code class="language-plaintext highlighter-rouge">makeCoordinator()</code> method, from which you can return an instance of a custom type. SwiftUI calls this method before creating the UIKit view from the first time, associates the coordinator with it, and keeps the coordinator alive for as long as the <code class="language-plaintext highlighter-rouge">UIView</code> is in use.</p> <p>We don’t know much about the coordinator type we need yet, except that it must store our data source. After initial creation, this coordinator is provided to the <code class="language-plaintext highlighter-rouge">makeUIView(context:)</code> method through the context parameter as a constant. We must be able to mutate the contained data source reference after the <code class="language-plaintext highlighter-rouge">UICollectionView</code> has been instantiated (the diffable data source namely requires the collection view at initialization time), so our <code class="language-plaintext highlighter-rouge">Coordinator</code> needs to be a <code class="language-plaintext highlighter-rouge">class</code>:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">CollectionView</span><span class="o">&lt;</span><span class="kt">Section</span><span class="p">:</span> <span class="kt">Hashable</span><span class="p">,</span> <span class="kt">Item</span><span class="p">:</span> <span class="kt">Hashable</span><span class="o">&gt;</span><span class="p">:</span> <span class="kt">UIViewRepresentable</span> <span class="p">{</span> <span class="kd">class</span> <span class="kt">Coordinator</span> <span class="p">{</span> <span class="kd">fileprivate</span> <span class="kd">typealias</span> <span class="kt">DataSource</span> <span class="o">=</span> <span class="kt">UICollectionViewDiffableDataSource</span><span class="o">&lt;</span><span class="kt">Section</span><span class="p">,</span> <span class="kt">Item</span><span class="o">&gt;</span> <span class="kd">fileprivate</span> <span class="k">var</span> <span class="nv">dataSource</span><span class="p">:</span> <span class="kt">DataSource</span><span class="p">?</span> <span class="o">=</span> <span class="kc">nil</span> <span class="p">}</span> <span class="c1">// ...</span> <span class="kd">func</span> <span class="nf">makeCoordinator</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">Coordinator</span> <span class="p">{</span> <span class="k">return</span> <span class="kt">Coordinator</span><span class="p">()</span> <span class="p">}</span> <span class="kd">func</span> <span class="nf">makeUIView</span><span class="p">(</span><span class="nv">context</span><span class="p">:</span> <span class="kt">Context</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">UICollectionView</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">collectionView</span> <span class="o">=</span> <span class="kt">UICollectionView</span><span class="p">(</span><span class="nv">frame</span><span class="p">:</span> <span class="o">.</span><span class="n">zero</span><span class="p">,</span> <span class="nv">collectionViewLayout</span><span class="p">:</span> <span class="nf">layout</span><span class="p">())</span> <span class="n">context</span><span class="o">.</span><span class="n">coordinator</span><span class="o">.</span><span class="n">dataSource</span> <span class="o">=</span> <span class="kt">Coordinator</span><span class="o">.</span><span class="kt">DataSource</span><span class="p">(</span><span class="nv">collectionView</span><span class="p">:</span> <span class="n">collectionView</span><span class="p">)</span> <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">item</span> <span class="k">in</span> <span class="c1">// Return UICollectionViewCell for the item</span> <span class="p">}</span> <span class="k">return</span> <span class="n">collectionView</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>Now that we have a data source properly created and retained for the lifetime of the collection view, we can discuss how to fill it with data.</p> <h2 id="data-source-snapshots">Data Source Snapshots</h2> <p>Updating a diffable data source is made in increments. When data changes it suffices to build a new data snapshot and apply it:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">CollectionView</span><span class="o">&lt;</span><span class="kt">Section</span><span class="p">:</span> <span class="kt">Hashable</span><span class="p">,</span> <span class="kt">Item</span><span class="p">:</span> <span class="kt">Hashable</span><span class="o">&gt;</span><span class="p">:</span> <span class="kt">UIViewRepresentable</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="kd">private</span> <span class="kd">func</span> <span class="nf">snapshot</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">NSDiffableDataSourceSnapshot</span><span class="o">&lt;</span><span class="kt">Section</span><span class="p">,</span> <span class="kt">Item</span><span class="o">&gt;</span> <span class="p">{</span> <span class="k">var</span> <span class="nv">snapshot</span> <span class="o">=</span> <span class="kt">NSDiffableDataSourceSnapshot</span><span class="o">&lt;</span><span class="kt">Section</span><span class="p">,</span> <span class="kt">Item</span><span class="o">&gt;</span><span class="p">()</span> <span class="k">for</span> <span class="n">row</span> <span class="k">in</span> <span class="n">rows</span> <span class="p">{</span> <span class="n">snapshot</span><span class="o">.</span><span class="nf">appendSections</span><span class="p">([</span><span class="n">row</span><span class="o">.</span><span class="n">section</span><span class="p">])</span> <span class="n">snapshot</span><span class="o">.</span><span class="nf">appendItems</span><span class="p">(</span><span class="n">row</span><span class="o">.</span><span class="n">items</span><span class="p">,</span> <span class="nv">toSection</span><span class="p">:</span> <span class="n">row</span><span class="o">.</span><span class="n">section</span><span class="p">)</span> <span class="p">}</span> <span class="k">return</span> <span class="n">snapshot</span> <span class="p">}</span> <span class="kd">func</span> <span class="nf">updateUIView</span><span class="p">(</span><span class="n">_</span> <span class="nv">uiView</span><span class="p">:</span> <span class="kt">UICollectionView</span><span class="p">,</span> <span class="nv">context</span><span class="p">:</span> <span class="kt">Context</span><span class="p">)</span> <span class="p">{</span> <span class="k">guard</span> <span class="k">let</span> <span class="nv">dataSource</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">coordinator</span><span class="o">.</span><span class="n">dataSource</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span> <span class="n">dataSource</span><span class="o">.</span><span class="nf">apply</span><span class="p">(</span><span class="nf">snapshot</span><span class="p">(),</span> <span class="nv">animatingDifferences</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>There are two performance issues associated with the above code, though:</p> <ul> <li>The first animated applied snapshot can be slow for a large amount of data.</li> <li>If you run this code on tvOS for a large number of row items and try to navigate the collection, you will discover that performance is really poor. If you try the same code on iOS performance is much better, though.</li> </ul> <p>Since performance issues are what motivated the creation of a custom <code class="language-plaintext highlighter-rouge">CollectionView</code> in the first place, we must fix these identified problems before going any further.</p> <h2 id="solving-performance-issues">Solving Performance Issues</h2> <p>To fix performance issues associated with the first snapshot, it suffices to factor out the corresponding code into a <code class="language-plaintext highlighter-rouge">reloadData(context:animated:)</code> method, called with or without animation depending on whether we are creating or updating the view:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">CollectionView</span><span class="o">&lt;</span><span class="kt">Section</span><span class="p">:</span> <span class="kt">Hashable</span><span class="p">,</span> <span class="kt">Item</span><span class="p">:</span> <span class="kt">Hashable</span><span class="p">,</span> <span class="kt">Cell</span><span class="p">:</span> <span class="kt">View</span><span class="o">&gt;</span><span class="p">:</span> <span class="kt">UIViewRepresentable</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="kd">private</span> <span class="kd">func</span> <span class="nf">reloadData</span><span class="p">(</span><span class="nv">context</span><span class="p">:</span> <span class="kt">Context</span><span class="p">,</span> <span class="nv">animated</span><span class="p">:</span> <span class="kt">Bool</span> <span class="o">=</span> <span class="kc">false</span><span class="p">)</span> <span class="p">{</span> <span class="k">guard</span> <span class="k">let</span> <span class="nv">dataSource</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">coordinator</span><span class="o">.</span><span class="n">dataSource</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span> <span class="n">dataSource</span><span class="o">.</span><span class="nf">apply</span><span class="p">(</span><span class="nf">snapshot</span><span class="p">(),</span> <span class="nv">animatingDifferences</span><span class="p">:</span> <span class="n">animated</span><span class="p">)</span> <span class="p">}</span> <span class="kd">func</span> <span class="nf">makeUIView</span><span class="p">(</span><span class="nv">context</span><span class="p">:</span> <span class="kt">Context</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">UICollectionView</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">collectionView</span> <span class="o">=</span> <span class="kt">UICollectionView</span><span class="p">(</span><span class="nv">frame</span><span class="p">:</span> <span class="o">.</span><span class="n">zero</span><span class="p">,</span> <span class="nv">collectionViewLayout</span><span class="p">:</span> <span class="nf">layout</span><span class="p">())</span> <span class="n">context</span><span class="o">.</span><span class="n">coordinator</span><span class="o">.</span><span class="n">dataSource</span> <span class="o">=</span> <span class="kt">Coordinator</span><span class="o">.</span><span class="kt">DataSource</span><span class="p">(</span><span class="nv">collectionView</span><span class="p">:</span> <span class="n">collectionView</span><span class="p">)</span> <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">item</span> <span class="k">in</span> <span class="c1">// Return UICollectionViewCell for the item</span> <span class="p">}</span> <span class="nf">reloadData</span><span class="p">(</span><span class="nv">context</span><span class="p">:</span> <span class="n">context</span><span class="p">)</span> <span class="k">return</span> <span class="n">collectionView</span> <span class="p">}</span> <span class="kd">func</span> <span class="nf">updateUIView</span><span class="p">(</span><span class="n">_</span> <span class="nv">uiView</span><span class="p">:</span> <span class="kt">UICollectionView</span><span class="p">,</span> <span class="nv">context</span><span class="p">:</span> <span class="kt">Context</span><span class="p">)</span> <span class="p">{</span> <span class="nf">reloadData</span><span class="p">(</span><span class="nv">context</span><span class="p">:</span> <span class="n">context</span><span class="p">,</span> <span class="nv">animated</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>The second performance problem we are facing on tvOS is due to snapshots being applied too many times. Recall that SwiftUI recalculates view bodies when a source of truth changes. Many kinds of changes can therefore trigger <code class="language-plaintext highlighter-rouge">updateUIView(_:context:)</code>, which is why you should attempt to keep its implementation as lightweight as possible in general.</p> <p>In particular <code class="language-plaintext highlighter-rouge">@Environment</code> changes trigger a view update. A specificity of tvOS is that focus updates are provided through the environment. Moving the focus around is therefore sufficient to trigger view updates with each and every move. This is the reason why navigating the collection is a lot heavier on tvOS than on iOS with the implementation above, as snaphots are applied every time the focus is moved.</p> <p>Sadly there is no way to distinguish updates due to data or enviroment changes in <code class="language-plaintext highlighter-rouge">updateUIView(_:context:)</code>. The context does not provide this kind of information, the implementation therefore has no way to know whether an update requires a snapshot to be applied (data change) or not (simple environment change). Is there a way to avoid applying snaphots when the data has not changed?</p> <p>Fortunately there is. As you may remember we made <code class="language-plaintext highlighter-rouge">CollectionViewRow</code> conform to <code class="language-plaintext highlighter-rouge">Hashable</code>. Calculating hashes is much cheaper than creating and applying a snapshot. Each time we apply a snapshot we can therefore store a hash of its <code class="language-plaintext highlighter-rouge">rows</code>, persist this value into our coordinator so that it stays available between updates, and only apply a new snapshot when the hash actually changed:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">CollectionView</span><span class="o">&lt;</span><span class="kt">Section</span><span class="p">:</span> <span class="kt">Hashable</span><span class="p">,</span> <span class="kt">Item</span><span class="p">:</span> <span class="kt">Hashable</span><span class="o">&gt;</span><span class="p">:</span> <span class="kt">UIViewRepresentable</span> <span class="p">{</span> <span class="kd">class</span> <span class="kt">Coordinator</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="kd">fileprivate</span> <span class="k">var</span> <span class="nv">rowsHash</span><span class="p">:</span> <span class="kt">Int</span><span class="p">?</span> <span class="o">=</span> <span class="kc">nil</span> <span class="p">}</span> <span class="c1">// ...</span> <span class="kd">private</span> <span class="kd">func</span> <span class="nf">reloadData</span><span class="p">(</span><span class="nv">context</span><span class="p">:</span> <span class="kt">Context</span><span class="p">,</span> <span class="nv">animated</span><span class="p">:</span> <span class="kt">Bool</span> <span class="o">=</span> <span class="kc">false</span><span class="p">)</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">coordinator</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">coordinator</span> <span class="k">guard</span> <span class="k">let</span> <span class="nv">dataSource</span> <span class="o">=</span> <span class="n">coordinator</span><span class="o">.</span><span class="n">dataSource</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span> <span class="k">let</span> <span class="nv">rowsHash</span> <span class="o">=</span> <span class="n">rows</span><span class="o">.</span><span class="n">hashValue</span> <span class="k">if</span> <span class="n">coordinator</span><span class="o">.</span><span class="n">rowsHash</span> <span class="o">!=</span> <span class="n">rowsHash</span> <span class="p">{</span> <span class="n">dataSource</span><span class="o">.</span><span class="nf">apply</span><span class="p">(</span><span class="nf">snapshot</span><span class="p">(),</span> <span class="nv">animatingDifferences</span><span class="p">:</span> <span class="n">animated</span><span class="p">)</span> <span class="n">coordinator</span><span class="o">.</span><span class="n">rowsHash</span> <span class="o">=</span> <span class="n">rowsHash</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>With this simple trick performance problems on tvOS are eliminated, even with large data sets containing thousands of items. Note that though this <code class="language-plaintext highlighter-rouge">CollectionView</code> implementation inhibits reloads due to environment changes, subviews (e.g. cells we will discuss below) can still individually respond to environment changes if they need to.</p> <p>We now almost have a first working <code class="language-plaintext highlighter-rouge">CollectionView</code> implementation. We only need to be able to configure its layout and cells when creating it.</p> <h2 id="collection-layout-support">Collection Layout Support</h2> <p>At the beginning of this article we instantiated a <code class="language-plaintext highlighter-rouge">UICollectionViewCompositionalLayout</code> but did not provide any meaningful implementation for it:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">CollectionView</span><span class="o">&lt;</span><span class="kt">Section</span><span class="p">:</span> <span class="kt">Hashable</span><span class="p">,</span> <span class="kt">Item</span><span class="p">:</span> <span class="kt">Hashable</span><span class="o">&gt;</span><span class="p">:</span> <span class="kt">UIViewRepresentable</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="kd">private</span> <span class="kd">func</span> <span class="nf">layout</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">UICollectionViewLayout</span> <span class="p">{</span> <span class="k">return</span> <span class="kt">UICollectionViewCompositionalLayout</span> <span class="p">{</span> <span class="n">sectionIndex</span><span class="p">,</span> <span class="n">layoutEnvironment</span> <span class="k">in</span> <span class="c1">// Return section layout</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>How should clients of our <code class="language-plaintext highlighter-rouge">CollectionView</code> provide a layout? We want our collection to behave as a native SwiftUI component, it would therefore be tempting to use <a href="https://www.swiftbysundell.com/articles/deep-dive-into-swift-function-builders">function builders</a> to have a SwiftUI-inspired syntax for layout construction too. But since the current compositional layout API is already declarative and simple, I think it is simpler to just use it as is, rather than introducing a new syntax and the code to support it.</p> <p>Our compositional layout internally requires a section layout provider trailing closure, so this is what our public type interface will let the user customize:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">CollectionView</span><span class="o">&lt;</span><span class="kt">Section</span><span class="p">:</span> <span class="kt">Hashable</span><span class="p">,</span> <span class="kt">Item</span><span class="p">:</span> <span class="kt">Hashable</span><span class="o">&gt;</span><span class="p">:</span> <span class="kt">UIViewRepresentable</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="k">let</span> <span class="nv">rows</span><span class="p">:</span> <span class="p">[</span><span class="kt">CollectionRow</span><span class="o">&lt;</span><span class="kt">Section</span><span class="p">,</span> <span class="kt">Item</span><span class="o">&gt;</span><span class="p">]</span> <span class="k">let</span> <span class="nv">sectionLayoutProvider</span><span class="p">:</span> <span class="p">(</span><span class="kt">Int</span><span class="p">,</span> <span class="kt">NSCollectionLayoutEnvironment</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">NSCollectionLayoutSection</span> <span class="kd">private</span> <span class="kd">func</span> <span class="nf">layout</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">UICollectionViewLayout</span> <span class="p">{</span> <span class="k">return</span> <span class="kt">UICollectionViewCompositionalLayout</span> <span class="p">{</span> <span class="n">sectionIndex</span><span class="p">,</span> <span class="n">layoutEnvironment</span> <span class="k">in</span> <span class="k">return</span> <span class="nf">sectionLayoutProvider</span><span class="p">(</span><span class="n">sectionIndex</span><span class="p">,</span> <span class="n">layoutEnvironment</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>Clients provide a section layout when instantiating a <code class="language-plaintext highlighter-rouge">CollectionView</code>, for example a shelf-like layout similar to the one we previously implemented with SwiftUI stacks and scroll views (see <a href="/swiftui_collection_part1">part 1</a>):</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">CollectionView</span><span class="p">(</span><span class="nv">rows</span><span class="p">:</span> <span class="nf">rows</span><span class="p">())</span> <span class="p">{</span> <span class="n">sectionIndex</span><span class="p">,</span> <span class="n">layoutEnvironment</span> <span class="k">in</span> <span class="k">let</span> <span class="nv">itemSize</span> <span class="o">=</span> <span class="kt">NSCollectionLayoutSize</span><span class="p">(</span><span class="nv">widthDimension</span><span class="p">:</span> <span class="o">.</span><span class="nf">fractionalWidth</span><span class="p">(</span><span class="mi">1</span><span class="p">),</span> <span class="nv">heightDimension</span><span class="p">:</span> <span class="o">.</span><span class="nf">fractionalHeight</span><span class="p">(</span><span class="mi">1</span><span class="p">))</span> <span class="k">let</span> <span class="nv">item</span> <span class="o">=</span> <span class="kt">NSCollectionLayoutItem</span><span class="p">(</span><span class="nv">layoutSize</span><span class="p">:</span> <span class="n">itemSize</span><span class="p">)</span> <span class="k">let</span> <span class="nv">groupSize</span> <span class="o">=</span> <span class="kt">NSCollectionLayoutSize</span><span class="p">(</span><span class="nv">widthDimension</span><span class="p">:</span> <span class="o">.</span><span class="nf">absolute</span><span class="p">(</span><span class="mi">320</span><span class="p">),</span> <span class="nv">heightDimension</span><span class="p">:</span> <span class="o">.</span><span class="nf">absolute</span><span class="p">(</span><span class="mi">180</span><span class="p">))</span> <span class="k">let</span> <span class="nv">group</span> <span class="o">=</span> <span class="kt">NSCollectionLayoutGroup</span><span class="o">.</span><span class="nf">horizontal</span><span class="p">(</span><span class="nv">layoutSize</span><span class="p">:</span> <span class="n">groupSize</span><span class="p">,</span> <span class="nv">subitems</span><span class="p">:</span> <span class="p">[</span><span class="n">item</span><span class="p">])</span> <span class="k">let</span> <span class="nv">section</span> <span class="o">=</span> <span class="kt">NSCollectionLayoutSection</span><span class="p">(</span><span class="nv">group</span><span class="p">:</span> <span class="n">group</span><span class="p">)</span> <span class="n">section</span><span class="o">.</span><span class="n">orthogonalScrollingBehavior</span> <span class="o">=</span> <span class="o">.</span><span class="n">continuous</span> <span class="k">return</span> <span class="n">section</span> <span class="p">}</span> </code></pre></div></div> <p>Refer to the <a href="https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/layouts">official documentation</a> for more information about the compositional layout API. Note that how <code class="language-plaintext highlighter-rouge">rows()</code> are actually provided is here omitted for simplicity, as it does not affect the layout.</p> <p>The layout example above is quite simple. In general each section layout might depend on the data model, though, for example if the layout is received from a web service. In such cases the <code class="language-plaintext highlighter-rouge">sectionLayoutProvider</code> block will capture the initial context and keep it for subsequent layout updates, which is not what we want if the layout returned by the web service later changes. To solve this issue our friend the coordinator again comes to the rescue:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">CollectionView</span><span class="o">&lt;</span><span class="kt">Section</span><span class="p">:</span> <span class="kt">Hashable</span><span class="p">,</span> <span class="kt">Item</span><span class="p">:</span> <span class="kt">Hashable</span><span class="o">&gt;</span><span class="p">:</span> <span class="kt">UIViewRepresentable</span> <span class="p">{</span> <span class="kd">class</span> <span class="kt">Coordinator</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="kd">fileprivate</span> <span class="k">var</span> <span class="nv">sectionLayoutProvider</span><span class="p">:</span> <span class="p">((</span><span class="kt">Int</span><span class="p">,</span> <span class="kt">NSCollectionLayoutEnvironment</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">NSCollectionLayoutSection</span><span class="p">)?</span> <span class="p">}</span> <span class="k">let</span> <span class="nv">rows</span><span class="p">:</span> <span class="p">[</span><span class="kt">CollectionRow</span><span class="o">&lt;</span><span class="kt">Section</span><span class="p">,</span> <span class="kt">Item</span><span class="o">&gt;</span><span class="p">]</span> <span class="k">let</span> <span class="nv">sectionLayoutProvider</span><span class="p">:</span> <span class="p">(</span><span class="kt">Int</span><span class="p">,</span> <span class="kt">NSCollectionLayoutEnvironment</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">NSCollectionLayoutSection</span> <span class="kd">private</span> <span class="kd">func</span> <span class="nf">layout</span><span class="p">(</span><span class="nv">context</span><span class="p">:</span> <span class="kt">Context</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">UICollectionViewLayout</span> <span class="p">{</span> <span class="k">return</span> <span class="kt">UICollectionViewCompositionalLayout</span> <span class="p">{</span> <span class="n">sectionIndex</span><span class="p">,</span> <span class="n">layoutEnvironment</span> <span class="k">in</span> <span class="k">return</span> <span class="n">context</span><span class="o">.</span><span class="n">coordinator</span><span class="o">.</span><span class="nf">sectionLayoutProvider</span><span class="o">!</span><span class="p">(</span><span class="n">sectionIndex</span><span class="p">,</span> <span class="n">layoutEnvironment</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="kd">private</span> <span class="kd">func</span> <span class="nf">reloadData</span><span class="p">(</span><span class="nv">context</span><span class="p">:</span> <span class="kt">Context</span><span class="p">,</span> <span class="nv">animated</span><span class="p">:</span> <span class="kt">Bool</span> <span class="o">=</span> <span class="kc">false</span><span class="p">)</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">coordinator</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">coordinator</span> <span class="n">coordinator</span><span class="o">.</span><span class="n">sectionLayoutProvider</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="n">sectionLayoutProvider</span> <span class="k">guard</span> <span class="k">let</span> <span class="nv">dataSource</span> <span class="o">=</span> <span class="n">coordinator</span><span class="o">.</span><span class="n">dataSource</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span> <span class="k">let</span> <span class="nv">rowsHash</span> <span class="o">=</span> <span class="n">rows</span><span class="o">.</span><span class="n">hashValue</span> <span class="k">if</span> <span class="n">coordinator</span><span class="o">.</span><span class="n">rowsHash</span> <span class="o">!=</span> <span class="n">rowsHash</span> <span class="p">{</span> <span class="n">dataSource</span><span class="o">.</span><span class="nf">apply</span><span class="p">(</span><span class="nf">snapshot</span><span class="p">(),</span> <span class="nv">animatingDifferences</span><span class="p">:</span> <span class="n">animated</span><span class="p">)</span> <span class="n">coordinator</span><span class="o">.</span><span class="n">rowsHash</span> <span class="o">=</span> <span class="n">rowsHash</span> <span class="p">}</span> <span class="p">}</span> <span class="c1">// ...</span> <span class="kd">func</span> <span class="nf">updateUIView</span><span class="p">(</span><span class="n">_</span> <span class="nv">uiView</span><span class="p">:</span> <span class="kt">UICollectionView</span><span class="p">,</span> <span class="nv">context</span><span class="p">:</span> <span class="kt">Context</span><span class="p">)</span> <span class="p">{</span> <span class="nf">reloadData</span><span class="p">(</span><span class="nv">context</span><span class="p">:</span> <span class="n">context</span><span class="p">,</span> <span class="nv">animated</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>This way we ensure the section layout provider is kept up-to-date between view updates so that the layout is always the most recent one, especially if it depends on the data model. Note that the optional <code class="language-plaintext highlighter-rouge">sectionLayoutProvider</code> stored by the <code class="language-plaintext highlighter-rouge">Coordinator</code> can here be safely force unwrapped, as it will always be available by construction. Note we also updated the <code class="language-plaintext highlighter-rouge">layout(context:)</code> method with a <code class="language-plaintext highlighter-rouge">Context</code> parameter to retrieve the coordinator from.</p> <p>With layout definition needs covered let us finish by discussing cell layout and display.</p> <h2 id="cell-layout">Cell Layout</h2> <p>Our <code class="language-plaintext highlighter-rouge">CollectionView</code> first implementation is almost finished, but the cell provider closure of our <code class="language-plaintext highlighter-rouge">UICollectionViewDiffableDataSource</code> is still missing. Recall that we don’t want to layout cells with UIKit code but with SwiftUI declarative syntax. How should we now proceed?</p> <p>SwiftUI syntax is built on top of <a href="https://developer.apple.com/documentation/swiftui/viewbuilder">view builders</a>, actually a special kind of function builder. To be able to associate SwiftUI cells with their <code class="language-plaintext highlighter-rouge">UICollectionViewCell</code> counterparts (which we still need internally for our <code class="language-plaintext highlighter-rouge">UICollectionView</code> implementation), we simply introduce a view builder taking an index path and item as parameters, and returning some SwiftUI view as cell:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">CollectionView</span><span class="o">&lt;</span><span class="kt">Section</span><span class="p">:</span> <span class="kt">Hashable</span><span class="p">,</span> <span class="kt">Item</span><span class="p">:</span> <span class="kt">Hashable</span><span class="p">,</span> <span class="kt">Cell</span><span class="p">:</span> <span class="kt">View</span><span class="o">&gt;</span><span class="p">:</span> <span class="kt">UIViewRepresentable</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="k">let</span> <span class="nv">rows</span><span class="p">:</span> <span class="p">[</span><span class="kt">CollectionRow</span><span class="o">&lt;</span><span class="kt">Section</span><span class="p">,</span> <span class="kt">Item</span><span class="o">&gt;</span><span class="p">]</span> <span class="k">let</span> <span class="nv">sectionLayoutProvider</span><span class="p">:</span> <span class="p">(</span><span class="kt">Int</span><span class="p">,</span> <span class="kt">NSCollectionLayoutEnvironment</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">NSCollectionLayoutSection</span> <span class="k">let</span> <span class="nv">cell</span><span class="p">:</span> <span class="p">(</span><span class="kt">IndexPath</span><span class="p">,</span> <span class="kt">Item</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Cell</span> <span class="nf">init</span><span class="p">(</span><span class="nv">rows</span><span class="p">:</span> <span class="p">[</span><span class="kt">CollectionRow</span><span class="o">&lt;</span><span class="kt">Section</span><span class="p">,</span> <span class="kt">Item</span><span class="o">&gt;</span><span class="p">],</span> <span class="nv">sectionLayoutProvider</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="kt">Int</span><span class="p">,</span> <span class="kt">NSCollectionLayoutEnvironment</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">NSCollectionLayoutSection</span><span class="p">,</span> <span class="kd">@ViewBuilder</span> <span class="nv">cell</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="kt">IndexPath</span><span class="p">,</span> <span class="kt">Item</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Cell</span><span class="p">)</span> <span class="p">{</span> <span class="k">self</span><span class="o">.</span><span class="n">rows</span> <span class="o">=</span> <span class="n">rows</span> <span class="k">self</span><span class="o">.</span><span class="n">sectionLayoutProvider</span> <span class="o">=</span> <span class="n">sectionLayoutProvider</span> <span class="k">self</span><span class="o">.</span><span class="n">cell</span> <span class="o">=</span> <span class="n">cell</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>The <code class="language-plaintext highlighter-rouge">@ViewBuilder</code> syntax requires a dedicated initializer, as <code class="language-plaintext highlighter-rouge">@ViewBuilder</code> can only appear as a parameter-attribute. Note that Swift infers the type of the cell body based on the view builder block, forcing us to add <code class="language-plaintext highlighter-rouge">Cell</code> as additional parameter of the <code class="language-plaintext highlighter-rouge">CollectionView</code> generic type list.</p> <h2 id="cell-display">Cell Display</h2> <p>Cells whose layout is defined with SwiftUI code are ultimately displayed by a <code class="language-plaintext highlighter-rouge">UICollectionView</code> and thus must be wrapped for display by UIKit. This is achieved by using <code class="language-plaintext highlighter-rouge">UIHostingController</code> and a simple host <code class="language-plaintext highlighter-rouge">UICollectionViewCell</code>:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">CollectionView</span><span class="o">&lt;</span><span class="kt">Section</span><span class="p">:</span> <span class="kt">Hashable</span><span class="p">,</span> <span class="kt">Item</span><span class="p">:</span> <span class="kt">Hashable</span><span class="p">,</span> <span class="kt">Cell</span><span class="p">:</span> <span class="kt">View</span><span class="o">&gt;</span><span class="p">:</span> <span class="kt">UIViewRepresentable</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="kd">private</span> <span class="kd">class</span> <span class="kt">HostCell</span><span class="p">:</span> <span class="kt">UICollectionViewCell</span> <span class="p">{</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">hostController</span><span class="p">:</span> <span class="kt">UIHostingController</span><span class="o">&lt;</span><span class="kt">Cell</span><span class="o">&gt;</span><span class="p">?</span> <span class="k">override</span> <span class="kd">func</span> <span class="nf">prepareForReuse</span><span class="p">()</span> <span class="p">{</span> <span class="k">if</span> <span class="k">let</span> <span class="nv">hostView</span> <span class="o">=</span> <span class="n">hostController</span><span class="p">?</span><span class="o">.</span><span class="n">view</span> <span class="p">{</span> <span class="n">hostView</span><span class="o">.</span><span class="nf">removeFromSuperview</span><span class="p">()</span> <span class="p">}</span> <span class="n">hostController</span> <span class="o">=</span> <span class="kc">nil</span> <span class="p">}</span> <span class="k">var</span> <span class="nv">hostedCell</span><span class="p">:</span> <span class="kt">Cell</span><span class="p">?</span> <span class="p">{</span> <span class="k">willSet</span> <span class="p">{</span> <span class="k">guard</span> <span class="k">let</span> <span class="nv">view</span> <span class="o">=</span> <span class="n">newValue</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span> <span class="n">hostController</span> <span class="o">=</span> <span class="kt">UIHostingController</span><span class="p">(</span><span class="nv">rootView</span><span class="p">:</span> <span class="n">view</span><span class="p">)</span> <span class="k">if</span> <span class="k">let</span> <span class="nv">hostView</span> <span class="o">=</span> <span class="n">hostController</span><span class="p">?</span><span class="o">.</span><span class="n">view</span> <span class="p">{</span> <span class="n">hostView</span><span class="o">.</span><span class="n">frame</span> <span class="o">=</span> <span class="n">contentView</span><span class="o">.</span><span class="n">bounds</span> <span class="n">hostView</span><span class="o">.</span><span class="n">autoresizingMask</span> <span class="o">=</span> <span class="p">[</span><span class="o">.</span><span class="n">flexibleWidth</span><span class="p">,</span> <span class="o">.</span><span class="n">flexibleHeight</span><span class="p">]</span> <span class="n">contentView</span><span class="o">.</span><span class="nf">addSubview</span><span class="p">(</span><span class="n">hostView</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>The implementation of this cell is straightforward:</p> <ul> <li>We prepare a <code class="language-plaintext highlighter-rouge">UIHostingController</code> when setting a SwiftUI cell. The hosting controller view is installed to fill the entire cell content view. Such embedding is actually made possible by the fact that creating a <code class="language-plaintext highlighter-rouge">UIHostingController</code> is actually quite cheap.</li> <li>When a cell is reused, the hosting controller view is removed and the controller itself is released.</li> </ul> <p>Now that we have a cell, it suffices to register it and return dequeued instances from our data source, assigning it the corresponding SwiftUI cell:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">CollectionView</span><span class="o">&lt;</span><span class="kt">Section</span><span class="p">:</span> <span class="kt">Hashable</span><span class="p">,</span> <span class="kt">Item</span><span class="p">:</span> <span class="kt">Hashable</span><span class="o">&gt;</span><span class="p">:</span> <span class="kt">UIViewRepresentable</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="kd">func</span> <span class="nf">makeUIView</span><span class="p">(</span><span class="nv">context</span><span class="p">:</span> <span class="kt">Context</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">UICollectionView</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">cellIdentifier</span> <span class="o">=</span> <span class="s">"hostCell"</span> <span class="k">let</span> <span class="nv">collectionView</span> <span class="o">=</span> <span class="kt">UICollectionView</span><span class="p">(</span><span class="nv">frame</span><span class="p">:</span> <span class="o">.</span><span class="n">zero</span><span class="p">,</span> <span class="nv">collectionViewLayout</span><span class="p">:</span> <span class="nf">layout</span><span class="p">(</span><span class="nv">context</span><span class="p">:</span> <span class="n">context</span><span class="p">))</span> <span class="n">collectionView</span><span class="o">.</span><span class="nf">register</span><span class="p">(</span><span class="kt">HostCell</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forCellWithReuseIdentifier</span><span class="p">:</span> <span class="n">cellIdentifier</span><span class="p">)</span> <span class="n">context</span><span class="o">.</span><span class="n">coordinator</span><span class="o">.</span><span class="n">dataSource</span> <span class="o">=</span> <span class="kt">Coordinator</span><span class="o">.</span><span class="kt">DataSource</span><span class="p">(</span><span class="nv">collectionView</span><span class="p">:</span> <span class="n">collectionView</span><span class="p">)</span> <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">item</span> <span class="k">in</span> <span class="k">let</span> <span class="nv">hostCell</span> <span class="o">=</span> <span class="n">collectionView</span><span class="o">.</span><span class="nf">dequeueReusableCell</span><span class="p">(</span><span class="nv">withReuseIdentifier</span><span class="p">:</span> <span class="n">cellIdentifier</span><span class="p">,</span> <span class="nv">for</span><span class="p">:</span> <span class="n">indexPath</span><span class="p">)</span> <span class="k">as?</span> <span class="kt">HostCell</span> <span class="n">hostCell</span><span class="p">?</span><span class="o">.</span><span class="n">hostedCell</span> <span class="o">=</span> <span class="nf">cell</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="k">return</span> <span class="n">hostCell</span> <span class="p">}</span> <span class="nf">reloadData</span><span class="p">(</span><span class="nv">context</span><span class="p">:</span> <span class="n">context</span><span class="p">)</span> <span class="k">return</span> <span class="n">collectionView</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>Note that, unlike cells defined in UIKit, there is no need for clients of our <code class="language-plaintext highlighter-rouge">CollectionView</code> to mess with cell class registrations and reuse identifiers. Only one cell identifier is required and managed internally. All cells provided in SwiftUI namely share the same <code class="language-plaintext highlighter-rouge">Cell</code> generic type parameter, whose actual value is filled in by the Swift compiler based on what is inside the view builder closure.</p> <h3 class="no_toc" id="remark">Remark</h3> <p>iOS and tvOS 14 provide a <a href="https://developer.apple.com/documentation/uikit/uicollectionview/cellregistration">new <code class="language-plaintext highlighter-rouge">CellRegistration</code> API</a> but, to keep things simple, we still use the old cell registration and dequeuing API, so that the implementation is compatible with iOS and tvOS 13 without the use of availability macros. The iOS and tvOS 14 modern implementation is left as an exercise for the reader.</p> <h2 id="example-of-a-grid-layout">Example of a Grid Layout</h2> <p>Bringing everything together, we now have a first working implementation of a <code class="language-plaintext highlighter-rouge">CollectionView</code> in SwiftUI, powered by <code class="language-plaintext highlighter-rouge">UICollectionView</code>. The complete source code will be provided <a href="/swiftui_collection_part3">in the third and last article in this series</a>, but let us have a look at how a shelf-like layout like the one we discussed in <a href="/swiftui_collection_part1">part 1</a> is implemented with the API we have defined throughout this article, on tvOS:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">SwiftUI</span> <span class="kd">struct</span> <span class="kt">Shelf</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span> <span class="kd">typealias</span> <span class="kt">Row</span> <span class="o">=</span> <span class="kt">CollectionRow</span><span class="o">&lt;</span><span class="kt">Int</span><span class="p">,</span> <span class="kt">String</span><span class="o">&gt;</span> <span class="k">var</span> <span class="nv">rows</span><span class="p">:</span> <span class="p">[</span><span class="kt">Row</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span> <span class="k">var</span> <span class="nv">rows</span> <span class="o">=</span> <span class="p">[</span><span class="kt">Row</span><span class="p">]()</span> <span class="k">for</span> <span class="n">i</span> <span class="k">in</span> <span class="mi">0</span><span class="o">..&lt;</span><span class="mi">20</span> <span class="p">{</span> <span class="n">rows</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="kt">Row</span><span class="p">(</span><span class="nv">section</span><span class="p">:</span> <span class="n">i</span><span class="p">,</span> <span class="nv">items</span><span class="p">:</span> <span class="p">(</span><span class="mi">0</span><span class="o">..&lt;</span><span class="mi">10</span><span class="p">)</span><span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="s">"</span><span class="se">\(</span><span class="n">i</span><span class="se">)</span><span class="s">, </span><span class="se">\(</span><span class="nv">$0</span><span class="se">)</span><span class="s">"</span> <span class="p">}))</span> <span class="p">}</span> <span class="k">return</span> <span class="n">rows</span> <span class="p">}()</span> <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">CollectionView</span><span class="p">(</span><span class="nv">rows</span><span class="p">:</span> <span class="n">rows</span><span class="p">)</span> <span class="p">{</span> <span class="n">sectionIndex</span><span class="p">,</span> <span class="n">layoutEnvironment</span> <span class="k">in</span> <span class="k">let</span> <span class="nv">itemSize</span> <span class="o">=</span> <span class="kt">NSCollectionLayoutSize</span><span class="p">(</span><span class="nv">widthDimension</span><span class="p">:</span> <span class="o">.</span><span class="nf">fractionalWidth</span><span class="p">(</span><span class="mi">1</span><span class="p">),</span> <span class="nv">heightDimension</span><span class="p">:</span> <span class="o">.</span><span class="nf">fractionalHeight</span><span class="p">(</span><span class="mi">1</span><span class="p">))</span> <span class="k">let</span> <span class="nv">item</span> <span class="o">=</span> <span class="kt">NSCollectionLayoutItem</span><span class="p">(</span><span class="nv">layoutSize</span><span class="p">:</span> <span class="n">itemSize</span><span class="p">)</span> <span class="k">let</span> <span class="nv">groupSize</span> <span class="o">=</span> <span class="kt">NSCollectionLayoutSize</span><span class="p">(</span><span class="nv">widthDimension</span><span class="p">:</span> <span class="o">.</span><span class="nf">absolute</span><span class="p">(</span><span class="mi">320</span><span class="p">),</span> <span class="nv">heightDimension</span><span class="p">:</span> <span class="o">.</span><span class="nf">absolute</span><span class="p">(</span><span class="mi">180</span><span class="p">))</span> <span class="k">let</span> <span class="nv">group</span> <span class="o">=</span> <span class="kt">NSCollectionLayoutGroup</span><span class="o">.</span><span class="nf">horizontal</span><span class="p">(</span><span class="nv">layoutSize</span><span class="p">:</span> <span class="n">groupSize</span><span class="p">,</span> <span class="nv">subitems</span><span class="p">:</span> <span class="p">[</span><span class="n">item</span><span class="p">])</span> <span class="k">let</span> <span class="nv">section</span> <span class="o">=</span> <span class="kt">NSCollectionLayoutSection</span><span class="p">(</span><span class="nv">group</span><span class="p">:</span> <span class="n">group</span><span class="p">)</span> <span class="n">section</span><span class="o">.</span><span class="n">contentInsets</span> <span class="o">=</span> <span class="kt">NSDirectionalEdgeInsets</span><span class="p">(</span><span class="nv">top</span><span class="p">:</span> <span class="mi">20</span><span class="p">,</span> <span class="nv">leading</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="nv">bottom</span><span class="p">:</span> <span class="mi">20</span><span class="p">,</span> <span class="nv">trailing</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span> <span class="n">section</span><span class="o">.</span><span class="n">interGroupSpacing</span> <span class="o">=</span> <span class="mi">40</span> <span class="n">section</span><span class="o">.</span><span class="n">orthogonalScrollingBehavior</span> <span class="o">=</span> <span class="o">.</span><span class="n">continuous</span> <span class="k">return</span> <span class="n">section</span> <span class="p">}</span> <span class="nv">cell</span><span class="p">:</span> <span class="p">{</span> <span class="n">indexPath</span><span class="p">,</span> <span class="n">item</span> <span class="k">in</span> <span class="kt">GeometryReader</span> <span class="p">{</span> <span class="n">geometry</span> <span class="k">in</span> <span class="kt">Button</span><span class="p">(</span><span class="nv">action</span><span class="p">:</span> <span class="p">{})</span> <span class="p">{</span> <span class="kt">Text</span><span class="p">(</span><span class="n">item</span><span class="p">)</span> <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="n">geometry</span><span class="o">.</span><span class="n">size</span><span class="o">.</span><span class="n">width</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="n">geometry</span><span class="o">.</span><span class="n">size</span><span class="o">.</span><span class="n">height</span><span class="p">)</span> <span class="o">.</span><span class="nf">background</span><span class="p">(</span><span class="kt">Color</span><span class="o">.</span><span class="n">blue</span><span class="p">)</span> <span class="p">}</span> <span class="o">.</span><span class="nf">buttonStyle</span><span class="p">(</span><span class="kt">CardButtonStyle</span><span class="p">())</span> <span class="p">}</span> <span class="p">}</span> <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxWidth</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">,</span> <span class="nv">maxHeight</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">)</span> <span class="o">.</span><span class="nf">ignoresSafeArea</span><span class="p">(</span><span class="o">.</span><span class="n">all</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>The formalism is quite elegant and compact but, when actually running the code above, we find a few more issues:</p> <ul> <li>When scrolling a shelf horizontally, cells which appear from the edges are not properly sized.</li> <li>Buttons do not exhibit the <code class="language-plaintext highlighter-rouge">CardButtonStyle</code> appearance when focused.</li> </ul> <p><img src="/images/first_swiftui_collection.jpg" alt="SwiftUI CollectionView" /></p> <h2 id="wrapping-up">Wrapping Up</h2> <p>In this lengthy article we have implemented a working collection view for SwiftUI, based on <code class="language-plaintext highlighter-rouge">UICollectionView</code>. We have designed an API to layout the collection itself as well as its individual cells. We also have ensured performance is on par with what we expect from a usual <code class="language-plaintext highlighter-rouge">UIKit</code> collection view, thus addressing our initial problem.</p> <p>The end result, while certainly promising, still suffers from a few issues we will solve in the next and final article in this series.</p> <p>Read next: <a href="/swiftui_collection_part3">Part 3: Fixes and Focus Management</a></p>Faced with the inability to reach an acceptable level of performance with grid layouts made of simple SwiftUI building blocks, I decided to roll my own solution.Building a Collection For SwiftUI (Part 1) - Mimicking Collections in SwiftUI2020-09-13T00:00:00+00:002020-09-13T00:00:00+00:00/swiftui_collection_part1<p>SwiftUI does not offer any full-featured collection component like UIKit does, merely smaller building blocks like stacks, lists and scroll views (as well as lazy stacks and grids newly introduced in iOS and tvOS 14). In isolation none of these components is a true competitor to <code class="language-plaintext highlighter-rouge">UICollectionView</code> but, combined together, they can be used to build pretty advanced grid and table layouts.</p> <p>In fact, combining these basic components to achieve grid and table layouts in SwiftUI is incredibly simple, with a clean and beautiful formalism. And with the lazy components added this year, there hasn’t apparently been any better time to fully embrace SwiftUI, has there?</p> <p><em>This article is part 1 in the <a href="/swiftui_collection_intro">Building a Collection For SwiftUI</a> series</em>.</p> <ul id="markdown-toc"> <li><a href="#basic-grid-layouts-in-swiftui" id="markdown-toc-basic-grid-layouts-in-swiftui">Basic Grid Layouts in SwiftUI</a></li> <li><a href="#better-swiftui-grid-layouts-with-lazy-stacks" id="markdown-toc-better-swiftui-grid-layouts-with-lazy-stacks">Better SwiftUI Grid Layouts With Lazy Stacks</a></li> <li><a href="#the-problem-with-swiftui-stack-based-grid-layouts" id="markdown-toc-the-problem-with-swiftui-stack-based-grid-layouts">The Problem with SwiftUI Stack-based Grid Layouts</a></li> <li><a href="#wrapping-up" id="markdown-toc-wrapping-up">Wrapping Up</a></li> </ul> <h2 id="basic-grid-layouts-in-swiftui">Basic Grid Layouts in SwiftUI</h2> <p>Think about the TV, App Store or Netflix apps. They present their content in horizontally scrollable shelves, a layout fairly common among iOS and tvOS apps.</p> <p>Creating such a layout in SwiftUI is as simple as nesting a few stacks and scroll views:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">Cell</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">row</span><span class="p">:</span> <span class="kt">Int</span> <span class="k">let</span> <span class="nv">column</span><span class="p">:</span> <span class="kt">Int</span> <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">Text</span><span class="p">(</span><span class="s">"</span><span class="se">\(</span><span class="n">row</span><span class="se">)</span><span class="s">, </span><span class="se">\(</span><span class="n">column</span><span class="se">)</span><span class="s">"</span><span class="p">)</span> <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="mi">320</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="mi">180</span><span class="p">)</span> <span class="o">.</span><span class="nf">background</span><span class="p">(</span><span class="kt">Color</span><span class="o">.</span><span class="n">blue</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="kd">struct</span> <span class="kt">Row</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">index</span><span class="p">:</span> <span class="kt">Int</span> <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">ScrollView</span><span class="p">(</span><span class="o">.</span><span class="n">horizontal</span><span class="p">)</span> <span class="p">{</span> <span class="kt">HStack</span> <span class="p">{</span> <span class="kt">ForEach</span><span class="p">(</span><span class="mi">0</span><span class="o">..&lt;</span><span class="mi">10</span><span class="p">)</span> <span class="p">{</span> <span class="n">i</span> <span class="k">in</span> <span class="kt">Cell</span><span class="p">(</span><span class="nv">row</span><span class="p">:</span> <span class="n">index</span><span class="p">,</span> <span class="nv">column</span><span class="p">:</span> <span class="n">i</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="kd">struct</span> <span class="kt">Grid</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span> <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">ScrollView</span> <span class="p">{</span> <span class="kt">VStack</span> <span class="p">{</span> <span class="kt">ForEach</span><span class="p">(</span><span class="mi">0</span><span class="o">..&lt;</span><span class="mi">20</span><span class="p">)</span> <span class="p">{</span> <span class="n">i</span> <span class="k">in</span> <span class="kt">Row</span><span class="p">(</span><span class="nv">index</span><span class="p">:</span> <span class="n">i</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p><img src="/images/collection_stack_grid.jpg" alt="Stack grid" /></p> <p>Achieving a similar result with UIKit would have usually required a <code class="language-plaintext highlighter-rouge">UICollectionView</code> with a compositional layout made of horizontally scrollable sections.</p> <p>Compositional layouts are only available since iOS and tvOS 13, though. On earlier versions you would rather have used a main vertical collection or table, containing as many nested horizontal collections as needed for rows. Not to mention that in this case horizontal content offsets need to be saved and restored as the main vertical collection or table is scrolled and cells are reused. All these behaviors are provided for free with the above SwiftUI layout code, which is quite amazing.</p> <p>The real downside of the above SwiftUI implementation is that, unlike <code class="language-plaintext highlighter-rouge">UICollectionView</code>, all view bodies are loaded initially, as is appearant when you profile the code with Instruments:</p> <p><img src="/images/collection_instruments_stack.jpg" alt="Instruments stack" /></p> <p>Surely this is not optimal from a performance and memory consumption point of view, and fortunately Apple’s engineers probably thought the same.</p> <h2 id="better-swiftui-grid-layouts-with-lazy-stacks">Better SwiftUI Grid Layouts With Lazy Stacks</h2> <p>This year SwiftUI introduces lazy variants of stacks in iOS and tvOS 14. Seems like magic when you watch <a href="https://developer.apple.com/wwdc20/10031">WWDC 2020 10031</a> where they are presented in action, but you still have to be somewhat careful about which stacks you promote to laziness.</p> <p>In our case only the outermost stack should be made lazy so that each row height can be properly calculated:</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">Cell</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">row</span><span class="p">:</span> <span class="kt">Int</span> <span class="k">let</span> <span class="nv">column</span><span class="p">:</span> <span class="kt">Int</span> <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">Text</span><span class="p">(</span><span class="s">"</span><span class="se">\(</span><span class="n">row</span><span class="se">)</span><span class="s">, </span><span class="se">\(</span><span class="n">column</span><span class="se">)</span><span class="s">"</span><span class="p">)</span> <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="mi">320</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="mi">180</span><span class="p">)</span> <span class="o">.</span><span class="nf">background</span><span class="p">(</span><span class="kt">Color</span><span class="o">.</span><span class="n">blue</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="kd">struct</span> <span class="kt">Row</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">index</span><span class="p">:</span> <span class="kt">Int</span> <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">ScrollView</span><span class="p">(</span><span class="o">.</span><span class="n">horizontal</span><span class="p">)</span> <span class="p">{</span> <span class="kt">HStack</span> <span class="p">{</span> <span class="kt">ForEach</span><span class="p">(</span><span class="mi">0</span><span class="o">..&lt;</span><span class="mi">10</span><span class="p">)</span> <span class="p">{</span> <span class="n">i</span> <span class="k">in</span> <span class="kt">Cell</span><span class="p">(</span><span class="nv">row</span><span class="p">:</span> <span class="n">index</span><span class="p">,</span> <span class="nv">column</span><span class="p">:</span> <span class="n">i</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="kd">struct</span> <span class="kt">Grid</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span> <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">ScrollView</span> <span class="p">{</span> <span class="kt">LazyVStack</span> <span class="p">{</span> <span class="kt">ForEach</span><span class="p">(</span><span class="mi">0</span><span class="o">..&lt;</span><span class="mi">20</span><span class="p">)</span> <span class="p">{</span> <span class="n">i</span> <span class="k">in</span> <span class="kt">Row</span><span class="p">(</span><span class="nv">index</span><span class="p">:</span> <span class="n">i</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>When profiled with Instruments we clearly see an improvement:</p> <p><img src="/images/collection_instruments_lazy_stack.jpg" alt="Instruments stack" /></p> <p>Pretty nice until now, isn’t it?</p> <h3 class="no_toc" id="remark">Remark</h3> <p>Though iOS and tvOS 14 also introduce lazy grids with similar behavior, those are not suited for our shelf-based layout as they currently do not support independently scrollable rows.</p> <h2 id="the-problem-with-swiftui-stack-based-grid-layouts">The Problem with SwiftUI Stack-based Grid Layouts</h2> <p>What is not immediately apparant with the code above is that, while you can easily navigate this grid on iOS by swiping the screen, you cannot do the same on tvOS. There is namely no <a href="https://developer.apple.com/design/human-interface-guidelines/tvos/app-architecture/focus-and-selection">focusable</a> item in the layout code above, therefore no way to navigate the collection on Apple TV.</p> <p>Fortunately it is very easy to make cells focusable by turning them into buttons. We can even have standard look and feel with the new tvOS 14 <a href="https://developer.apple.com/documentation/swiftui/cardbuttonstyle">card button style</a>, which makes focused buttons pop out with a large shadow underneath and tilting support.</p> <p>Since focus makes the button larger and adds a large shadow to it, I tweaked the margins to let the content shine when focused, but otherwise the code is identical to the one above, except for an added button wrapper in cells. Note that this code only runs on tvOS 14, as the button style is only available there (it is quite easy to make the code compatible with iOS 13 and tvOS 13, but this is left as an exercise for the reader):</p> <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">Cell</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">row</span><span class="p">:</span> <span class="kt">Int</span> <span class="k">let</span> <span class="nv">column</span><span class="p">:</span> <span class="kt">Int</span> <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">Button</span><span class="p">(</span><span class="nv">action</span><span class="p">:</span> <span class="p">{})</span> <span class="p">{</span> <span class="kt">Text</span><span class="p">(</span><span class="s">"</span><span class="se">\(</span><span class="n">row</span><span class="se">)</span><span class="s">, </span><span class="se">\(</span><span class="n">column</span><span class="se">)</span><span class="s">"</span><span class="p">)</span> <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="mi">320</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="mi">180</span><span class="p">)</span> <span class="o">.</span><span class="nf">background</span><span class="p">(</span><span class="kt">Color</span><span class="o">.</span><span class="n">blue</span><span class="p">)</span> <span class="p">}</span> <span class="o">.</span><span class="nf">buttonStyle</span><span class="p">(</span><span class="kt">CardButtonStyle</span><span class="p">())</span> <span class="p">}</span> <span class="p">}</span> <span class="kd">struct</span> <span class="kt">Row</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span> <span class="k">let</span> <span class="nv">index</span><span class="p">:</span> <span class="kt">Int</span> <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">ScrollView</span><span class="p">(</span><span class="o">.</span><span class="n">horizontal</span><span class="p">)</span> <span class="p">{</span> <span class="kt">HStack</span> <span class="p">{</span> <span class="kt">ForEach</span><span class="p">(</span><span class="mi">0</span><span class="o">..&lt;</span><span class="mi">10</span><span class="p">)</span> <span class="p">{</span> <span class="n">i</span> <span class="k">in</span> <span class="kt">Cell</span><span class="p">(</span><span class="nv">row</span><span class="p">:</span> <span class="n">index</span><span class="p">,</span> <span class="nv">column</span><span class="p">:</span> <span class="n">i</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="o">.</span><span class="nf">padding</span><span class="p">([</span><span class="o">.</span><span class="n">leading</span><span class="p">,</span> <span class="o">.</span><span class="n">trailing</span><span class="p">],</span> <span class="mi">40</span><span class="p">)</span> <span class="o">.</span><span class="nf">padding</span><span class="p">(</span><span class="o">.</span><span class="n">top</span><span class="p">,</span> <span class="mi">20</span><span class="p">)</span> <span class="o">.</span><span class="nf">padding</span><span class="p">(</span><span class="o">.</span><span class="n">bottom</span><span class="p">,</span> <span class="mi">80</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="kd">struct</span> <span class="kt">Grid</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span> <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span> <span class="kt">ScrollView</span> <span class="p">{</span> <span class="kt">LazyVStack</span> <span class="p">{</span> <span class="kt">ForEach</span><span class="p">(</span><span class="mi">0</span><span class="o">..&lt;</span><span class="mi">20</span><span class="p">)</span> <span class="p">{</span> <span class="n">i</span> <span class="k">in</span> <span class="kt">Row</span><span class="p">(</span><span class="nv">index</span><span class="p">:</span> <span class="n">i</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>We can now navigate the collection and enjoy the native tvOS behavior we expect according to the <a href="https://developer.apple.com/design/human-interface-guidelines/tvos/overview/focus-and-parallax">Human Interface Guidelines</a>:</p> <p><img src="/images/collection_shelf_focus.jpg" alt="Lazy stack grid" /></p> <p><code class="language-plaintext highlighter-rouge">UICollectionView</code> excels at ensuring smooth scrolling for large data sets by reusing cells as the view is scrolled. Stacks initially introduced by SwiftUI load all content at the same time, but surely lazy variants added this year are supposed to address this issue, aren’t they?</p> <p>To verify this assumption it suffices to tweak the code above and to load 20 rows of 50 items instead. If you attempt to run the result on an Apple TV device you will clearly experience severe issues. Scrolling performance namely degrades fast with increasing number of items and the user experience becomes horrendous, even with the basic cells we display. You can also notice that lazy stacks do not work well on tvOS, as they force the focus to move one item at a time near collection boundaries.</p> <h3 class="no_toc" id="remark-1">Remark</h3> <p>If you run the code above on iOS (without the card button style which is available for tvOS only), the experience is better overall:</p> <ul> <li>Scrolling is smoother, which does not mean that it is anywhere the performance of a <code class="language-plaintext highlighter-rouge">UICollectionView</code> displaying the same number of items.</li> <li>It is possible to move more than one item at a time near view boundaries since only swipes are involved for navigation.</li> </ul> <p>Without having the time to dig into what actually makes things worse on tvOS, I conjectured these issues are related to tvOS focus changes, which trigger <code class="language-plaintext highlighter-rouge">@Environment</code> updates probably leading to additional layout work. This is something we will discuss again in <a href="/swiftui_collection_part2">part 2 of this article series</a> but, if I am correct, I think this problem can likely be solved in a future SwiftUI release. Still we must find a solution in the meantime.</p> <h2 id="wrapping-up">Wrapping Up</h2> <p>SwiftUI grid-based layouts made of nested stacks and scroll views currently suffer from major performance issues. On tvOS these performance issues are so significant that attempting to load a few hundred items leads to a poor user experience. Loading thousands of items can bring an Apple TV to its knees.</p> <p><a href="http://openradar.appspot.com/radar?id=4957846227648512">I reported this problem to Apple during the iOS and tvOS 14 beta phase</a>, knowing it would likely not be fixed in the official releases, and considered the available options:</p> <ul> <li>Blissful optimism: Do nothing, continue to work with nested stack and scroll views, and hope that later iOS and tvOS releases fix the issue. In the meantime put an upper bound on the amount of content we display in grids, a couple hundred items at most.</li> <li>Complete pessimism: Consider SwiftUI is not mature enough and use UIKit.</li> <li>Compromise: Use SwiftUI where possible but find a way to solve collection performance issues.</li> </ul> <p>Having tasted how great working and thinking in SwiftUI can be, the mere idea of dropping it entirely was disappointing, especially knowing it can be integrated with UIKit fairly easily. Putting upper bounds to the amount of items displayed was not acceptable either. I therefore decided to go with the compromise and roll my own collection, knowing I would probably learn a lot along the way.</p> <p>Read next: <a href="/swiftui_collection_part2">Part 2: SwiftUI Collection Implementation</a></p>SwiftUI does not offer any full-featured collection component like UIKit does, merely smaller building blocks like stacks, lists and scroll views (as well as lazy stacks and grids newly introduced in iOS and tvOS 14). In isolation none of these components is a true competitor to UICollectionView but, combined together, they can be used to build pretty advanced grid and table layouts.Building a Collection For SwiftUI - Introduction2020-09-12T00:00:00+00:002020-09-12T00:00:00+00:00/swiftui_collection_intro<p><code class="language-plaintext highlighter-rouge">UICollectionView</code> has been an essential tool for Apple app developers since its introduction in iOS 6. Over the years it received a range of improvements and was ported to tvOS so that you can build layouts with the same formalism on both platforms. In iOS and tvOS 13 <code class="language-plaintext highlighter-rouge">UICollectionView</code> received the most significant enhancements ever made since its introduction, namely diffable data sources and compositional layouts.</p> <p>Last year Apple introduced SwiftUI, its declarative UI framework. Now in its second iteration with iOS and tvOS 14, SwiftUI is obviously still lagging behind in terms of functionality. While SwiftUI provides lists, scrollable views and grids as of version 14, it still does not provide a collection which could compete with <code class="language-plaintext highlighter-rouge">UICollectionView</code> in terms of feature set or versatility. This does not mean that grid and table layouts usually achieved with <code class="language-plaintext highlighter-rouge">UICollectionView</code> are not possible with SwiftUI. The smaller components already available can be easily combined to create complex scrollable layouts, so that what can be achieved in UIKit can be more or less achieved in SwiftUI as well.</p> <p>When evaluating SwiftUI as a viable option for porting our <a href="https://github.com/SRGSSR/playsrg-apple">iOS apps</a> to tvOS, one of the essential requirements to satisfy was to ensure grid and table layouts, used throughout our implementation, would be easy to create in SwiftUI, with satisfying user experience. I initially and naively thought so, but I did not quite imagine how wrong I was.</p> <p>I thought sharing my experience could probably be helpful to other developers who might be considering SwiftUI for their own apps as well. Since the material to be covered is larger than initially expected, I opted out for a series of articles requiring some prior SwiftUI and UIKit knowledge, but which I tried to keep accessible for newcomers as well:</p> <ul> <li><a href="/swiftui_collection_part1">Part 1: Mimicking Collections in SwiftUI</a>: A mild introduction into the world of collection aleternatives in SwiftUI (and associated delusions).</li> <li><a href="/swiftui_collection_part2">Part 2: SwiftUI Collection Implementation</a>: Where we build a first working SwiftUI collection from the ground up.</li> <li><a href="/swiftui_collection_part3">Part 3: Fixes and Focus Management</a>: Where we solve issues with the implementation and implement focus management for tvOS.</li> </ul> <p>The <a href="https://github.com/defagos/SwiftUICollection">source code</a> for this series is available if you want to have a look at the implementation while reading the articles.</p>UICollectionView has been an essential tool for Apple app developers since its introduction in iOS 6. Over the years it received a range of improvements and was ported to tvOS so that you can build layouts with the same formalism on both platforms. In iOS and tvOS 13 UICollectionView received the most significant enhancements ever made since its introduction, namely diffable data sources and compositional layouts.Yet another article about method swizzling2014-12-04T00:00:00+00:002014-12-04T00:00:00+00:00/yet_another_article_about_method_swizzling<p>Many Objective-C developers disregard method swizzling, considering it a bad practice. I don’t like method swizzling, I love it. Of course it is risky and can hurt you like a bullet. Carefully done, though, it makes it possible to fill annoying gaps in system frameworks which would be impossible to fill otherwise. From simply providing a convenient way to <a href="https://github.com/defagos/CoconutKit/blob/f6d8d3486a2a201d0cda4657e681c3d56cf7a261/CoconutKit/Sources/ViewControllers/UIPopoverController+HLSExtensions.m#L16-L74">track the parent popover controller of a view controller</a> to implementing <a href="https://github.com/defagos/CoconutKit/blob/f6d8d3486a2a201d0cda4657e681c3d56cf7a261/CoconutKit/Sources/Bindings/UIView+HLSViewBinding.m#L237-L255">Cocoa-like bindings on iOS</a>, method swizzling has always been an invaluable tool to me.</p> <ul id="markdown-toc"> <li><a href="#function-prototype" id="markdown-toc-function-prototype">Function prototype</a></li> <li><a href="#issues-in-class-hierarchies" id="markdown-toc-issues-in-class-hierarchies">Issues in class hierarchies</a></li> <li><a href="#first-implementation-attempt" id="markdown-toc-first-implementation-attempt">First implementation attempt</a></li> <li><a href="#large-struct-return-values" id="markdown-toc-large-struct-return-values">Large struct return values</a></li> <li><a href="#wrapping-up-imp-swizzling" id="markdown-toc-wrapping-up-imp-swizzling">Wrapping up: IMP-swizzling</a></li> <li><a href="#block-swizzling" id="markdown-toc-block-swizzling">Block-swizzling</a></li> <li><a href="#macros" id="markdown-toc-macros">Macros</a></li> <li><a href="#conclusion" id="markdown-toc-conclusion">Conclusion</a></li> </ul> <p>Implementing swizzling correctly is not easy, though, probably because it looks straightforward at first (all is needed is a few Objective-C runtime function calls, after all). Though the Web is <a href="https://www.google.ch/webhp?sourceid=chrome-instant&amp;ion=1&amp;espv=2&amp;ie=UTF-8#q=objective-c%20swizzling%20right">crawling with articles</a> about the right way to swizzle a method, I sadly found issues with all of them.</p> <p>The implementation discussed in this article may have issues of its own, but I do hope sharing it will help improve existing implementations, as well as mine. For this reason, do not regard these few words as a <em>Done right</em> article, only as a small step towards hopefully better swizzling implementations. There is a correct way to swizzle methods, but it probably still remains to be found.</p> <h2 id="function-prototype">Function prototype</h2> <p>Since the Objective-C runtime is a series of functions, I decided to implement swizzling as a function as well. Most existing implementations, for example the well-respected <a href="https://github.com/rentzsch/jrswizzle">JRSwizzle</a>, exchange <code class="language-plaintext highlighter-rouge">IMP</code>s associated with two selectors. Ultimately, though, method swizzling is about changing, not exchanging, which is why I prefer a function expecting an original <code class="language-plaintext highlighter-rouge">SEL</code> and an <code class="language-plaintext highlighter-rouge">IMP</code> arguments:</p> <div class="language-objc highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">IMP</span> <span class="nf">class_swizzleSelector</span><span class="p">(</span><span class="n">Class</span> <span class="n">clazz</span><span class="p">,</span> <span class="n">SEL</span> <span class="n">selector</span><span class="p">,</span> <span class="n">IMP</span> <span class="n">newImplementation</span><span class="p">);</span> </code></pre></div></div> <p>instead of two <code class="language-plaintext highlighter-rouge">SEL</code> arguments:</p> <div class="language-objc highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">IMP</span> <span class="nf">class_swizzleSelectorWithSelector</span><span class="p">(</span><span class="n">Class</span> <span class="n">clazz</span><span class="p">,</span> <span class="n">SEL</span> <span class="n">selector</span><span class="p">,</span> <span class="n">SEL</span> <span class="n">swizzlingSelector</span><span class="p">);</span> </code></pre></div></div> <p>Moreover, using an <code class="language-plaintext highlighter-rouge">IMP</code> (in other words a C-function) instead of a selector implementation avoids potential clashes if your swizzling selector name convention happens to be the same as the one used elsewhere, especially when dealing with 3rd party code. Don’t be too optimistic, accidental method overriding due to <a href="https://github.com/search?l=objective-c&amp;q=%22%28void%29commonInit%22&amp;ref=searchresults&amp;type=Code&amp;utf8=%E2%9C%93">bad conventions</a> can happen all the time.</p> <p>The function above returns the original implementation, which must be properly cast and called from within the swizzling method implementation, so that the original behavior is preserved. If the method to swizzle is not implemented, I decided the function must do nothing and return <code class="language-plaintext highlighter-rouge">NULL</code>.</p> <h2 id="issues-in-class-hierarchies">Issues in class hierarchies</h2> <p>When swizzling methods in class hierarchies, we must take extra care when the method we swizzle is not implemented by the class on which it is swizzled, but is implemented by one of its parents. For example, the <code class="language-plaintext highlighter-rouge">-awakeFromNib</code> method, declared and implemented at the <code class="language-plaintext highlighter-rouge">NSObject</code> level, is neither implemented by the <code class="language-plaintext highlighter-rouge">UIView</code> nor by the <code class="language-plaintext highlighter-rouge">UILabel</code> subclasses. When calling this method on an instance of any of theses classes, it is therefore the <code class="language-plaintext highlighter-rouge">NSObject</code> implementation which gets called:</p> <p><img src="/images/standard_hierarchy.jpg" alt="Standard hierarchy" /></p> <p>If we naively swizzle the <code class="language-plaintext highlighter-rouge">-awakeFromNib</code> method both at the <code class="language-plaintext highlighter-rouge">UIView</code> and <code class="language-plaintext highlighter-rouge">UILabel</code> levels, we get the following result:</p> <p><img src="/images/standard_hierarchy_swizzled.jpg" alt="Standard hierarchy, swizzled" /></p> <p>As we see, when <code class="language-plaintext highlighter-rouge">-[UILabel awakeFromNib]</code> is now called, the swizzled <code class="language-plaintext highlighter-rouge">UIView </code>implementation does not get called, which is not what is expected from proper swizzling.</p> <p>The situation would be completely different if the <code class="language-plaintext highlighter-rouge">-awakeFromNib</code> method was implemented on <code class="language-plaintext highlighter-rouge">UIView</code> and <code class="language-plaintext highlighter-rouge">UILabel</code>. If this was the case, and if each implementation properly called the <code class="language-plaintext highlighter-rouge">super</code> method counterpart first, we would namely obtain:</p> <p><img src="/images/tweaked_hierarchy.jpg" alt="Tweaked hierarchy" /></p> <p>and, after swizzling:</p> <p><img src="/images/tweaked_hierarchy_swizzled.jpg" alt="Tweaked hierarchy, swizzled" /></p> <p>No swizzling implementation I encountered correctly deals with this issue, <a href="https://github.com/rentzsch/jrswizzle/issues/4">not even JRSwizzle</a>. As should be clear from the last picture above, the solution to this problem is to ensure a method is always implemented by a class before swizzling it. If this is not the case, an implementation must be injected first, simply calling the super method counterpart. This way, all implementations will correctly be called after swizzling.</p> <h2 id="first-implementation-attempt">First implementation attempt</h2> <p>Based on the above, I first implemented instance method swizzling as follows:</p> <div class="language-objc highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#import &lt;objc/runtime.h&gt; #import &lt;objc/message.h&gt; </span> <span class="n">IMP</span> <span class="nf">class_swizzleSelector</span><span class="p">(</span><span class="n">Class</span> <span class="n">clazz</span><span class="p">,</span> <span class="n">SEL</span> <span class="n">selector</span><span class="p">,</span> <span class="n">IMP</span> <span class="n">newImplementation</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// If the method does not exist for this class, do nothing</span> <span class="n">Method</span> <span class="n">method</span> <span class="o">=</span> <span class="n">class_getInstanceMethod</span><span class="p">(</span><span class="n">clazz</span><span class="p">,</span> <span class="n">selector</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span> <span class="n">method</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="nb">NULL</span><span class="p">;</span> <span class="p">}</span> <span class="c1">// Make sure the class implements the method. If this is not the case, inject an implementation, calling 'super'</span> <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">types</span> <span class="o">=</span> <span class="n">method_getTypeEncoding</span><span class="p">(</span><span class="n">method</span><span class="p">);</span> <span class="n">class_addMethod</span><span class="p">(</span><span class="n">clazz</span><span class="p">,</span> <span class="n">selector</span><span class="p">,</span> <span class="n">imp_implementationWithBlock</span><span class="p">(</span><span class="o">^</span><span class="p">(</span><span class="n">__unsafe_unretained</span> <span class="n">id</span> <span class="n">self</span><span class="p">,</span> <span class="kt">va_list</span> <span class="n">argp</span><span class="p">)</span> <span class="p">{</span> <span class="k">struct</span> <span class="n">objc_super</span> <span class="n">super</span> <span class="o">=</span> <span class="p">{</span> <span class="p">.</span><span class="n">receiver</span> <span class="o">=</span> <span class="n">self</span><span class="p">,</span> <span class="p">.</span><span class="n">super_class</span> <span class="o">=</span> <span class="n">class_getSuperclass</span><span class="p">(</span><span class="n">clazz</span><span class="p">)</span> <span class="p">};</span> <span class="n">id</span> <span class="p">(</span><span class="o">*</span><span class="n">objc_msgSendSuper_typed</span><span class="p">)(</span><span class="k">struct</span> <span class="n">objc_super</span> <span class="o">*</span><span class="p">,</span> <span class="n">SEL</span><span class="p">,</span> <span class="kt">va_list</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">objc_msgSendSuper</span><span class="p">;</span> <span class="k">return</span> <span class="n">objc_msgSendSuper_typed</span><span class="p">(</span><span class="o">&amp;</span><span class="n">super</span><span class="p">,</span> <span class="n">selector</span><span class="p">,</span> <span class="n">argp</span><span class="p">);</span> <span class="p">}),</span> <span class="n">types</span><span class="p">);</span> <span class="c1">// Can now safely swizzle</span> <span class="k">return</span> <span class="n">class_replaceMethod</span><span class="p">(</span><span class="n">clazz</span><span class="p">,</span> <span class="n">selector</span><span class="p">,</span> <span class="n">newImplementation</span><span class="p">,</span> <span class="n">types</span><span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p>For class method swizzling, it suffices to call the above function on a metaclass:</p> <div class="language-objc highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">IMP</span> <span class="nf">class_swizzleClassSelector</span><span class="p">(</span><span class="n">Class</span> <span class="n">clazz</span><span class="p">,</span> <span class="n">SEL</span> <span class="n">selector</span><span class="p">,</span> <span class="n">IMP</span> <span class="n">newImplementation</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="n">class_swizzleSelector</span><span class="p">(</span><span class="n">object_getClass</span><span class="p">(</span><span class="n">clazz</span><span class="p">),</span> <span class="n">selector</span><span class="p">,</span> <span class="n">newImplementation</span><span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p>The <code class="language-plaintext highlighter-rouge">imp_implementationWithBlock</code> function is used as a trampoline to accomodate any kind of method prototype through a variable argument list <code class="language-plaintext highlighter-rouge">va_list</code>. The <code class="language-plaintext highlighter-rouge">super</code> method call is made by properly casting <code class="language-plaintext highlighter-rouge">objc_msgSendSuper</code>, available from <code class="language-plaintext highlighter-rouge">&lt;objc/message.h&gt;</code>. In order to prevent ARC from inserting incorrect memory management calls, the <code class="language-plaintext highlighter-rouge">self</code> parameter of the implementation block has been marked with <code class="language-plaintext highlighter-rouge">__unsafe_unretained</code>.</p> <h2 id="large-struct-return-values">Large struct return values</h2> <p>As pointed out by <a href="https://twitter.com/steipete/status/540818091014627329">Peter Steinberger</a> and <a href="https://twitter.com/__block/status/540794469046812674">@__block</a> on Twitter, struct returns require special care on some architectures.</p> <p>Most method calls are funnelled through the <code class="language-plaintext highlighter-rouge">objc_msgSend</code> function, returning the result in a register. For large structs which cannot fit in a register, though, the compiler might generate a call to the special <code class="language-plaintext highlighter-rouge">objc_msgSend_stret</code> function, which returns the parameter on the stack instead. According to the <a href="https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/LowLevelABI/000-Introduction/introduction.html">ABI</a>, this happens on 32-bit architectures for types whose size is neither 1, 2, 4 nor 8. The implementation above does not account for this special case and must therefore be changed to account for such cases.</p> <p>For method returning large structs, we need the <code class="language-plaintext highlighter-rouge">imp_implementationWithBlock</code> function to generate the correct implementation by having the block return a large struct. The kind of struct and its layout are irrelevant, we only need it to be sufficiently large so that the compiler can make the right decision. As for <code class="language-plaintext highlighter-rouge">objc_msgSend_stret</code>, there is an <code class="language-plaintext highlighter-rouge">objc_msgSendSuper_stret</code> for super calls to methods returning large structs, which we need to use instead.</p> <p>For large struct returns, instead of calling the <code class="language-plaintext highlighter-rouge">class_swizzleSelector</code> function above, we therefore must call the following <code class="language-plaintext highlighter-rouge">class_swizzleSelector_stret</code> function:</p> <div class="language-objc highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#import &lt;objc/runtime.h&gt; #import &lt;objc/message.h&gt; </span> <span class="n">IMP</span> <span class="nf">class_swizzleSelector_stret</span><span class="p">(</span><span class="n">Class</span> <span class="n">clazz</span><span class="p">,</span> <span class="n">SEL</span> <span class="n">selector</span><span class="p">,</span> <span class="n">IMP</span> <span class="n">newImplementation</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// If the method does not exist for this class, do nothing</span> <span class="n">Method</span> <span class="n">method</span> <span class="o">=</span> <span class="n">class_getInstanceMethod</span><span class="p">(</span><span class="n">clazz</span><span class="p">,</span> <span class="n">selector</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span> <span class="n">method</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="nb">NULL</span><span class="p">;</span> <span class="p">}</span> <span class="c1">// Make sure the class implements the method. If this is not the case, inject an implementation, only calling 'super'</span> <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">types</span> <span class="o">=</span> <span class="n">method_getTypeEncoding</span><span class="p">(</span><span class="n">method</span><span class="p">);</span> <span class="n">class_addMethod</span><span class="p">(</span><span class="n">clazz</span><span class="p">,</span> <span class="n">selector</span><span class="p">,</span> <span class="n">imp_implementationWithBlock</span><span class="p">(</span><span class="o">^</span><span class="p">(</span><span class="n">__unsafe_unretained</span> <span class="n">id</span> <span class="n">self</span><span class="p">,</span> <span class="kt">va_list</span> <span class="n">argp</span><span class="p">)</span> <span class="p">{</span> <span class="k">struct</span> <span class="n">objc_super</span> <span class="n">super</span> <span class="o">=</span> <span class="p">{</span> <span class="p">.</span><span class="n">receiver</span> <span class="o">=</span> <span class="n">self</span><span class="p">,</span> <span class="p">.</span><span class="n">super_class</span> <span class="o">=</span> <span class="n">class_getSuperclass</span><span class="p">(</span><span class="n">clazz</span><span class="p">)</span> <span class="p">};</span> <span class="c1">// Sufficiently large struct</span> <span class="k">typedef</span> <span class="k">struct</span> <span class="n">LargeStruct_</span> <span class="p">{</span> <span class="kt">char</span> <span class="n">dummy</span><span class="p">[</span><span class="mi">16</span><span class="p">];</span> <span class="p">}</span> <span class="n">LargeStruct</span><span class="p">;</span> <span class="c1">// Cast the call to objc_msgSendSuper_stret appropriately</span> <span class="n">LargeStruct</span> <span class="p">(</span><span class="o">*</span><span class="n">objc_msgSendSuper_stret_typed</span><span class="p">)(</span><span class="k">struct</span> <span class="n">objc_super</span> <span class="o">*</span><span class="p">,</span> <span class="n">SEL</span><span class="p">,</span> <span class="kt">va_list</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">objc_msgSendSuper_stret</span><span class="p">;</span> <span class="k">return</span> <span class="n">objc_msgSendSuper_stret_typed</span><span class="p">(</span><span class="o">&amp;</span><span class="n">super</span><span class="p">,</span> <span class="n">selector</span><span class="p">,</span> <span class="n">argp</span><span class="p">);</span> <span class="p">}),</span> <span class="n">types</span><span class="p">);</span> <span class="c1">// Can now safely swizzle</span> <span class="k">return</span> <span class="n">class_replaceMethod</span><span class="p">(</span><span class="n">clazz</span><span class="p">,</span> <span class="n">selector</span><span class="p">,</span> <span class="n">newImplementation</span><span class="p">,</span> <span class="n">types</span><span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <h2 id="wrapping-up-imp-swizzling">Wrapping up: IMP-swizzling</h2> <p>Having a separate <code class="language-plaintext highlighter-rouge">class_swizzleSelector_stret</code> function which must appropriately be called when large structs are returned is rather inconvenient. Fortunately, its implementation can be merged into <code class="language-plaintext highlighter-rouge">class_swizzleSelector</code> by checking return type size information for 32-bit architectures first. We obtain a single function for method swizzling with an <code class="language-plaintext highlighter-rouge">IMP</code>:</p> <div class="language-objc highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#import &lt;objc/runtime.h&gt; #import &lt;objc/message.h&gt; </span> <span class="n">IMP</span> <span class="nf">class_swizzleSelector</span><span class="p">(</span><span class="n">Class</span> <span class="n">clazz</span><span class="p">,</span> <span class="n">SEL</span> <span class="n">selector</span><span class="p">,</span> <span class="n">IMP</span> <span class="n">newImplementation</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// If the method does not exist for this class, do nothing</span> <span class="n">Method</span> <span class="n">method</span> <span class="o">=</span> <span class="n">class_getInstanceMethod</span><span class="p">(</span><span class="n">clazz</span><span class="p">,</span> <span class="n">selector</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span> <span class="n">method</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// Cannot swizzle methods which are not implemented by the class or one of its parents</span> <span class="k">return</span> <span class="nb">NULL</span><span class="p">;</span> <span class="p">}</span> <span class="c1">// Make sure the class implements the method. If this is not the case, inject an implementation, only calling 'super'</span> <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">types</span> <span class="o">=</span> <span class="n">method_getTypeEncoding</span><span class="p">(</span><span class="n">method</span><span class="p">);</span> <span class="cp">#if !defined(__arm64__) </span> <span class="n">NSUInteger</span> <span class="n">returnSize</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">NSGetSizeAndAlignment</span><span class="p">(</span><span class="n">types</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">returnSize</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">);</span> <span class="c1">// Large structs on 32-bit architectures</span> <span class="k">if</span> <span class="p">(</span><span class="k">sizeof</span><span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="p">)</span> <span class="o">==</span> <span class="mi">4</span> <span class="o">&amp;&amp;</span> <span class="n">types</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="n">_C_STRUCT_B</span> <span class="o">&amp;&amp;</span> <span class="n">returnSize</span> <span class="o">!=</span> <span class="mi">1</span> <span class="o">&amp;&amp;</span> <span class="n">returnSize</span> <span class="o">!=</span> <span class="mi">2</span> <span class="o">&amp;&amp;</span> <span class="n">returnSize</span> <span class="o">!=</span> <span class="mi">4</span> <span class="o">&amp;&amp;</span> <span class="n">returnSize</span> <span class="o">!=</span> <span class="mi">8</span><span class="p">)</span> <span class="p">{</span> <span class="n">class_addMethod</span><span class="p">(</span><span class="n">clazz</span><span class="p">,</span> <span class="n">selector</span><span class="p">,</span> <span class="n">imp_implementationWithBlock</span><span class="p">(</span><span class="o">^</span><span class="p">(</span><span class="n">__unsafe_unretained</span> <span class="n">id</span> <span class="n">self</span><span class="p">,</span> <span class="kt">va_list</span> <span class="n">argp</span><span class="p">)</span> <span class="p">{</span> <span class="k">struct</span> <span class="n">objc_super</span> <span class="n">super</span> <span class="o">=</span> <span class="p">{</span> <span class="p">.</span><span class="n">receiver</span> <span class="o">=</span> <span class="n">self</span><span class="p">,</span> <span class="p">.</span><span class="n">super_class</span> <span class="o">=</span> <span class="n">class_getSuperclass</span><span class="p">(</span><span class="n">clazz</span><span class="p">)</span> <span class="p">};</span> <span class="c1">// Sufficiently large struct</span> <span class="k">typedef</span> <span class="k">struct</span> <span class="n">LargeStruct_</span> <span class="p">{</span> <span class="kt">char</span> <span class="n">dummy</span><span class="p">[</span><span class="mi">16</span><span class="p">];</span> <span class="p">}</span> <span class="n">LargeStruct</span><span class="p">;</span> <span class="c1">// Cast the call to objc_msgSendSuper_stret appropriately</span> <span class="n">LargeStruct</span> <span class="p">(</span><span class="o">*</span><span class="n">objc_msgSendSuper_stret_typed</span><span class="p">)(</span><span class="k">struct</span> <span class="n">objc_super</span> <span class="o">*</span><span class="p">,</span> <span class="n">SEL</span><span class="p">,</span> <span class="kt">va_list</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">objc_msgSendSuper_stret</span><span class="p">;</span> <span class="k">return</span> <span class="n">objc_msgSendSuper_stret_typed</span><span class="p">(</span><span class="o">&amp;</span><span class="n">super</span><span class="p">,</span> <span class="n">selector</span><span class="p">,</span> <span class="n">argp</span><span class="p">);</span> <span class="p">}),</span> <span class="n">types</span><span class="p">);</span> <span class="p">}</span> <span class="c1">// All other cases</span> <span class="k">else</span> <span class="p">{</span> <span class="cp">#endif </span> <span class="n">class_addMethod</span><span class="p">(</span><span class="n">clazz</span><span class="p">,</span> <span class="n">selector</span><span class="p">,</span> <span class="n">imp_implementationWithBlock</span><span class="p">(</span><span class="o">^</span><span class="p">(</span><span class="n">__unsafe_unretained</span> <span class="n">id</span> <span class="n">self</span><span class="p">,</span> <span class="kt">va_list</span> <span class="n">argp</span><span class="p">)</span> <span class="p">{</span> <span class="k">struct</span> <span class="n">objc_super</span> <span class="n">super</span> <span class="o">=</span> <span class="p">{</span> <span class="p">.</span><span class="n">receiver</span> <span class="o">=</span> <span class="n">self</span><span class="p">,</span> <span class="p">.</span><span class="n">super_class</span> <span class="o">=</span> <span class="n">class_getSuperclass</span><span class="p">(</span><span class="n">clazz</span><span class="p">)</span> <span class="p">};</span> <span class="c1">// Cast the call to objc_msgSendSuper appropriately</span> <span class="n">id</span> <span class="p">(</span><span class="o">*</span><span class="n">objc_msgSendSuper_typed</span><span class="p">)(</span><span class="k">struct</span> <span class="n">objc_super</span> <span class="o">*</span><span class="p">,</span> <span class="n">SEL</span><span class="p">,</span> <span class="kt">va_list</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">objc_msgSendSuper</span><span class="p">;</span> <span class="k">return</span> <span class="n">objc_msgSendSuper_typed</span><span class="p">(</span><span class="o">&amp;</span><span class="n">super</span><span class="p">,</span> <span class="n">selector</span><span class="p">,</span> <span class="n">argp</span><span class="p">);</span> <span class="p">}),</span> <span class="n">types</span><span class="p">);</span> <span class="cp">#if !defined(__arm64__) </span> <span class="p">}</span> <span class="cp">#endif </span> <span class="c1">// Swizzling</span> <span class="k">return</span> <span class="n">class_replaceMethod</span><span class="p">(</span><span class="n">clazz</span><span class="p">,</span> <span class="n">selector</span><span class="p">,</span> <span class="n">newImplementation</span><span class="p">,</span> <span class="n">types</span><span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p>The <code class="language-plaintext highlighter-rouge">_stret</code> variants are not available on ARM 64, thus the extra preprocessor adornments.</p> <h3 class="no_toc" id="example-of-use">Example of use</h3> <p>To swizzle a method, define a static C-function for the new implementation and call <code class="language-plaintext highlighter-rouge">class_swizzleSelector</code> or <code class="language-plaintext highlighter-rouge">class_swizzleClassSelector</code> to set it as new one. Save the original implementation into a function pointer matching the function signature, and make sure the new implementation calls it somehow:</p> <div class="language-objc highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="n">id</span> <span class="p">(</span><span class="o">*</span><span class="n">initWithFrame</span><span class="p">)(</span><span class="n">id</span><span class="p">,</span> <span class="n">SEL</span><span class="p">,</span> <span class="n">CGRect</span><span class="p">)</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span> <span class="k">static</span> <span class="kt">void</span> <span class="p">(</span><span class="o">*</span><span class="n">awakeFromNib</span><span class="p">)(</span><span class="n">id</span><span class="p">,</span> <span class="n">SEL</span><span class="p">)</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span> <span class="k">static</span> <span class="kt">void</span> <span class="p">(</span><span class="o">*</span><span class="n">dealloc</span><span class="p">)(</span><span class="n">__unsafe_unretained</span> <span class="n">id</span><span class="p">,</span> <span class="n">SEL</span><span class="p">)</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span> <span class="k">static</span> <span class="n">id</span> <span class="nf">swizzle_initWithFrame</span><span class="p">(</span><span class="n">UILabel</span> <span class="o">*</span><span class="n">self</span><span class="p">,</span> <span class="n">SEL</span> <span class="n">_cmd</span><span class="p">,</span> <span class="n">CGRect</span> <span class="n">frame</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">((</span><span class="n">self</span> <span class="o">=</span> <span class="n">initWithFrame</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">_cmd</span><span class="p">,</span> <span class="n">frame</span><span class="p">)))</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="p">}</span> <span class="k">return</span> <span class="n">self</span><span class="p">;</span> <span class="p">}</span> <span class="k">static</span> <span class="kt">void</span> <span class="nf">swizzle_awakeFromNib</span><span class="p">(</span><span class="n">UILabel</span> <span class="o">*</span><span class="n">self</span><span class="p">,</span> <span class="n">SEL</span> <span class="n">_cmd</span><span class="p">)</span> <span class="p">{</span> <span class="n">awakeFromNib</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">_cmd</span><span class="p">);</span> <span class="c1">// ...</span> <span class="p">}</span> <span class="k">static</span> <span class="kt">void</span> <span class="nf">swizzle_dealloc</span><span class="p">(</span><span class="n">__unsafe_unretained</span> <span class="n">UILabel</span> <span class="o">*</span><span class="n">self</span><span class="p">,</span> <span class="n">SEL</span> <span class="n">_cmd</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="n">dealloc</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">_cmd</span><span class="p">);</span> <span class="p">}</span> <span class="k">@implementation</span> <span class="nc">UILabel</span> <span class="p">(</span><span class="nl">SwizzlingExamples</span><span class="p">)</span> <span class="k">+</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">load</span> <span class="p">{</span> <span class="n">initWithFrame</span> <span class="o">=</span> <span class="p">(</span><span class="n">__typeof</span><span class="p">(</span><span class="n">initWithFrame</span><span class="p">))</span><span class="n">class_swizzleSelector</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="k">@selector</span><span class="p">(</span><span class="n">initWithFrame</span><span class="o">:</span><span class="p">),</span> <span class="p">(</span><span class="n">IMP</span><span class="p">)</span><span class="n">swizzle_initWithFrame</span><span class="p">);</span> <span class="n">awakeFromNib</span> <span class="o">=</span> <span class="p">(</span><span class="n">__typeof</span><span class="p">(</span><span class="n">awakeFromNib</span><span class="p">))</span><span class="n">class_swizzleSelector</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="k">@selector</span><span class="p">(</span><span class="n">awakeFromNib</span><span class="p">),</span> <span class="p">(</span><span class="n">IMP</span><span class="p">)</span><span class="n">swizzle_awakeFromNib</span><span class="p">);</span> <span class="n">dealloc</span> <span class="o">=</span> <span class="p">(</span><span class="n">__typeof</span><span class="p">(</span><span class="n">dealloc</span><span class="p">))</span><span class="n">class_swizzleSelector</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">sel_getUid</span><span class="p">(</span><span class="s">"dealloc"</span><span class="p">),</span> <span class="p">(</span><span class="n">IMP</span><span class="p">)</span><span class="n">swizzle_dealloc</span><span class="p">);</span> <span class="p">}</span> <span class="k">@end</span> </code></pre></div></div> <p>Note that I added an extra <code class="language-plaintext highlighter-rouge">__unsafe_unretained</code> specifier to the <code class="language-plaintext highlighter-rouge">swizzle_dealloc</code> prototype to ensure ARC does not insert additional memory management calls. I also cheated by getting the <code class="language-plaintext highlighter-rouge">dealloc</code> selector with <code class="language-plaintext highlighter-rouge">sel_getUid</code>, since <code class="language-plaintext highlighter-rouge">@selector(dealloc)</code> cannot be used with ARC.</p> <h2 id="block-swizzling">Block-swizzling</h2> <p>Thanks to <code class="language-plaintext highlighter-rouge">imp_implementationWithBlock</code>, we can provide a block instead of an <code class="language-plaintext highlighter-rouge">IMP</code> for the new implementation:</p> <div class="language-objc highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">IMP</span> <span class="nf">class_swizzleSelectorWithBlock</span><span class="p">(</span><span class="n">Class</span> <span class="n">clazz</span><span class="p">,</span> <span class="n">SEL</span> <span class="n">selector</span><span class="p">,</span> <span class="n">id</span> <span class="n">newImplementationBlock</span><span class="p">)</span> <span class="p">{</span> <span class="n">IMP</span> <span class="n">newImplementation</span> <span class="o">=</span> <span class="n">imp_implementationWithBlock</span><span class="p">(</span><span class="n">newImplementationBlock</span><span class="p">);</span> <span class="k">return</span> <span class="n">class_swizzleSelector</span><span class="p">(</span><span class="n">clazz</span><span class="p">,</span> <span class="n">selector</span><span class="p">,</span> <span class="n">newImplementation</span><span class="p">);</span> <span class="p">}</span> <span class="n">IMP</span> <span class="nf">class_swizzleClassSelectorWithBlock</span><span class="p">(</span><span class="n">Class</span> <span class="n">clazz</span><span class="p">,</span> <span class="n">SEL</span> <span class="n">selector</span><span class="p">,</span> <span class="n">id</span> <span class="n">newImplementationBlock</span><span class="p">)</span> <span class="p">{</span> <span class="n">IMP</span> <span class="n">newImplementation</span> <span class="o">=</span> <span class="n">imp_implementationWithBlock</span><span class="p">(</span><span class="n">newImplementationBlock</span><span class="p">);</span> <span class="k">return</span> <span class="n">class_swizzleClassSelector</span><span class="p">(</span><span class="n">clazz</span><span class="p">,</span> <span class="n">selector</span><span class="p">,</span> <span class="n">newImplementation</span><span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p>The block signature itself does not include the selector parameter, as specified in the <code class="language-plaintext highlighter-rouge">imp_implementationWithBlock</code> <a href="https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ObjCRuntimeRef/index.html">documentation</a>.</p> <h3 class="no_toc" id="example-of-use-1">Example of use</h3> <p>The above example can be rewritten using blocks, eliminating the need for static methods and function pointers:</p> <div class="language-objc highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">@implementation</span> <span class="nc">UILabel</span> <span class="p">(</span><span class="nl">SwizzlingExamples</span><span class="p">)</span> <span class="k">+</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">load</span> <span class="p">{</span> <span class="n">__block</span> <span class="n">IMP</span> <span class="n">originalInitWithFrame</span> <span class="o">=</span> <span class="n">class_swizzleSelectorWithBlock</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="k">@selector</span><span class="p">(</span><span class="n">initWithFrame</span><span class="o">:</span><span class="p">),</span> <span class="o">^</span><span class="p">(</span><span class="n">UILabel</span> <span class="o">*</span><span class="n">self</span><span class="p">,</span> <span class="n">CGRect</span> <span class="n">frame</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">((</span><span class="n">self</span> <span class="o">=</span> <span class="p">((</span><span class="n">id</span> <span class="p">(</span><span class="o">*</span><span class="p">)(</span><span class="n">id</span><span class="p">,</span> <span class="n">SEL</span><span class="p">,</span> <span class="n">CGRect</span><span class="p">))</span><span class="n">originalInitWithFrame</span><span class="p">)(</span><span class="n">self</span><span class="p">,</span> <span class="k">@selector</span><span class="p">(</span><span class="n">initWithFrame</span><span class="o">:</span><span class="p">),</span> <span class="n">frame</span><span class="p">)))</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="p">}</span> <span class="k">return</span> <span class="n">self</span><span class="p">;</span> <span class="p">});</span> <span class="n">__block</span> <span class="n">IMP</span> <span class="n">originalAwakeFromNib</span> <span class="o">=</span> <span class="n">class_swizzleSelectorWithBlock</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="k">@selector</span><span class="p">(</span><span class="n">awakeFromNib</span><span class="p">),</span> <span class="o">^</span><span class="p">(</span><span class="n">UILabel</span> <span class="o">*</span><span class="n">self</span><span class="p">)</span> <span class="p">{</span> <span class="p">((</span><span class="kt">void</span> <span class="p">(</span><span class="o">*</span><span class="p">)(</span><span class="n">id</span><span class="p">,</span> <span class="n">SEL</span><span class="p">))</span><span class="n">originalAwakeFromNib</span><span class="p">)(</span><span class="n">self</span><span class="p">,</span> <span class="k">@selector</span><span class="p">(</span><span class="n">awakeFromNib</span><span class="p">));</span> <span class="c1">// ...</span> <span class="p">});</span> <span class="n">__block</span> <span class="n">IMP</span> <span class="n">originalDealloc</span> <span class="o">=</span> <span class="n">class_swizzleSelectorWithBlock</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">sel_getUid</span><span class="p">(</span><span class="s">"dealloc"</span><span class="p">),</span> <span class="o">^</span><span class="p">(</span><span class="n">__unsafe_unretained</span> <span class="n">UILabel</span> <span class="o">*</span><span class="n">self</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="p">((</span><span class="kt">void</span> <span class="p">(</span><span class="o">*</span><span class="p">)(</span><span class="n">id</span><span class="p">,</span> <span class="n">SEL</span><span class="p">))</span><span class="n">originalDealloc</span><span class="p">)(</span><span class="n">self</span><span class="p">,</span> <span class="n">sel_getUid</span><span class="p">(</span><span class="s">"dealloc"</span><span class="p">));</span> <span class="p">});</span> <span class="p">}</span> <span class="k">@end</span> </code></pre></div></div> <p>Returned original implementations must be saved into <code class="language-plaintext highlighter-rouge">__block</code> variables to be accessible from within the corresponding implementation blocks.</p> <h2 id="macros">Macros</h2> <p>Some redundancy is found in both examples of use above, but can be eliminated by defining a few convenience macros:</p> <div class="language-objc highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define SwizzleSelector(clazz, selector, newImplementation, pPreviousImplementation) \ (*pPreviousImplementation) = (__typeof((*pPreviousImplementation)))class_swizzleSelector((clazz), (selector), (IMP)(newImplementation)) </span> <span class="cp">#define SwizzleClassSelector(clazz, selector, newImplementation, pPreviousImplementation) \ (*pPreviousImplementation) = (__typeof((*pPreviousImplementation)))class_swizzleClassSelector((clazz), (selector), (IMP)(newImplementation)) </span> <span class="cp">#define SwizzleSelectorWithBlock_Begin(clazz, selector) { \ SEL _cmd = selector; \ __block IMP _imp = class_swizzleSelectorWithBlock((clazz), (selector), #define SwizzleSelectorWithBlock_End );} </span> <span class="cp">#define SwizzleClassSelectorWithBlock_Begin(clazz, selector) { \ SEL _cmd = selector; \ __block IMP _imp = class_swizzleClassSelectorWithBlock((clazz), (selector), #define SwizzleClassSelectorWithBlock_End );} </span></code></pre></div></div> <p>To emphasize that the <code class="language-plaintext highlighter-rouge">IMP</code>-swizzling macros set the new implementation variable, the corresponding macro parameter needs to be adorned with an ampersand.</p> <p>Block-swizzling, on the other hand, has been turned into a pair of macros. This avoids block parameters, which do not work well with macros:</p> <ul> <li>Macro expansion of a block turns the block implementation into a single line, confusing the debugger</li> <li>A block can contain commas, preventing correct macro argument detection (this can be avoided by enclosing the blocks within parentheses, though)</li> </ul> <p>Moreover, the block-swizzling macros declare a hidden scope where the selector <code class="language-plaintext highlighter-rouge">_cmd</code> and original implementation <code class="language-plaintext highlighter-rouge">_imp</code> are immediately available.</p> <h3 class="no_toc" id="example-of-use-2">Example of use</h3> <p>The two previous examples can now be rewritten as follows:</p> <div class="language-objc highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">@implementation</span> <span class="nc">UILabel</span> <span class="p">(</span><span class="nl">SwizzlingExamples</span><span class="p">)</span> <span class="c1">// Function pointers and static functions, as defined above</span> <span class="k">+</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">load</span> <span class="p">{</span> <span class="n">SwizzleSelector</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="k">@selector</span><span class="p">(</span><span class="n">initWithFrame</span><span class="o">:</span><span class="p">),</span> <span class="n">swizzle_initWithFrame</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">initWithFrame</span><span class="p">);</span> <span class="n">SwizzleSelector</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="k">@selector</span><span class="p">(</span><span class="n">awakeFromNib</span><span class="p">),</span> <span class="o">&amp;</span><span class="n">swizzle_awakeFromNib</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">awakeFromNib</span><span class="p">);</span> <span class="n">SwizzleSelector</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">sel_getUid</span><span class="p">(</span><span class="s">"dealloc"</span><span class="p">),</span> <span class="o">&amp;</span><span class="n">swizzle_dealloc</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">dealloc</span><span class="p">);</span> <span class="p">}</span> <span class="k">@end</span> </code></pre></div></div> <p>respectively:</p> <div class="language-objc highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">@implementation</span> <span class="nc">UILabel</span> <span class="p">(</span><span class="nl">SwizzlingExamples</span><span class="p">)</span> <span class="k">+</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">load</span> <span class="p">{</span> <span class="n">SwizzleSelectorWithBlock_Begin</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="k">@selector</span><span class="p">(</span><span class="n">initWithFrame</span><span class="o">:</span><span class="p">))</span> <span class="o">^</span><span class="p">(</span><span class="n">UILabel</span> <span class="o">*</span><span class="n">self</span><span class="p">,</span> <span class="n">CGRect</span> <span class="n">frame</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">((</span><span class="n">self</span> <span class="o">=</span> <span class="p">((</span><span class="n">id</span> <span class="p">(</span><span class="o">*</span><span class="p">)(</span><span class="n">id</span><span class="p">,</span> <span class="n">SEL</span><span class="p">,</span> <span class="n">CGRect</span><span class="p">))</span><span class="n">_imp</span><span class="p">)(</span><span class="n">self</span><span class="p">,</span> <span class="n">_cmd</span><span class="p">,</span> <span class="n">frame</span><span class="p">)))</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="p">}</span> <span class="k">return</span> <span class="n">self</span><span class="p">;</span> <span class="p">}</span> <span class="n">SwizzleSelectorWithBlock_End</span><span class="p">;</span> <span class="n">SwizzleSelectorWithBlock_Begin</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="k">@selector</span><span class="p">(</span><span class="n">awakeFromNib</span><span class="p">))</span> <span class="o">^</span><span class="p">(</span><span class="n">UILabel</span> <span class="o">*</span><span class="n">self</span><span class="p">)</span> <span class="p">{</span> <span class="p">((</span><span class="kt">void</span> <span class="p">(</span><span class="o">*</span><span class="p">)(</span><span class="n">id</span><span class="p">,</span> <span class="n">SEL</span><span class="p">))</span><span class="n">_imp</span><span class="p">)(</span><span class="n">self</span><span class="p">,</span> <span class="n">_cmd</span><span class="p">);</span> <span class="c1">// ...</span> <span class="p">}</span> <span class="n">SwizzleSelectorWithBlock_End</span><span class="p">;</span> <span class="n">SwizzleSelectorWithBlock_Begin</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">sel_getUid</span><span class="p">(</span><span class="s">"dealloc"</span><span class="p">))</span> <span class="o">^</span><span class="p">(</span><span class="n">__unsafe_unretained</span> <span class="n">UILabel</span> <span class="o">*</span><span class="n">self</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="p">((</span><span class="kt">void</span> <span class="p">(</span><span class="o">*</span><span class="p">)(</span><span class="n">id</span><span class="p">,</span> <span class="n">SEL</span><span class="p">))</span><span class="n">_imp</span><span class="p">)(</span><span class="n">self</span><span class="p">,</span> <span class="n">_cmd</span><span class="p">);</span> <span class="p">}</span> <span class="n">SwizzleSelectorWithBlock_End</span><span class="p">;</span> <span class="p">}</span> <span class="k">@end</span> </code></pre></div></div> <p>I especially like the compactness of block-swizzling using macros. There is no need to define separate functions and method pointers, and the swizzled method implementation can be easily recognized, enclosed between the <code class="language-plaintext highlighter-rouge">Begin</code> and <code class="language-plaintext highlighter-rouge">End</code> macros.</p> <h2 id="conclusion">Conclusion</h2> <p>The implementation discussed in this article might not be optimal, but should cover most practical cases. It is available from my <a href="https://github.com/defagos/CoconutKit">CoconutKit framework</a>, with a comprehensive <a href="https://github.com/defagos/CoconutKit/blob/f6d8d3486a2a201d0cda4657e681c3d56cf7a261/CoconutKit-tests/Sources/Core/HLSRuntimeTestCase.m#L1277-L1370">test suite</a>, or from the following <a href="https://gist.github.com/defagos/1312fec96b48540efa5c">gist</a>. Have fun with method swizzling!</p>Many Objective-C developers disregard method swizzling, considering it a bad practice. I don’t like method swizzling, I love it. Of course it is risky and can hurt you like a bullet. Carefully done, though, it makes it possible to fill annoying gaps in system frameworks which would be impossible to fill otherwise. From simply providing a convenient way to track the parent popover controller of a view controller to implementing Cocoa-like bindings on iOS, method swizzling has always been an invaluable tool to me.Sharing LLDB debugging helpers between projects using a dynamic library2014-03-11T00:00:00+00:002014-03-11T00:00:00+00:00/building_ios_dynamic_libraries<p>Xcode 5 added <a href="https://developer.apple.com/library/mac/documentation/IDEs/Conceptual/CustomClassDisplay_in_QuickLook/Introduction/Introduction.html">QuickLook support</a> for standard types like <code class="language-plaintext highlighter-rouge">UIImage</code>, <code class="language-plaintext highlighter-rouge">NSData</code> or <code class="language-plaintext highlighter-rouge">NSString</code>, right within Xcode. Starting with Xcode 5.1, <code class="language-plaintext highlighter-rouge">UIView</code> and custom types can be quickly previewed as well, which can be quite convenient when debugging projects.</p> <p><img src="/images/quick_look_xcode5.jpg" alt="QuickLook" /></p> <p>The <a href="https://github.com/ryanolsonk/LLDB-QuickLook">LLDB-QuickLook</a> project adds a similar functionality to the LLDB Xcode console as well, but requires some categories to be added to each project you want to use LLDB-QuickLook with. This process is rather inconvenient since it requires you to edit your projects to make these categories somehow available (most probably by including the corresponding source files).</p> <p>Based on a <a href="http://blog.ittybittyapps.com/blog/2013/11/07/integrating-reveal-without-modifying-your-xcode-project/">trick</a> similar to the one used to link in <a href="http://revealapp.com/">Reveal</a> dynamic library, we can improve LLDB-QuickLook so that its categories can be used transparently with all your projects.</p> <p>Instead of adding the categories to the projects, the LLDB-QuickLook categories can namely be packaged as a dynamic library, and made available when LLDB starts. Within the iOS simulator, the library can be loaded without <code class="language-plaintext highlighter-rouge">dlopen</code>-ing it from the application, and without <a href="http://petersteinberger.com/blog/2013/how-to-inspect-the-view-hierarchy-of-3rd-party-apps/">jailbreaking</a>. This is why this post only focuses on the iOS simulator.</p> <p>The approach described below can be applied to your own LLDB debugging helpers, which can be packaged as a dynamic library shared between your projects.</p> <h2 id="enabling-ios-dynamic-library-compilation-in-xcode">Enabling iOS dynamic library compilation in Xcode</h2> <p>Apple does not officially provide a way to create dynamic libraries for iOS, since their use does not comply with App Store rules. Xcode unsurprisingly complains when we try to build one:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>target specifies product type 'com.apple.product-type.library.dynamic', but there's no such product type for the 'iphonesimulator' platform </code></pre></div></div> <p>You can still enable iOS dynamic library support by editing some Xcode configuration files, though, as described <a href="http://mysteri0uss.diandian.com/post/2013-06-06/40050450784">elsewhere</a>:</p> <ul> <li> <p>For the simulator, open the Mac OS specification file:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Xcode/Specifications/MacOSX Product Types.xcspec </code></pre></div> </div> <p>and copy over the section beginning with <code class="language-plaintext highlighter-rouge">com.apple.product-type.library.dynamic</code> to its iOS counterpart:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Specifications/iPhone Simulator ProductTypes.xcspec </code></pre></div> </div> </li> <li> <p>Proceed similarly with the following files:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Xcode/Specifications/MacOSX Package Types.xcspec /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Specifications/iPhone Simulator PackageTypes.xcspec </code></pre></div> </div> <p>and the section beginning with <code class="language-plaintext highlighter-rouge">com.apple.package-type.mach-o-dylib</code></p> </li> </ul> <p>Restart Xcode to take the changes into account.</p> <h2 id="building-an-lldb-quicklook-dynamic-library">Building an LLDB-Quicklook dynamic library</h2> <p>To build a dynamic library we create a Mac OS X Cocoa Library project, setting its type to <em>dynamic</em>. We remove all useless stuff, set its SDK to <em>Latest iOS</em> and add LLDB-QuickLook source files. The resulting project can be found on my <a href="https://github.com/defagos/LLDB-QuickLook/tree/dylib">Github page</a>, on a branch called <code class="language-plaintext highlighter-rouge">dylib</code>.</p> <p><img src="/images/creating_dylib.jpg" alt="dylib creation" /></p> <p>Checkout the repository, switch to the <code class="language-plaintext highlighter-rouge">dylib</code> branch and run the following command to build the library:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> xcodebuild -sdk iphonesimulator -configuration Release -project LLDB-QuickLook.xcodeproj </code></pre></div></div> <p>The dynamic library can be found in the <code class="language-plaintext highlighter-rouge">build</code> directory, and is called <code class="language-plaintext highlighter-rouge">LLDB-QuickLook.dylib</code>.</p> <h2 id="loading-the-lldb-quicklook-dynamic-library-into-lldb">Loading the LLDB-QuickLook dynamic library into LLDB</h2> <p>To load the <code class="language-plaintext highlighter-rouge">LLDB-QuickLook.dylib</code> dynamic library when LLDB starts, copy it somewhere (e.g. <code class="language-plaintext highlighter-rouge">~/Library/lldb/lib/LLDB-QuickLook.dylib</code>) and add the following line to your <code class="language-plaintext highlighter-rouge">~/.lldbinit</code> configuration file:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>command alias quick_look_load_sim process load ~/Library/lldb/lib/LLDB-QuickLook.dylib </code></pre></div></div> <p>replacing the path by the one you chose.</p> <p>The <code class="language-plaintext highlighter-rouge">quick_look_load_sim</code> command can be manually triggered in LLDB to load the library (when running in the iOS simulator), but cannot work when no process has been attached.</p> <p>To run the <code class="language-plaintext highlighter-rouge">quick_look_load_sim</code> command early when an application has been attached, add a user-defined symbolic breakpoint on a function which gets called early, e.g. <code class="language-plaintext highlighter-rouge">UIApplicationMain</code>. User-defined breakpoints are common to all you projects, and can trigger actions, like executing LLDB commands. To call our custom <code class="language-plaintext highlighter-rouge">quick_look_load_sim</code> command, add a <em>Debugger Command</em> action calling <code class="language-plaintext highlighter-rouge">quick_look_load_sim</code> to the breakpoint, and check <em>Automatically continue after evaluating</em>.</p> <p><img src="/images/breakpoints.jpg" alt="Breakpoints" /></p> <p>There is hope pending breakpoints <a href="http://prod.lists.apple.com/archives/xcode-users/2013/Feb/msg00069.html">might be added</a> to LLDB in the future, in which case this trick could be replaced with pending breakpoints defined in the <code class="language-plaintext highlighter-rouge">~/.lldbinit</code> file.</p> <h2 id="quicklooking">QuickLooking</h2> <p>Follow the <a href="https://github.com/ryanolsonk/LLDB-QuickLook/blob/master/README.md">LLDB-QuickLook installation steps</a> and add the following commands to your <code class="language-plaintext highlighter-rouge">~/.lldbinit</code> file:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>command script import ~/Library/lldb/lldb_quick_look.py command alias ql quicklook </code></pre></div></div> <p>Here I saved the Python script under <code class="language-plaintext highlighter-rouge">~/Library/lldb</code>.</p> <p>Now run any project in the simulator (be sure that the user-defined breakpoint is available), pause the execution, and try to enter a QuickLook LLDB command, e.g.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ql @"Hello, World!" </code></pre></div></div> <p>QuickLook should open and display the string.</p> <p><img src="/images/lldb_quick_look_example.jpg" alt="QuickLook example" /></p> <h2 id="wrapping-up">Wrapping up</h2> <p>This post discussed how debugging code can be packaged as a dynamic library, and how it can be automatically loaded when an application is debugged in the iOS simulator. By applying the same strategy, you could package your own LLDB debugging helpers as dynamic libraries which can be shared between your projects, without having to modify them. Have fun!</p>Xcode 5 added QuickLook support for standard types like UIImage, NSData or NSString, right within Xcode. Starting with Xcode 5.1, UIView and custom types can be quickly previewed as well, which can be quite convenient when debugging projects.