Jekyll2020-08-12T14:25:08+00:00https://jozsef-vesza.dev/feed.xmlJózsef VeszaThis blog covers varous topics in iOS development. Currently covering: Swift, SwiftUI, and Combine. Written by József Vesza.József Veszajozsef.vesza@gmail.comCombine Publishers, Part 3: Thead Safety2020-08-08T00:00:00+00:002020-08-08T00:00:00+00:00https://jozsef-vesza.dev/2020/08/08/thread-safe-publishers<p>In <a href="https://jozsef-vesza.dev/2020/07/24/creating-a-custom-combine-publisher/">Parts 1</a>, and <a href="https://jozsef-vesza.dev/2020/08/01/unit-testing-publishers/">2</a>, you’ve built a custom Publisher, and did your best to cover its rough edges with unit tests. Now it’s finally time to kick back and relax while it does its job, emitting playback progress. Or is it? Of course it will work well in most cases; but what happens if some threading issues are introduced? Let’s find out!</p>
<blockquote>
<p>I only ended up digging into this topic after <a href="https://www.reddit.com/r/iOSProgramming/comments/i1pmfd/combine_publishers_part_1_creating_a_publisher/g079tio/">discussing it on Reddit</a>. Credit goes to <a href="https://www.reddit.com/user/pstmail4757483">u/pstmail4757483</a> for being kind enough to talk me through it.</p>
</blockquote>
<h2 id="getting-started">Getting Started</h2>
<p>Combine’s documentation is a bit absent around thread safety, but after some digging it turns out that <a href="https://forums.swift.org/t/thread-safety-for-combine-publishers/29491/11">there are some rules</a> to follow:</p>
<blockquote>
<ol>
<li>A call to <code class="language-plaintext highlighter-rouge">receive(subscriber:)</code> can come from any thread</li>
<li>“Downstream” calls to Subscriber’s <code class="language-plaintext highlighter-rouge">receive(subscription:)</code>, <code class="language-plaintext highlighter-rouge">receive(_:)</code>, and <code class="language-plaintext highlighter-rouge">receive(completion:)</code> must be serialized (but may be on different threads)</li>
<li>“Upstream” calls to Subscription’s <code class="language-plaintext highlighter-rouge">request(_:)</code> and <code class="language-plaintext highlighter-rouge">cancel()</code> must be serialized (but may be on different threads)</li>
</ol>
</blockquote>
<p>Throughout this guide you’ll look at how these rules apply to the <code class="language-plaintext highlighter-rouge">PlayheadProgressPublisher</code> type you worked on in previous parts of the series.</p>
<p>Before going any further, enable <a href="http://clang.llvm.org/docs/ThreadSanitizer.html">Thread Sanitizer</a> in your test target. Whenever you run into concurrency bugs, it will highlight them for you. You can enable it in the scheme editor:</p>
<picture>
<source srcset="/assets/2020-08-08-thread-safe-publishers/tsan-enable-dark.png" media="(prefers-color-scheme: dark)" />
<img src="/assets/2020-08-08-thread-safe-publishers/tsan-enable.png" alt="Screenshot of enabling Thread Sanitizer for Tests via the Scheme Editor" />
</picture>
<h3 id="receiving-demand">Receiving Demand</h3>
<p>Let’s start with the most obvious scenario: the Publisher’s <code class="language-plaintext highlighter-rouge">request(_:)</code> method is called anytime to request more values. As noted in Rule #3 above, this call can come from any thread, so it’s worth examining. You’ll start by extracting parts of it into separate methods so it can be changed easier:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">request</span><span class="p">(</span><span class="n">_</span> <span class="nv">demand</span><span class="p">:</span> <span class="kt">Subscribers</span><span class="o">.</span><span class="kt">Demand</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">processDemand</span><span class="p">(</span><span class="n">demand</span><span class="p">)</span>
<span class="p">}</span>
<span class="kd">private</span> <span class="kd">func</span> <span class="nf">processDemand</span><span class="p">(</span><span class="n">_</span> <span class="nv">demand</span><span class="p">:</span> <span class="kt">Subscribers</span><span class="o">.</span><span class="kt">Demand</span><span class="p">)</span> <span class="p">{</span>
<span class="n">requested</span> <span class="o">+=</span> <span class="n">demand</span>
<span class="k">guard</span> <span class="n">timeObserverToken</span> <span class="o">==</span> <span class="kc">nil</span><span class="p">,</span> <span class="n">requested</span> <span class="o">></span> <span class="o">.</span><span class="k">none</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">interval</span> <span class="o">=</span> <span class="kt">CMTime</span><span class="p">(</span><span class="nv">seconds</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">interval</span><span class="p">,</span> <span class="nv">preferredTimescale</span><span class="p">:</span> <span class="kt">CMTimeScale</span><span class="p">(</span><span class="kt">NSEC_PER_SEC</span><span class="p">))</span>
<span class="n">timeObserverToken</span> <span class="o">=</span> <span class="n">player</span><span class="o">.</span><span class="nf">addPeriodicTimeObserver</span><span class="p">(</span><span class="nv">forInterval</span><span class="p">:</span> <span class="n">interval</span><span class="p">,</span> <span class="nv">queue</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="p">{</span> <span class="p">[</span><span class="k">weak</span> <span class="k">self</span><span class="p">]</span> <span class="n">time</span> <span class="k">in</span>
<span class="k">self</span><span class="p">?</span><span class="o">.</span><span class="nf">sendValue</span><span class="p">(</span><span class="n">time</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">sendValue</span><span class="p">(</span><span class="n">_</span> <span class="nv">time</span><span class="p">:</span> <span class="kt">CMTime</span><span class="p">)</span> <span class="p">{</span>
<span class="k">guard</span> <span class="k">let</span> <span class="nv">subscriber</span> <span class="o">=</span> <span class="n">subscriber</span><span class="p">,</span> <span class="n">requested</span> <span class="o">></span> <span class="o">.</span><span class="k">none</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
<span class="n">requested</span> <span class="o">-=</span> <span class="o">.</span><span class="nf">max</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">newDemand</span> <span class="o">=</span> <span class="n">subscriber</span><span class="o">.</span><span class="nf">receive</span><span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">seconds</span><span class="p">)</span>
<span class="n">requested</span> <span class="o">+=</span> <span class="n">newDemand</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Looking at the very first line of <code class="language-plaintext highlighter-rouge">processDemand(_:)</code>, it’s becoming clearer how it could be the subject of race conditions: it updates the <code class="language-plaintext highlighter-rouge">requested</code> property. If this method is called multiple times from separate threads, some updates might be overwritten.</p>
<p>You could simulate this scenario with the following unit test (<a href="https://github.com/jozsef-vesza/AVFoundation-Combine/blob/master/AVFoundation-CombineTests/TestSubscriber.swift">click</a> for the source code of <code class="language-plaintext highlighter-rouge">TestSubscriber</code> if you need a refresher):</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kd">func</span> <span class="nf">testWhenValuesAreRequestedFromMultipleThreads_RequestsAreSerialized</span><span class="p">()</span> <span class="p">{</span>
<span class="c1">// given</span>
<span class="k">let</span> <span class="nv">requestCount</span> <span class="o">=</span> <span class="mi">1000</span>
<span class="k">let</span> <span class="nv">expectation</span> <span class="o">=</span> <span class="kt">XCTestExpectation</span><span class="p">(</span><span class="nv">description</span><span class="p">:</span> <span class="s">"</span><span class="se">\(</span><span class="n">requestCount</span><span class="se">)</span><span class="s"> values should be received"</span><span class="p">)</span>
<span class="n">expectation</span><span class="o">.</span><span class="n">expectedFulfillmentCount</span> <span class="o">=</span> <span class="n">requestCount</span>
<span class="k">let</span> <span class="nv">subscriber</span> <span class="o">=</span> <span class="kt">TestSubscriber</span><span class="o"><</span><span class="kt">TimeInterval</span><span class="o">></span><span class="p">(</span><span class="nv">demand</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="n">_</span> <span class="k">in</span>
<span class="n">expectation</span><span class="o">.</span><span class="nf">fulfill</span><span class="p">()</span>
<span class="k">return</span> <span class="mi">0</span>
<span class="p">}</span>
<span class="n">sut</span><span class="o">.</span><span class="nf">subscribe</span><span class="p">(</span><span class="n">subscriber</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">group</span> <span class="o">=</span> <span class="kt">DispatchGroup</span><span class="p">()</span>
<span class="k">for</span> <span class="n">_</span> <span class="k">in</span> <span class="mi">0</span><span class="o">..<</span><span class="n">requestCount</span> <span class="p">{</span>
<span class="n">group</span><span class="o">.</span><span class="nf">enter</span><span class="p">()</span>
<span class="kt">DispatchQueue</span><span class="o">.</span><span class="nf">global</span><span class="p">()</span><span class="o">.</span><span class="n">async</span> <span class="p">{</span>
<span class="n">subscriber</span><span class="o">.</span><span class="nf">startRequestingValues</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="n">group</span><span class="o">.</span><span class="nf">leave</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="n">_</span> <span class="o">=</span> <span class="n">group</span><span class="o">.</span><span class="nf">wait</span><span class="p">(</span><span class="nv">timeout</span><span class="p">:</span> <span class="kt">DispatchTime</span><span class="o">.</span><span class="nf">now</span><span class="p">()</span> <span class="o">+</span> <span class="mi">5</span><span class="p">)</span>
<span class="c1">// when</span>
<span class="p">(</span><span class="mi">1</span><span class="o">...</span><span class="n">requestCount</span><span class="p">)</span><span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="kt">TimeInterval</span><span class="p">(</span><span class="nv">$0</span><span class="p">)</span> <span class="p">}</span><span class="o">.</span><span class="n">forEach</span> <span class="p">{</span> <span class="n">time</span> <span class="k">in</span>
<span class="n">player</span><span class="o">.</span><span class="nf">updateClosure</span><span class="p">?(</span><span class="kt">CMTime</span><span class="p">(</span><span class="nv">seconds</span><span class="p">:</span> <span class="n">time</span><span class="p">,</span> <span class="nv">preferredTimescale</span><span class="p">:</span> <span class="kt">CMTimeScale</span><span class="p">(</span><span class="kt">NSEC_PER_SEC</span><span class="p">)))</span>
<span class="p">}</span>
<span class="c1">// then</span>
<span class="nf">wait</span><span class="p">(</span><span class="nv">for</span><span class="p">:</span> <span class="p">[</span><span class="n">expectation</span><span class="p">],</span> <span class="nv">timeout</span><span class="p">:</span> <span class="mi">1</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>The test will request 1000 values, all from different threads. Then, 1000 mock values are served. If the Publisher worked correctly, it would be reasonable to expect that all 1000 requests are registered, and the values are served.</p>
<p>In reality however, it will mostly work, but sometimes it won’t: since access to the Subscription’s <code class="language-plaintext highlighter-rouge">requested</code> property is not protected in any way, the demand may end up being less than 1000 due to data races.</p>
<p>Running this test with the Thread Sanitizer enabled will immediately flag the issue:</p>
<picture>
<source srcset="/assets/2020-08-08-thread-safe-publishers/race1-dark.png" media="(prefers-color-scheme: dark)" />
<img src="/assets/2020-08-08-thread-safe-publishers/race1.png" alt="Screenshot showing how there's a data race when the requested property is accessed from separate threads" />
</picture>
<p>A possible way to solve this issue is to introduce a serial queue to make sure that demand requests are handled in order:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">final</span> <span class="kd">class</span> <span class="kt">PlayheadProgressSubscription</span><span class="o"><</span><span class="kt">S</span><span class="p">:</span> <span class="kt">Subscriber</span><span class="o">></span><span class="p">:</span> <span class="kt">Subscription</span> <span class="k">where</span> <span class="kt">S</span><span class="o">.</span><span class="kt">Input</span> <span class="o">==</span> <span class="kt">TimeInterval</span> <span class="p">{</span>
<span class="kd">private</span> <span class="k">let</span> <span class="nv">queue</span> <span class="o">=</span> <span class="kt">DispatchQueue</span><span class="p">(</span><span class="nv">label</span><span class="p">:</span> <span class="s">"PlayheadProgressSubscription.serial"</span><span class="p">)</span>
<span class="o">...</span>
<span class="kd">func</span> <span class="nf">request</span><span class="p">(</span><span class="n">_</span> <span class="nv">demand</span><span class="p">:</span> <span class="kt">Subscribers</span><span class="o">.</span><span class="kt">Demand</span><span class="p">)</span> <span class="p">{</span>
<span class="n">queue</span><span class="o">.</span><span class="n">sync</span> <span class="p">{</span>
<span class="nf">processDemand</span><span class="p">(</span><span class="n">demand</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="o">...</span>
<span class="p">}</span>
</code></pre></div></div>
<p>If you re-run the test, you’ll notice that the data race warning is gone.</p>
<h3 id="sending-values">Sending Values</h3>
<p>One issue down, but you’re not quite done yet. If you recall, whenever a value is emitted, the Subscriber gets a chance to update the demand.</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">func</span> <span class="nf">sendValue</span><span class="p">(</span><span class="n">_</span> <span class="nv">time</span><span class="p">:</span> <span class="kt">CMTime</span><span class="p">)</span> <span class="p">{</span>
<span class="k">guard</span> <span class="k">let</span> <span class="nv">subscriber</span> <span class="o">=</span> <span class="n">subscriber</span><span class="p">,</span> <span class="n">requested</span> <span class="o">></span> <span class="o">.</span><span class="k">none</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
<span class="n">requested</span> <span class="o">-=</span> <span class="o">.</span><span class="nf">max</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">newDemand</span> <span class="o">=</span> <span class="n">subscriber</span><span class="o">.</span><span class="nf">receive</span><span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">seconds</span><span class="p">)</span>
<span class="n">requested</span> <span class="o">+=</span> <span class="n">newDemand</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Progress updates from AVPlayer are currently served on the main thread, which means that <code class="language-plaintext highlighter-rouge">requested</code> is still unsafe: it can be updated from the private serial queue, and the main thread as well. To catch that, let’s create another test:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kd">func</span> <span class="nf">testWhenRequestAndDemandUpdateAreSentFromDifferentThreads_UpdatesAreSerialized</span><span class="p">()</span> <span class="p">{</span>
<span class="c1">// given</span>
<span class="k">let</span> <span class="nv">expectation</span> <span class="o">=</span> <span class="kt">XCTestExpectation</span><span class="p">(</span><span class="nv">description</span><span class="p">:</span> <span class="s">"1 value should be received"</span><span class="p">)</span>
<span class="n">expectation</span><span class="o">.</span><span class="n">expectedFulfillmentCount</span> <span class="o">=</span> <span class="mi">1</span>
<span class="k">let</span> <span class="nv">subscriber</span> <span class="o">=</span> <span class="kt">TestSubscriber</span><span class="o"><</span><span class="kt">TimeInterval</span><span class="o">></span><span class="p">(</span><span class="nv">demand</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="n">_</span> <span class="k">in</span>
<span class="n">expectation</span><span class="o">.</span><span class="nf">fulfill</span><span class="p">()</span>
<span class="k">return</span> <span class="mi">0</span>
<span class="p">}</span>
<span class="n">sut</span><span class="o">.</span><span class="nf">subscribe</span><span class="p">(</span><span class="n">subscriber</span><span class="p">)</span>
<span class="n">subscriber</span><span class="o">.</span><span class="nf">startRequestingValues</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">group</span> <span class="o">=</span> <span class="kt">DispatchGroup</span><span class="p">()</span>
<span class="n">group</span><span class="o">.</span><span class="nf">enter</span><span class="p">()</span>
<span class="kt">DispatchQueue</span><span class="o">.</span><span class="nf">global</span><span class="p">()</span><span class="o">.</span><span class="n">async</span> <span class="p">{</span>
<span class="n">subscriber</span><span class="o">.</span><span class="nf">startRequestingValues</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="n">group</span><span class="o">.</span><span class="nf">leave</span><span class="p">()</span>
<span class="p">}</span>
<span class="n">player</span><span class="o">.</span><span class="nf">updateClosure</span><span class="p">?(</span><span class="kt">CMTime</span><span class="p">(</span><span class="nv">seconds</span><span class="p">:</span> <span class="kt">TimeInterval</span><span class="p">(</span><span class="mi">1</span><span class="p">),</span> <span class="nv">preferredTimescale</span><span class="p">:</span> <span class="kt">CMTimeScale</span><span class="p">(</span><span class="kt">NSEC_PER_SEC</span><span class="p">)))</span>
<span class="n">_</span> <span class="o">=</span> <span class="n">group</span><span class="o">.</span><span class="nf">wait</span><span class="p">(</span><span class="nv">timeout</span><span class="p">:</span> <span class="kt">DispatchTime</span><span class="o">.</span><span class="nf">now</span><span class="p">()</span> <span class="o">+</span> <span class="mi">5</span><span class="p">)</span>
<span class="c1">// then</span>
<span class="nf">wait</span><span class="p">(</span><span class="nv">for</span><span class="p">:</span> <span class="p">[</span><span class="n">expectation</span><span class="p">],</span> <span class="nv">timeout</span><span class="p">:</span> <span class="mi">1</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>As a logic test, this is not super useful, but it creates just enough chaos to simulate a race condition: a demand is dispatched from a global queue, and a mock value is sent from the main thread. Running it will trigger Thread Sanitizer most of the time:</p>
<picture>
<source srcset="/assets/2020-08-08-thread-safe-publishers/race2-dark.png" media="(prefers-color-scheme: dark)" />
<img src="/assets/2020-08-08-thread-safe-publishers/race2.png" alt="Screenshot showing how the requested property is accessed from both the private and the main queues" />
</picture>
<p>You’ll notice that both Thread 1 and 11 are attempting to modify <code class="language-plaintext highlighter-rouge">requested</code>, confirming a race condition. At first it might seem like a good idea to use the same queue in the <code class="language-plaintext highlighter-rouge">sendValue(_:)</code> method:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">func</span> <span class="nf">sendValue</span><span class="p">(</span><span class="n">_</span> <span class="nv">time</span><span class="p">:</span> <span class="kt">CMTime</span><span class="p">)</span> <span class="p">{</span>
<span class="n">queue</span><span class="o">.</span><span class="n">sync</span> <span class="p">{</span>
<span class="k">guard</span> <span class="k">let</span> <span class="nv">subscriber</span> <span class="o">=</span> <span class="n">subscriber</span><span class="p">,</span> <span class="n">requested</span> <span class="o">></span> <span class="o">.</span><span class="k">none</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
<span class="n">requested</span> <span class="o">-=</span> <span class="o">.</span><span class="nf">max</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">newDemand</span> <span class="o">=</span> <span class="n">subscriber</span><span class="o">.</span><span class="nf">receive</span><span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">seconds</span><span class="p">)</span>
<span class="n">requested</span> <span class="o">+=</span> <span class="n">newDemand</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>However doing this can produce a deadlock. Again, this is a contrived example, but let’s suppose that the Subscriber invokes <code class="language-plaintext highlighter-rouge">request(_:)</code> immediately upon receiving a value:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">receive</span><span class="p">(</span><span class="n">_</span> <span class="nv">input</span><span class="p">:</span> <span class="kt">T</span><span class="p">)</span> <span class="o">-></span> <span class="kt">Subscribers</span><span class="o">.</span><span class="kt">Demand</span> <span class="p">{</span>
<span class="n">subscription</span><span class="p">?</span><span class="o">.</span><span class="nf">request</span><span class="p">(</span><span class="o">.</span><span class="nf">max</span><span class="p">(</span><span class="mi">1</span><span class="p">))</span>
<span class="k">return</span> <span class="o">.</span><span class="k">none</span>
<span class="p">}</span>
</code></pre></div></div>
<p>This will produce a deadlock, as the method will have to wait until the queue (currently also used for delvering the value) is freed up. A quick fix-it is to use <code class="language-plaintext highlighter-rouge">queue.async</code> instead of <code class="language-plaintext highlighter-rouge">sync</code>, to prevent the queues from blocking.</p>
<p>Instead of going async, another option is to use a recursive lock: <code class="language-plaintext highlighter-rouge">NSRecursiveLock</code> provides exclusive access, but also prevents simultaneous requests (like the one described above) from deadlocking. Also, it allows the implementation to remain synchronous.</p>
<p>To adopt this locking behavior, let’s introduce a few changes to <code class="language-plaintext highlighter-rouge">PlayheadProgressSubscriber</code>:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">final</span> <span class="kd">class</span> <span class="kt">PlayheadProgressSubscription</span><span class="o"><</span><span class="kt">S</span><span class="p">:</span> <span class="kt">Subscriber</span><span class="o">></span><span class="p">:</span> <span class="kt">Subscription</span> <span class="k">where</span> <span class="kt">S</span><span class="o">.</span><span class="kt">Input</span> <span class="o">==</span> <span class="kt">TimeInterval</span> <span class="p">{</span>
<span class="o">...</span>
<span class="kd">private</span> <span class="k">let</span> <span class="nv">lock</span> <span class="o">=</span> <span class="kt">NSRecursiveLock</span><span class="p">()</span>
<span class="kd">private</span> <span class="kd">func</span> <span class="nf">withLock</span><span class="p">(</span><span class="n">_</span> <span class="nv">operation</span><span class="p">:</span> <span class="p">()</span> <span class="o">-></span> <span class="kt">Void</span><span class="p">)</span> <span class="p">{</span>
<span class="n">lock</span><span class="o">.</span><span class="nf">lock</span><span class="p">()</span>
<span class="k">defer</span> <span class="p">{</span> <span class="n">lock</span><span class="o">.</span><span class="nf">unlock</span><span class="p">()</span> <span class="p">}</span>
<span class="nf">operation</span><span class="p">()</span>
<span class="p">}</span>
<span class="o">...</span>
<span class="p">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">withLock(_:)</code> will save you from doing the lock/unlock dance each time. Now you can update <code class="language-plaintext highlighter-rouge">request(_:)</code> and <code class="language-plaintext highlighter-rouge">sendValue(_:)</code>:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">request</span><span class="p">(</span><span class="n">_</span> <span class="nv">demand</span><span class="p">:</span> <span class="kt">Subscribers</span><span class="o">.</span><span class="kt">Demand</span><span class="p">)</span> <span class="p">{</span>
<span class="n">withLock</span> <span class="p">{</span>
<span class="nf">processDemand</span><span class="p">(</span><span class="n">demand</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">sendValue</span><span class="p">(</span><span class="n">_</span> <span class="nv">time</span><span class="p">:</span> <span class="kt">CMTime</span><span class="p">)</span> <span class="p">{</span>
<span class="n">withLock</span> <span class="p">{</span>
<span class="k">guard</span> <span class="k">let</span> <span class="nv">subscriber</span> <span class="o">=</span> <span class="n">subscriber</span><span class="p">,</span> <span class="n">requested</span> <span class="o">></span> <span class="o">.</span><span class="k">none</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
<span class="n">requested</span> <span class="o">-=</span> <span class="o">.</span><span class="nf">max</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">newDemand</span> <span class="o">=</span> <span class="n">subscriber</span><span class="o">.</span><span class="nf">receive</span><span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">seconds</span><span class="p">)</span>
<span class="n">requested</span> <span class="o">+=</span> <span class="n">newDemand</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>If you run the test suite now, you’ll see that no data races occur.</p>
<h3 id="cancellation">Cancellation</h3>
<p>The thread safety rules outlined above mention that calls to <code class="language-plaintext highlighter-rouge">cancel()</code> should also be serialized. In fact, the <a href="https://developer.apple.com/documentation/combine/subscription">documentation</a> for Subscription also mentions that cancelling must be thread-safe</p>
<p>Let’s look at the implementation to see why:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">func</span> <span class="nf">sendValue</span><span class="p">(</span><span class="n">_</span> <span class="nv">time</span><span class="p">:</span> <span class="kt">CMTime</span><span class="p">)</span> <span class="p">{</span>
<span class="k">guard</span> <span class="k">let</span> <span class="nv">subscriber</span> <span class="o">=</span> <span class="n">subscriber</span><span class="p">,</span> <span class="n">requested</span> <span class="o">></span> <span class="o">.</span><span class="k">none</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
<span class="n">requested</span> <span class="o">-=</span> <span class="o">.</span><span class="nf">max</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">newDemand</span> <span class="o">=</span> <span class="n">subscriber</span><span class="o">.</span><span class="nf">receive</span><span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">seconds</span><span class="p">)</span>
<span class="n">requested</span> <span class="o">+=</span> <span class="n">newDemand</span>
<span class="p">}</span>
<span class="kd">func</span> <span class="nf">cancel</span><span class="p">()</span> <span class="p">{</span>
<span class="k">if</span> <span class="k">let</span> <span class="nv">timeObserverToken</span> <span class="o">=</span> <span class="n">timeObserverToken</span> <span class="p">{</span>
<span class="n">player</span><span class="o">.</span><span class="nf">removeTimeObserver</span><span class="p">(</span><span class="n">timeObserverToken</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">timeObserverToken</span> <span class="o">=</span> <span class="kc">nil</span>
<span class="n">subscriber</span> <span class="o">=</span> <span class="kc">nil</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Notice, how the <code class="language-plaintext highlighter-rouge">subscriber</code> is modified in <code class="language-plaintext highlighter-rouge">cancel()</code>, while also read in <code class="language-plaintext highlighter-rouge">sendValue(_:)</code>. There’s no good way to consistently reproduce it, but this setup also may also result in a race condition. So just to be on the safe side, let’s follow the rules, and apply locking on cancellation as well.</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">cancel</span><span class="p">()</span> <span class="p">{</span>
<span class="n">withLock</span> <span class="p">{</span>
<span class="k">if</span> <span class="k">let</span> <span class="nv">timeObserverToken</span> <span class="o">=</span> <span class="n">timeObserverToken</span> <span class="p">{</span>
<span class="n">player</span><span class="o">.</span><span class="nf">removeTimeObserver</span><span class="p">(</span><span class="n">timeObserverToken</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">timeObserverToken</span> <span class="o">=</span> <span class="kc">nil</span>
<span class="n">subscriber</span> <span class="o">=</span> <span class="kc">nil</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="conclusion">Conclusion</h3>
<p>With the locks in place, you ensured that that <code class="language-plaintext highlighter-rouge">processDemand(_:)</code>, <code class="language-plaintext highlighter-rouge">sendValue(_:)</code>, and <code class="language-plaintext highlighter-rouge">cancel()</code> cannot run at the same time, thus making sure that <code class="language-plaintext highlighter-rouge">requested</code>, and <code class="language-plaintext highlighter-rouge">subscriber</code> is only accessed by one thread.</p>
<p>As you can see, it’s not easy to come up with reliable tests for race conditions; but just because you can’t reproduce them, it doesn’t mean that they can’t surprise you with obscure, hard to debug crashes in production. It’s better to be on the safe side, and apply defensive locking.</p>
<p>To finish up, I’d like to highlight a few resources I found useful while researching:</p>
<ul>
<li><a href="https://www.cocoawithlove.com/blog/twenty-two-short-tests-of-combine-part-3.html">22 short tests of combine – Part 3: Asynchrony</a></li>
<li><a href="https://developer.apple.com/videos/play/wwdc2016/412/">Thread Sanitizer and Static Analysis</a></li>
</ul>
<p>For more details, feel free to check out <a href="https://github.com/jozsef-vesza/AVFoundation-Combine">the full project on GitHub</a>. Also, please send any questions or feedback on <a href="https://twitter.com/j_vesza">Twitter</a>. This post wouldn’t exist if it wasn’t for reader feedback. Thank you for reading!</p>József Veszajozsef.vesza@gmail.comIn Parts 1, and 2, you’ve built a custom Publisher, and did your best to cover its rough edges with unit tests. Now it’s finally time to kick back and relax while it does its job, emitting playback progress. Or is it? Of course it will work well in most cases; but what happens if some threading issues are introduced? Let’s find out!Combine Publishers, Part 2: Unit Testing Custom Publishers2020-08-01T00:00:00+00:002020-08-01T00:00:00+00:00https://jozsef-vesza.dev/2020/08/01/unit-testing-publishers<p>In <a href="https://jozsef-vesza.dev/2020/07/24/creating-a-custom-combine-publisher/">Part 1</a> of the series, you’ve built a Publisher from scratch. One of the key parts of Combine’s event delivery is keeping up with the Subscriber’s demand. It involves some manual bookkeeping, which is known to be error-prone, so in this guide you’ll learn how to ensure that your custom Publisher is not misbehaving.</p>
<h2 id="getting-started">Getting Started</h2>
<p>Let’s have a quick look at the current Subscription implementation:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">request</span><span class="p">(</span><span class="n">_</span> <span class="nv">demand</span><span class="p">:</span> <span class="kt">Subscribers</span><span class="o">.</span><span class="kt">Demand</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// 1.</span>
<span class="n">requested</span> <span class="o">+=</span> <span class="n">demand</span>
<span class="k">guard</span> <span class="n">timeObserverToken</span> <span class="o">==</span> <span class="kc">nil</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">interval</span> <span class="o">=</span> <span class="kt">CMTime</span><span class="p">(</span><span class="nv">seconds</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">interval</span><span class="p">,</span> <span class="nv">preferredTimescale</span><span class="p">:</span> <span class="kt">CMTimeScale</span><span class="p">(</span><span class="kt">NSEC_PER_SEC</span><span class="p">))</span>
<span class="n">timeObserverToken</span> <span class="o">=</span> <span class="n">player</span><span class="o">.</span><span class="nf">addPeriodicTimeObserver</span><span class="p">(</span><span class="nv">forInterval</span><span class="p">:</span> <span class="n">interval</span><span class="p">,</span> <span class="nv">queue</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="p">{</span> <span class="p">[</span><span class="k">weak</span> <span class="k">self</span><span class="p">]</span> <span class="n">time</span> <span class="k">in</span>
<span class="c1">// 2.</span>
<span class="k">guard</span>
<span class="k">let</span> <span class="nv">self</span> <span class="o">=</span> <span class="k">self</span><span class="p">,</span>
<span class="k">let</span> <span class="nv">subscriber</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="n">subscriber</span><span class="p">,</span>
<span class="k">self</span><span class="o">.</span><span class="n">requested</span> <span class="o">></span> <span class="o">.</span><span class="k">none</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
<span class="c1">// 3.</span>
<span class="k">self</span><span class="o">.</span><span class="n">requested</span> <span class="o">-=</span> <span class="o">.</span><span class="nf">max</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">newDemand</span> <span class="o">=</span> <span class="n">subscriber</span><span class="o">.</span><span class="nf">receive</span><span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">seconds</span><span class="p">)</span>
<span class="c1">// 4.</span>
<span class="k">self</span><span class="o">.</span><span class="n">requested</span> <span class="o">+=</span> <span class="n">newDemand</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Looking at the marked lines, it’s becoming clear that the responsibility of coping with the demand falls entirely on the Subscription implementation:</p>
<ol>
<li>First, it registers the initial demand.</li>
<li>When a value is sent to the Subscriber, it first checks if there’a a demand to fulfill.</li>
<li>Then, it updates the demand to avoid sending too many values.</li>
<li>The Subscriber may choose to increment the demand, so the Subscription updates the value stored in <code class="language-plaintext highlighter-rouge">requested</code> to keep up.</li>
</ol>
<p>To cover all theses cases, you could divide the tests into the following categories:</p>
<ul>
<li>Tests, that work with unlimited demand: The most common use case for Publishers is to subscribe using <code class="language-plaintext highlighter-rouge">sink(receiveCompletion:receiveValue:)</code>, and handle values as they are emitted. So you’ll have to make sure that your Publisher works well with this setup.</li>
<li>Tests, that work with a fixed demand: it’s fairly common that a Subscriber wants to limit the number of values emitted by a Publisher. Your tests need to verify that the Publisher emits the correct amount of values.</li>
<li>Tests, that dynamically change the demand: a more advanced scenario is to update the demand during a Subscription’s lifetime. This can be useful for heavier tasks, where the Publisher emits so many values that the Subscriber has a hard time keeping up. In this guide you’ll create tests for the case where the demand is initially zero, but later increased.</li>
</ul>
<h2 id="setting-up-the-tests">Setting Up the Tests</h2>
<p><code class="language-plaintext highlighter-rouge">PlayheadProgressPublisher</code> relies on AVPlayer to provide progress updates. In order to test its behavior properly, you need to be able to take control over these updates. Add the following implementation to your unit test case:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="kt">MockAVPlayer</span><span class="p">:</span> <span class="kt">AVPlayer</span> <span class="p">{</span>
<span class="k">var</span> <span class="nv">updateClosure</span><span class="p">:</span> <span class="p">((</span><span class="n">_</span> <span class="nv">time</span><span class="p">:</span> <span class="kt">CMTime</span><span class="p">)</span> <span class="o">-></span> <span class="kt">Void</span><span class="p">)?</span>
<span class="c1">// 1.</span>
<span class="k">override</span> <span class="kd">func</span> <span class="nf">addPeriodicTimeObserver</span><span class="p">(</span><span class="n">forInterval</span> <span class="nv">interval</span><span class="p">:</span> <span class="kt">CMTime</span><span class="p">,</span>
<span class="nv">queue</span><span class="p">:</span> <span class="kt">DispatchQueue</span><span class="p">?,</span>
<span class="n">using</span> <span class="nv">block</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="kt">CMTime</span><span class="p">)</span> <span class="o">-></span> <span class="kt">Void</span><span class="p">)</span> <span class="o">-></span> <span class="kt">Any</span> <span class="p">{</span>
<span class="c1">// 2.</span>
<span class="n">updateClosure</span> <span class="o">=</span> <span class="n">block</span>
<span class="c1">// 3.</span>
<span class="k">return</span> <span class="k">super</span><span class="o">.</span><span class="nf">addPeriodicTimeObserver</span><span class="p">(</span><span class="nv">forInterval</span><span class="p">:</span> <span class="n">interval</span><span class="p">,</span>
<span class="nv">queue</span><span class="p">:</span> <span class="n">queue</span><span class="p">,</span>
<span class="nv">using</span><span class="p">:</span> <span class="n">block</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Let’s look at how it works:</p>
<ol>
<li>You’ll hook into the AVPlayer’s <code class="language-plaintext highlighter-rouge">addPeriodicTimeObserver(forInterval:queue:using:)</code> method. <code class="language-plaintext highlighter-rouge">PlayheadProgressPublisher</code> invokes this method when subscribed to, so you can be sure that the hook will be executed.</li>
<li>The default implementation invokes the <code class="language-plaintext highlighter-rouge">block</code> parameter periodically to post updates. Your mock captures this closure into a property, so you can freely invoke it with arbitrary values in your tests.</li>
<li><code class="language-plaintext highlighter-rouge">AVPlayer</code> returns an observer token, which is later used to stop the progress updates. Since it’s an opaque value, it’s better to invoke the default implementation to make sure it’s correct.</li>
</ol>
<p>With the mock in place, it’s time to add some setup code to your test case:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="kt">PlayheadProgressPublisherTests</span><span class="p">:</span> <span class="kt">XCTestCase</span> <span class="p">{</span>
<span class="c1">// 1.</span>
<span class="k">var</span> <span class="nv">sut</span><span class="p">:</span> <span class="kt">Publishers</span><span class="o">.</span><span class="kt">PlayheadProgressPublisher</span><span class="o">!</span>
<span class="k">var</span> <span class="nv">player</span><span class="p">:</span> <span class="kt">MockAVPlayer</span><span class="o">!</span>
<span class="k">var</span> <span class="nv">subscriptions</span> <span class="o">=</span> <span class="kt">Set</span><span class="o"><</span><span class="kt">AnyCancellable</span><span class="o">></span><span class="p">()</span>
<span class="c1">// 2.</span>
<span class="k">override</span> <span class="kd">func</span> <span class="nf">setUp</span><span class="p">()</span> <span class="p">{</span>
<span class="n">player</span> <span class="o">=</span> <span class="kt">MockAVPlayer</span><span class="p">()</span>
<span class="n">sut</span> <span class="o">=</span> <span class="kt">Publishers</span><span class="o">.</span><span class="kt">PlayheadProgressPublisher</span><span class="p">(</span><span class="nv">player</span><span class="p">:</span> <span class="n">player</span><span class="p">)</span>
<span class="p">}</span>
<span class="c1">// 3.</span>
<span class="k">override</span> <span class="kd">func</span> <span class="nf">tearDown</span><span class="p">()</span> <span class="p">{</span>
<span class="n">subscriptions</span> <span class="o">=</span> <span class="p">[]</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<ol>
<li>You’ll keep references to the Publisher (“sut” is an abbreviation for “system under test”), and the mock AVPlayer implementation. Additionally there’s a <code class="language-plaintext highlighter-rouge">subscriptions</code> set which will retain the subscriptions created in test cases.</li>
<li>You inject the mock AVPlayer implementation into the Publisher.</li>
<li>Each test should start with a clean slate: so once one is finished, it’s good to get rid of the subscriptions.</li>
</ol>
<p>Now that the setup is done, it’s time to write your first test.</p>
<h2 id="testing-with-sink">Testing With Sink</h2>
<p>Add the following test method to your test case:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">testWhenDemandIsUnlimited_AndTimeIsUpdated_ItEmitsTheNewTime</span><span class="p">()</span> <span class="p">{</span>
<span class="c1">// 1.</span>
<span class="k">var</span> <span class="nv">receivedTimes</span><span class="p">:</span> <span class="p">[</span><span class="kt">TimeInterval</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">let</span> <span class="nv">expectedTimes</span><span class="p">:</span> <span class="p">[</span><span class="kt">TimeInterval</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">]</span>
<span class="c1">// 2.</span>
<span class="n">sut</span><span class="o">.</span><span class="n">sink</span> <span class="p">{</span> <span class="n">time</span> <span class="k">in</span>
<span class="n">receivedTimes</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="n">time</span><span class="p">)</span>
<span class="p">}</span>
<span class="o">.</span><span class="nf">store</span><span class="p">(</span><span class="nv">in</span><span class="p">:</span> <span class="o">&</span><span class="n">subscriptions</span><span class="p">)</span>
<span class="c1">// 3.</span>
<span class="k">let</span> <span class="nv">progress</span> <span class="o">=</span> <span class="kt">CMTime</span><span class="p">(</span><span class="nv">seconds</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nv">preferredTimescale</span><span class="p">:</span> <span class="kt">CMTimeScale</span><span class="p">(</span><span class="kt">NSEC_PER_SEC</span><span class="p">))</span>
<span class="n">player</span><span class="o">.</span><span class="nf">updateClosure</span><span class="p">?(</span><span class="n">progress</span><span class="p">)</span>
<span class="c1">// 4.</span>
<span class="kt">XCTAssertEqual</span><span class="p">(</span><span class="n">receivedTimes</span><span class="p">,</span> <span class="n">expectedTimes</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>This unit test represents the most common use case of a Publisher:</p>
<ol>
<li><code class="language-plaintext highlighter-rouge">receivedTimes</code> will hold the values received from the Publisher. <code class="language-plaintext highlighter-rouge">expectedTimes</code> declares the expected output.</li>
<li>You’ll use <code class="language-plaintext highlighter-rouge">sink(receiveValue:)</code> to kick off the Publisher. You’ll record each received value.</li>
<li>AVPlayer represents progress with <code class="language-plaintext highlighter-rouge">CMTime</code>. Here you’ll wrap the progress value of one second, and post it as the update.</li>
<li>Here you’ll simply check if the received value matches the expectation.</li>
</ol>
<p>If all goes well, this test should succeed. And with that implementation you’ve covered the majority of the use cases! Now it’s time to look into something a bit more complex.</p>
<h2 id="implementing-a-subscriber">Implementing a Subscriber</h2>
<p>In order to gain control over the demand, a custom Subscriber implementation is needed.</p>
<p>To get started, add the following declaration:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="kt">TestSubscriber</span><span class="p">:</span> <span class="kt">Subscriber</span> <span class="p">{</span>
<span class="kd">typealias</span> <span class="kt">Input</span> <span class="o">=</span> <span class="kt">TimeInterval</span>
<span class="kd">typealias</span> <span class="kt">Failure</span> <span class="o">=</span> <span class="kt">Never</span>
<span class="p">}</span>
</code></pre></div></div>
<p>This bit just declares the types of values and errors you’re interested in: it must match the Publisher’s emitted types, so you’ll use <code class="language-plaintext highlighter-rouge">TimeInterval</code> and <code class="language-plaintext highlighter-rouge">Never</code>. You’ll notice a few build errors now; Xcode’s fix-its to show the path forward. After applying them, you should see three new method stubs:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="kt">TestSubscriber</span><span class="p">:</span> <span class="kt">Subscriber</span> <span class="p">{</span>
<span class="kd">typealias</span> <span class="kt">Input</span> <span class="o">=</span> <span class="kt">TimeInterval</span>
<span class="kd">typealias</span> <span class="kt">Failure</span> <span class="o">=</span> <span class="kt">Never</span>
<span class="kd">func</span> <span class="nf">receive</span><span class="p">(</span><span class="nv">subscription</span><span class="p">:</span> <span class="kt">Subscription</span><span class="p">)</span> <span class="p">{</span>
<span class="p">}</span>
<span class="kd">func</span> <span class="nf">receive</span><span class="p">(</span><span class="n">_</span> <span class="nv">input</span><span class="p">:</span> <span class="kt">TimeInterval</span><span class="p">)</span> <span class="o">-></span> <span class="kt">Subscribers</span><span class="o">.</span><span class="kt">Demand</span> <span class="p">{</span>
<span class="p">}</span>
<span class="kd">func</span> <span class="nf">receive</span><span class="p">(</span><span class="nv">completion</span><span class="p">:</span> <span class="kt">Subscribers</span><span class="o">.</span><span class="kt">Completion</span><span class="o"><</span><span class="kt">Never</span><span class="o">></span><span class="p">)</span> <span class="p">{</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Before moving on, add the following properties to <code class="language-plaintext highlighter-rouge">TestSubscriber</code>:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="kt">TestSubscriber</span><span class="p">:</span> <span class="kt">Subscriber</span> <span class="p">{</span>
<span class="kd">private</span> <span class="k">let</span> <span class="nv">demand</span><span class="p">:</span> <span class="kt">Int</span>
<span class="kd">private</span> <span class="k">let</span> <span class="nv">onValueReceived</span><span class="p">:</span> <span class="p">(</span><span class="kt">Int</span><span class="p">)</span> <span class="o">-></span> <span class="kt">Void</span>
<span class="kd">private</span> <span class="k">var</span> <span class="nv">subscription</span><span class="p">:</span> <span class="kt">Subscription</span><span class="p">?</span> <span class="o">=</span> <span class="kc">nil</span>
<span class="nf">init</span><span class="p">(</span><span class="nv">demand</span><span class="p">:</span> <span class="kt">Int</span><span class="p">,</span> <span class="nv">onValueReceived</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="n">_</span> <span class="nv">receivedValue</span><span class="p">:</span> <span class="kt">Int</span><span class="p">)</span> <span class="o">-></span> <span class="kt">Void</span><span class="p">)</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">demand</span> <span class="o">=</span> <span class="n">demand</span>
<span class="k">self</span><span class="o">.</span><span class="n">receivedValues</span> <span class="o">=</span> <span class="n">receivedValues</span>
<span class="p">}</span>
<span class="o">...</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Let’s look at the properties one by one:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">demand</code>: the purpose of this Subscriber implementation is to gain control over the demand, so it will receive the demand as an <code class="language-plaintext highlighter-rouge">init</code> parameter.</li>
<li><code class="language-plaintext highlighter-rouge">onValueReceived</code>: this closure will be invoked as values are received from the Publisher.</li>
<li><code class="language-plaintext highlighter-rouge">subscription</code>: the Subscriber needs to hold a strong reference to the Subscription to prevent it from being deallocated.</li>
</ul>
<p>The next part will build heavily on the sequence of events described in <a href="https://jozsef-vesza.dev/2020/07/24/creating-a-custom-combine-publisher/">Part 1</a>, feel free to check it out if you need a refresher. Let’s look at the method stubs.</p>
<h3 id="receiving-the-subscribtion">Receiving the Subscribtion</h3>
<p>Once the Publisher has configured the Subscription, it will pass it back to the Subscriber by calling <code class="language-plaintext highlighter-rouge">receive(subscription:)</code>. When the Subscriber receives the Subscription, it can start asking for values. It also has to retain the Subscription, otherwise it would get deallocated. Update the body of <code class="language-plaintext highlighter-rouge">receive(subscription:)</code> to match these requirements:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">receive</span><span class="p">(</span><span class="nv">subscription</span><span class="p">:</span> <span class="kt">Subscription</span><span class="p">)</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">subscription</span> <span class="o">=</span> <span class="n">subscription</span>
<span class="n">subscription</span><span class="o">.</span><span class="nf">request</span><span class="p">(</span><span class="o">.</span><span class="nf">max</span><span class="p">(</span><span class="n">demand</span><span class="p">))</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="receiving-values">Receiving Values</h3>
<p>When the Publisher produces a value, it passes it to the Subscriber by calling <code class="language-plaintext highlighter-rouge">receive(_:)</code>. The Subscriber can then process the value, and optionally update the demand. Update the body of <code class="language-plaintext highlighter-rouge">receive(_:)</code> to the following:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">receive</span><span class="p">(</span><span class="n">_</span> <span class="nv">input</span><span class="p">:</span> <span class="kt">Int</span><span class="p">)</span> <span class="o">-></span> <span class="kt">Subscribers</span><span class="o">.</span><span class="kt">Demand</span> <span class="p">{</span>
<span class="nf">onValueReceived</span><span class="p">(</span><span class="n">input</span><span class="p">)</span>
<span class="k">return</span> <span class="o">.</span><span class="k">none</span>
<span class="p">}</span>
</code></pre></div></div>
<p>In its current state, <code class="language-plaintext highlighter-rouge">TestSubscriber</code> will work with the demand passed in during initialization, and will not change it dynamically. When a value is received, it invokes <code class="language-plaintext highlighter-rouge">onValueReceived</code> with it.</p>
<h3 id="receiving-completion">Receiving Completion</h3>
<p>Finally, fill out the implementation of <code class="language-plaintext highlighter-rouge">receive(completion:)</code>:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">receive</span><span class="p">(</span><span class="nv">completion</span><span class="p">:</span> <span class="kt">Subscribers</span><span class="o">.</span><span class="kt">Completion</span><span class="o"><</span><span class="kt">Never</span><span class="o">></span><span class="p">)</span> <span class="p">{</span>
<span class="nf">onComplete</span><span class="p">()</span>
<span class="n">subscription</span> <span class="o">=</span> <span class="kc">nil</span>
<span class="p">}</span>
</code></pre></div></div>
<p>The Publisher will call <code class="language-plaintext highlighter-rouge">receive(completion:)</code> when it finishes (either normally or with an error). Your implementation will invoke the completion handler, and nil out its reference to the Subscription. The latter step is important to break the retain cycle between the Subscriber and the Subscription.</p>
<h2 id="testing-with-arbitrary-demand">Testing With Arbitrary Demand</h2>
<p>Now it’s time to add a test for the scenario where a fixed number of values are requested. Add the following test method to your test case:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">testWhenTwoValuesAreRequested_ItCompletesAfterEmittingTwoValues</span><span class="p">()</span> <span class="p">{</span>
<span class="c1">// 1.</span>
<span class="k">let</span> <span class="nv">expectedValues</span><span class="p">:</span> <span class="p">[</span><span class="kt">TimeInterval</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">]</span>
<span class="k">var</span> <span class="nv">receivedValues</span><span class="p">:</span> <span class="p">[</span><span class="kt">TimeInterval</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span>
<span class="c1">// 2.</span>
<span class="k">let</span> <span class="nv">subscriber</span> <span class="o">=</span> <span class="kt">TestSubscriber</span><span class="p">(</span><span class="nv">demand</span><span class="p">:</span> <span class="mi">2</span><span class="p">)</span> <span class="p">{</span> <span class="n">value</span> <span class="k">in</span>
<span class="n">receivedValues</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">sut</span><span class="o">.</span><span class="nf">subscribe</span><span class="p">(</span><span class="n">subscriber</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">timeUpdates</span><span class="p">:</span> <span class="p">[</span><span class="kt">TimeInterval</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">]</span>
<span class="c1">// 3.</span>
<span class="n">timeUpdates</span><span class="o">.</span><span class="n">forEach</span> <span class="p">{</span> <span class="n">time</span> <span class="k">in</span> <span class="n">player</span><span class="o">.</span><span class="nf">updateClosure</span><span class="p">?(</span><span class="kt">CMTime</span><span class="p">(</span><span class="nv">seconds</span><span class="p">:</span> <span class="n">time</span><span class="p">,</span> <span class="nv">preferredTimescale</span><span class="p">:</span> <span class="kt">CMTimeScale</span><span class="p">(</span><span class="kt">NSEC_PER_SEC</span><span class="p">)))</span> <span class="p">}</span>
<span class="c1">// 4.</span>
<span class="kt">XCTAssertEqual</span><span class="p">(</span><span class="n">receivedValues</span><span class="p">,</span> <span class="n">expectedValues</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Let’s look at it in detail:</p>
<ol>
<li>Just as before, you establish the expected result, and declare an array to hold the received values.</li>
<li>You initialize a <code class="language-plaintext highlighter-rouge">TestSubscriber</code> instance which will request two values.</li>
<li>To verify that the Publisher doesn’t send more values than needed, you’ll use the mocked <code class="language-plaintext highlighter-rouge">updateClosure</code> to produce five values.</li>
<li>Finally, you’ll validate the received values.</li>
</ol>
<p>Run your test now, it should succeed.</p>
<h2 id="delaying-the-publishers-work">Delaying the Publisher’s Work</h2>
<p>The following test will add another twist to the setup: the initial demand will be zero, but you’ll request additional values later. This will validate that the Publisher doesn’t start emitting values prematurely. In order to manipulate the demand on the fly, add the following method to <code class="language-plaintext highlighter-rouge">TestSubscriber</code>:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">startRequestingValues</span><span class="p">(</span><span class="n">_</span> <span class="nv">demand</span><span class="p">:</span> <span class="kt">Int</span><span class="p">)</span> <span class="p">{</span>
<span class="k">guard</span> <span class="k">let</span> <span class="nv">subscription</span> <span class="o">=</span> <span class="n">subscription</span> <span class="k">else</span> <span class="p">{</span>
<span class="nf">fatalError</span><span class="p">(</span><span class="s">"requestValues(_:) may only be called after subscribing"</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">subscription</span><span class="o">.</span><span class="nf">request</span><span class="p">(</span><span class="o">.</span><span class="nf">max</span><span class="p">(</span><span class="n">demand</span><span class="p">))</span>
<span class="p">}</span>
</code></pre></div></div>
<p>The method makes sure that the subscription already exists, then requests the specified values.</p>
<p>Now add the following test method:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">testWhenDemandIsZero_ItEmitsNoValues</span><span class="p">()</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">expectedValues</span><span class="p">:</span> <span class="p">[</span><span class="kt">TimeInterval</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">var</span> <span class="nv">receivedValues</span><span class="p">:</span> <span class="p">[</span><span class="kt">TimeInterval</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">let</span> <span class="nv">subscriber</span> <span class="o">=</span> <span class="kt">TestSubscriber</span><span class="p">(</span><span class="nv">demand</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="n">value</span> <span class="k">in</span>
<span class="n">receivedValues</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">sut</span><span class="o">.</span><span class="nf">subscribe</span><span class="p">(</span><span class="n">subscriber</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">timeUpdates</span><span class="p">:</span> <span class="p">[</span><span class="kt">TimeInterval</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">]</span>
<span class="n">timeUpdates</span><span class="o">.</span><span class="n">forEach</span> <span class="p">{</span> <span class="n">time</span> <span class="k">in</span> <span class="n">player</span><span class="o">.</span><span class="nf">updateClosure</span><span class="p">?(</span><span class="kt">CMTime</span><span class="p">(</span><span class="nv">seconds</span><span class="p">:</span> <span class="n">time</span><span class="p">,</span> <span class="nv">preferredTimescale</span><span class="p">:</span> <span class="kt">CMTimeScale</span><span class="p">(</span><span class="kt">NSEC_PER_SEC</span><span class="p">)))</span> <span class="p">}</span>
<span class="kt">XCTAssertEqual</span><span class="p">(</span><span class="n">receivedValues</span><span class="p">,</span> <span class="n">expectedValues</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>This test verifies that the Publisher doesn’t start emitting values if the initial demand is zero. To test modifying the demand, you’ll use a slightly tweaked variation of the same method:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">testWhenInitialDemandIsZero_AndThenFiveValuesAreRequested_ItEmitsFiveValues</span><span class="p">()</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">expectedValues</span><span class="p">:</span> <span class="p">[</span><span class="kt">TimeInterval</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">]</span>
<span class="k">var</span> <span class="nv">receivedValues</span><span class="p">:</span> <span class="p">[</span><span class="kt">TimeInterval</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">let</span> <span class="nv">subscriber</span> <span class="o">=</span> <span class="kt">TestSubscriber</span><span class="p">(</span><span class="nv">demand</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="n">value</span> <span class="k">in</span>
<span class="n">receivedValues</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">sut</span><span class="o">.</span><span class="nf">subscribe</span><span class="p">(</span><span class="n">subscriber</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">timeUpdates</span><span class="p">:</span> <span class="p">[</span><span class="kt">TimeInterval</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">]</span>
<span class="c1">// Request more values</span>
<span class="n">subscriber</span><span class="o">.</span><span class="nf">startRequestingValues</span><span class="p">(</span><span class="mi">5</span><span class="p">)</span>
<span class="n">timeUpdates</span><span class="o">.</span><span class="n">forEach</span> <span class="p">{</span> <span class="n">time</span> <span class="k">in</span> <span class="n">player</span><span class="o">.</span><span class="nf">updateClosure</span><span class="p">?(</span><span class="kt">CMTime</span><span class="p">(</span><span class="nv">seconds</span><span class="p">:</span> <span class="n">time</span><span class="p">,</span> <span class="nv">preferredTimescale</span><span class="p">:</span> <span class="kt">CMTimeScale</span><span class="p">(</span><span class="kt">NSEC_PER_SEC</span><span class="p">)))</span> <span class="p">}</span>
<span class="kt">XCTAssertEqual</span><span class="p">(</span><span class="n">receivedValues</span><span class="p">,</span> <span class="n">expectedValues</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>The only notable difference is that in this test you request five more values after the initial subscription.</p>
<h2 id="updating-the-demand-when-a-value-is-received">Updating the Demand When a Value Is Received</h2>
<p>There is still a gap in the implementation to cover: when a Publisher sends a value by calling <code class="language-plaintext highlighter-rouge">receive(_:)</code>, the Subscriber has a chance to return an updated demand. One important thing to note here is that the new demand will be added to the existing one. So if the Subscriber initially demands two values, there’s no way to decrement that demand; you can return <code class="language-plaintext highlighter-rouge">none</code> to keep the demand as is, or specify the additional demand.</p>
<p>In order to test this setup, you’ll need to hook into the <code class="language-plaintext highlighter-rouge">receive(_:)</code> method of the Subscriber. Add the following changes to the implementation of <code class="language-plaintext highlighter-rouge">TestSubscriber</code>:
In order to test this setup, you’ll need to change the <code class="language-plaintext highlighter-rouge">TestSubscriber</code> to allow modifying the demand:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="kt">TestSubscriber</span><span class="p">:</span> <span class="kt">Subscriber</span> <span class="p">{</span>
<span class="o">...</span>
<span class="kd">private</span> <span class="k">let</span> <span class="nv">onValueReceived</span><span class="p">:</span> <span class="p">(</span><span class="kt">Int</span><span class="p">)</span> <span class="o">-></span> <span class="kt">Int</span>
<span class="o">...</span>
</code></pre></div></div>
<p>Now that <code class="language-plaintext highlighter-rouge">onValueReceived</code> has a return value, it will allow tests to update the demand. Update the implementation of <code class="language-plaintext highlighter-rouge">receive(_:)</code>:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">receive</span><span class="p">(</span><span class="n">_</span> <span class="nv">input</span><span class="p">:</span> <span class="kt">Int</span><span class="p">)</span> <span class="o">-></span> <span class="kt">Subscribers</span><span class="o">.</span><span class="kt">Demand</span> <span class="p">{</span>
<span class="n">receivedValues</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="n">input</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">newDemand</span> <span class="o">=</span> <span class="nf">onValueReceived</span><span class="p">(</span><span class="n">input</span><span class="p">)</span>
<span class="k">return</span> <span class="o">.</span><span class="nf">max</span><span class="p">(</span><span class="n">newDemand</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>The new implementation acquires the updated demand by invoking <code class="language-plaintext highlighter-rouge">onValueReceived</code>, and passes it back to the Publisher.</p>
<p>To verify the behavior, add the following test method:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">testWhenInitialDemandIsOne_AndAnAdditionalValueIsRequested_ItEmitsTwoValues</span><span class="p">()</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">expectedValues</span><span class="p">:</span> <span class="p">[</span><span class="kt">TimeInterval</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">]</span>
<span class="k">var</span> <span class="nv">receivedValues</span><span class="p">:</span> <span class="p">[</span><span class="kt">TimeInterval</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">let</span> <span class="nv">subscriber</span> <span class="o">=</span> <span class="kt">TestSubscriber</span><span class="p">(</span><span class="nv">demand</span><span class="p">:</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span> <span class="n">value</span> <span class="k">in</span>
<span class="n">receivedValues</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
<span class="k">return</span> <span class="n">value</span> <span class="o">==</span> <span class="mi">1</span> <span class="p">?</span> <span class="mi">1</span> <span class="p">:</span> <span class="mi">0</span>
<span class="p">}</span>
<span class="n">sut</span><span class="o">.</span><span class="nf">subscribe</span><span class="p">(</span><span class="n">subscriber</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">timeUpdates</span><span class="p">:</span> <span class="p">[</span><span class="kt">TimeInterval</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">]</span>
<span class="n">timeUpdates</span><span class="o">.</span><span class="n">forEach</span> <span class="p">{</span> <span class="n">time</span> <span class="k">in</span> <span class="n">player</span><span class="o">.</span><span class="nf">updateClosure</span><span class="p">?(</span><span class="kt">CMTime</span><span class="p">(</span><span class="nv">seconds</span><span class="p">:</span> <span class="n">time</span><span class="p">,</span> <span class="nv">preferredTimescale</span><span class="p">:</span> <span class="kt">CMTimeScale</span><span class="p">(</span><span class="kt">NSEC_PER_SEC</span><span class="p">)))</span> <span class="p">}</span>
<span class="kt">XCTAssertEqual</span><span class="p">(</span><span class="n">receivedValues</span><span class="p">,</span> <span class="n">expectedValues</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>The goal of this test is to request a single value initially, and request an additional one upon receiving it. So it checks the value received, and asks for another one if it equals one.</p>
<h2 id="conclusion">Conclusion</h2>
<p>By following along, you’ve covered all the possible use cases of a custom Publisher with tests, which will allow you to be more confident in your implementation. Along the way you’ve learned about Combine’s demand system, and implemented a custom Subscriber: although it was only used to support the unit tests, you may find yourself using such a Subscriber in a real-life scenario as well, when you need more control over the behavior of Publishers.</p>
<p>If you would like to see these topics in context, check out <a href="https://github.com/jozsef-vesza/AVFoundation-Combine">the full project on GitHub</a>, which contains all the code discussed here, and has an example app you can play around.
I hope this knowledge will serve you well when working with Combine in the wild. If you have any questions or feedback about the topics covered in this series, do not hesitate to reach out to me on <a href="https://twitter.com/j_vesza">Twitter</a>, I would love to hear it.</p>József Veszajozsef.vesza@gmail.comIn Part 1 of the series, you’ve built a Publisher from scratch. One of the key parts of Combine’s event delivery is keeping up with the Subscriber’s demand. It involves some manual bookkeeping, which is known to be error-prone, so in this guide you’ll learn how to ensure that your custom Publisher is not misbehaving.Combine Publishers, Part 1: Creating a Custom Publisher2020-07-24T00:00:00+00:002020-07-24T00:00:00+00:00https://jozsef-vesza.dev/2020/07/24/creating-a-custom-combine-publisher<p>If you’re just getting started with Combine, the idea of a custom publisher can sound scary, but diving into the topic has many benefits: you’ll understand how parts of the framework work together, and will be able to create your own Combine-powered APIs.</p>
<h2 id="getting-started">Getting Started</h2>
<p>Along with introducing Combine, Apple also extended many well-known APIs, such as <code class="language-plaintext highlighter-rouge">URLSession</code> and <code class="language-plaintext highlighter-rouge">NotificationCenter</code> to offer built-in Publishers. Make sure to have a look in the documentation before deciding to roll your own implementation.</p>
<p>AVFoundation is a good candidate to extend: is has events delivered via KVO, NotifcationCenter, and some of them you’ll have to query yourself. With Combine’s Publishers you could unify the event delivery. This post will guide you through the process by creating a publisher for observing AVPlayer’s playback progress.</p>
<p>There are multiple ways for creating your own Publishers: you can use the <code class="language-plaintext highlighter-rouge">@Published</code> property wrapper, or use a <code class="language-plaintext highlighter-rouge">PassthroughSubject</code> instance to send values on demand. In this guide however, you’ll build a Publisher from scratch.</p>
<p>Let’s have a look at Combine’s the key components.</p>
<h3 id="publishers">Publishers</h3>
<p>A Publisher represents a type that delivers values over time to Subscribers. A Publisher’s job is to accept a Subscriber, which it will later notify as events occur. Combine also offers various operators; these are Publishers, that receive data from an upstream Publishers, manipulate the data (e.g. map the received values to another type), and send the results downstream.</p>
<h3 id="subscribers">Subscribers</h3>
<p>A Subscriber receives values from a Publishers. Along with those values, it may also receive lifecycle events (such as completion). Combine provides two built-in Subscriber implementation:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">sink(receiveCompletion:receiveValue:)</code>: to execute arbitrary work as events occur</li>
<li><code class="language-plaintext highlighter-rouge">assign(to:on:)</code>: to assign received values to a key path of an object.
It’s also possible to create your own Subscriber, which will be covered later in the series.</li>
</ul>
<h3 id="subscriptions">Subscriptions</h3>
<p>Subscriptions represent the connection between a Publisher and a Subscriber. You’ll look at Subscribers in detail when implementing a Publisher.</p>
<h3 id="sequence-of-events">Sequence of Events</h3>
<p>These components operate together following a sequence of events:</p>
<ol>
<li>Subscriber subscribes to a Publisher</li>
<li>The Publisher creates the Subscription, and passes it to the Subscriber</li>
<li>The Subscriber will request values</li>
<li>The Publisher sends values</li>
<li>The Publisher completes (either regularly or due to an error)</li>
</ol>
<h2 id="creating-the-publisher">Creating the Publisher</h2>
<p>The goal of your Publisher will be to provide playback progress updates over a given interval. To do this, you can rely on AVPlayer’s <code class="language-plaintext highlighter-rouge">addPeriodicTimeObserver(forInterval:queue:using:)</code> method, which will periodically invoke its closure parameter to report the progress.</p>
<p>Let’s start by declaring the new Publisher:</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">struct</span> <span class="kt">PlayheadProgressPublisher</span><span class="p">:</span> <span class="kt">Publisher</span> <span class="p">{</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>This bit of code will not build, but Xcode will offer some helpful hints on how to proceed: A Publisher must declare the type of values, and errors it can emit. This Publisher will emit the current progress in seconds, and will not emit an error. Let’s update the implementation:</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">struct</span> <span class="kt">PlayheadProgressPublisher</span><span class="p">:</span> <span class="kt">Publisher</span> <span class="p">{</span>
<span class="kd">typealias</span> <span class="kt">Output</span> <span class="o">=</span> <span class="kt">TimeInterval</span>
<span class="kd">typealias</span> <span class="kt">Failure</span> <span class="o">=</span> <span class="kt">Never</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>After declaring the types, you’ll still get build errors, but thankfully Xcode will offer another round of fix-its. Update your implementation with the following:</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">struct</span> <span class="kt">PlayheadProgressPublisher</span><span class="p">:</span> <span class="kt">Publisher</span> <span class="p">{</span>
<span class="kd">typealias</span> <span class="kt">Output</span> <span class="o">=</span> <span class="kt">TimeInterval</span>
<span class="kd">typealias</span> <span class="kt">Failure</span> <span class="o">=</span> <span class="kt">Never</span>
<span class="kd">func</span> <span class="n">receive</span><span class="o"><</span><span class="kt">S</span><span class="o">></span><span class="p">(</span><span class="nv">subscriber</span><span class="p">:</span> <span class="kt">S</span><span class="p">)</span> <span class="k">where</span> <span class="kt">S</span> <span class="p">:</span> <span class="kt">Subscriber</span><span class="p">,</span> <span class="k">Self</span><span class="o">.</span><span class="kt">Failure</span> <span class="o">==</span> <span class="kt">S</span><span class="o">.</span><span class="kt">Failure</span><span class="p">,</span> <span class="k">Self</span><span class="o">.</span><span class="kt">Output</span> <span class="o">==</span> <span class="kt">S</span><span class="o">.</span><span class="kt">Input</span> <span class="p">{</span>
<span class="c1">// TODO:</span>
<span class="c1">// 1. Create Subscription</span>
<span class="c1">// 2. Pass Subscription to Subscriber</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Whenever a Subscriber subscribes to a Publisher, the <code class="language-plaintext highlighter-rouge">receive(subscriber:)</code> method is invoked. From that point, it’s the Publisher’s responsibility to create a Subscription, and pass it back to the Subscriber. Take a moment to look at the method signature: it states that the Subscriber’s Input type must match the Publisher’s Output type, and the Failure types must also match. Think back to the sequence of events: <strong>Steps 1 and 2</strong> are covered here.</p>
<p>To be able to pass the Subscription to the Subscriber, you’ll first need to take a detour to implement it. Let’s have a look.</p>
<h2 id="creating-the-subscription">Creating the Subscription</h2>
<p>The Subscription is where most of the work will happen: it’s responsible for performing the work based on the demand of the Subscriber. Below the <code class="language-plaintext highlighter-rouge">PlayheadProgressPublisher</code> struct, add the following declaration:</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="o">...</span>
<span class="kd">private</span> <span class="kd">final</span> <span class="kd">class</span> <span class="kt">PlayheadProgressSubscription</span><span class="o"><</span><span class="kt">S</span><span class="p">:</span> <span class="kt">Subscriber</span><span class="o">></span><span class="p">:</span> <span class="kt">Subscription</span> <span class="k">where</span> <span class="kt">S</span><span class="o">.</span><span class="kt">Input</span> <span class="o">==</span> <span class="kt">TimeInterval</span> <span class="p">{</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Notice how the type signature restricts the Subscriber’s Input to be <code class="language-plaintext highlighter-rouge">TimeInterval</code>. Xcode will step in again, offering to add stubs for a couple of methods:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">final</span> <span class="kd">class</span> <span class="kt">PlayheadProgressSubscription</span><span class="o"><</span><span class="kt">S</span><span class="p">:</span> <span class="kt">Subscriber</span><span class="o">></span><span class="p">:</span> <span class="kt">Subscription</span> <span class="k">where</span> <span class="kt">S</span><span class="o">.</span><span class="kt">Input</span> <span class="o">==</span> <span class="kt">TimeInterval</span> <span class="p">{</span>
<span class="kd">func</span> <span class="nf">request</span><span class="p">(</span><span class="n">_</span> <span class="nv">demand</span><span class="p">:</span> <span class="kt">Subscribers</span><span class="o">.</span><span class="kt">Demand</span><span class="p">)</span> <span class="p">{</span>
<span class="p">}</span>
<span class="kd">func</span> <span class="nf">cancel</span><span class="p">()</span> <span class="p">{</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>The first method will cover <strong>Step 3</strong> in the sequence of events: when a Subscriber starts requesting values from a Publisher, <code class="language-plaintext highlighter-rouge">request(_:)</code> will be invoked on the Subscription. The other method, <code class="language-plaintext highlighter-rouge">cancel()</code> is invoked when the Subscription is cancelled; it’s your chance to clean up.</p>
<p>Before diving into the implementation of these methods, let’s add a few properties, and an init method to the Subscription:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">final</span> <span class="kd">class</span> <span class="kt">PlayheadProgressSubscription</span><span class="o"><</span><span class="kt">S</span><span class="p">:</span> <span class="kt">Subscriber</span><span class="o">></span><span class="p">:</span> <span class="kt">Subscription</span> <span class="k">where</span> <span class="kt">S</span><span class="o">.</span><span class="kt">Input</span> <span class="o">==</span> <span class="kt">TimeInterval</span> <span class="p">{</span>
<span class="kd">private</span> <span class="k">var</span> <span class="nv">subscriber</span><span class="p">:</span> <span class="kt">S</span><span class="p">?</span>
<span class="kd">private</span> <span class="k">var</span> <span class="nv">requested</span><span class="p">:</span> <span class="kt">Subscribers</span><span class="o">.</span><span class="kt">Demand</span> <span class="o">=</span> <span class="o">.</span><span class="k">none</span>
<span class="kd">private</span> <span class="k">var</span> <span class="nv">timeObserverToken</span><span class="p">:</span> <span class="kt">Any</span><span class="p">?</span> <span class="o">=</span> <span class="kc">nil</span>
<span class="kd">private</span> <span class="k">let</span> <span class="nv">interval</span><span class="p">:</span> <span class="kt">TimeInterval</span>
<span class="kd">private</span> <span class="k">let</span> <span class="nv">player</span><span class="p">:</span> <span class="kt">AVPlayer</span>
<span class="nf">init</span><span class="p">(</span><span class="nv">subscriber</span><span class="p">:</span> <span class="kt">S</span><span class="p">,</span> <span class="nv">interval</span><span class="p">:</span> <span class="kt">TimeInterval</span> <span class="o">=</span> <span class="mf">0.25</span><span class="p">,</span> <span class="nv">player</span><span class="p">:</span> <span class="kt">AVPlayer</span><span class="p">)</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">player</span> <span class="o">=</span> <span class="n">player</span>
<span class="k">self</span><span class="o">.</span><span class="n">subscriber</span> <span class="o">=</span> <span class="n">subscriber</span>
<span class="k">self</span><span class="o">.</span><span class="n">interval</span> <span class="o">=</span> <span class="n">interval</span>
<span class="p">}</span>
<span class="o">...</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Let’s look at them one by one:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">subscriber</code>: the Subscription retains the Subscriber to be able to notify it as events occur</li>
<li><code class="language-plaintext highlighter-rouge">requested</code>: the Subscription keeps track of the demand coming from a Subscriber. There is an initial value passed via <code class="language-plaintext highlighter-rouge">request(_:)</code>, but it can also increase during the lifetime of the Subscription.</li>
<li><code class="language-plaintext highlighter-rouge">timeObserverToken</code>: will be used to hold the return value of AVPlayer’s <code class="language-plaintext highlighter-rouge">addPeriodicTimeObserver(forInterval:queue:using:)</code></li>
<li><code class="language-plaintext highlighter-rouge">interval</code>: the time interval at which values should be provided</li>
<li><code class="language-plaintext highlighter-rouge">player</code>: the AVPlayer instance to observe</li>
</ul>
<p>Now it’s time to implement <code class="language-plaintext highlighter-rouge">request(_:)</code>. Update your implementation to the following:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">request</span><span class="p">(</span><span class="n">_</span> <span class="nv">demand</span><span class="p">:</span> <span class="kt">Subscribers</span><span class="o">.</span><span class="kt">Demand</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// 1.</span>
<span class="n">requested</span> <span class="o">+=</span> <span class="n">demand</span>
<span class="c1">// 2.</span>
<span class="k">guard</span> <span class="n">timeObserverToken</span> <span class="o">==</span> <span class="kc">nil</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
<span class="c1">// 3.</span>
<span class="k">let</span> <span class="nv">interval</span> <span class="o">=</span> <span class="kt">CMTime</span><span class="p">(</span><span class="nv">seconds</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">interval</span><span class="p">,</span> <span class="nv">preferredTimescale</span><span class="p">:</span> <span class="kt">CMTimeScale</span><span class="p">(</span><span class="kt">NSEC_PER_SEC</span><span class="p">))</span>
<span class="n">timeObserverToken</span> <span class="o">=</span> <span class="n">player</span><span class="o">.</span><span class="nf">addPeriodicTimeObserver</span><span class="p">(</span><span class="nv">forInterval</span><span class="p">:</span> <span class="n">interval</span><span class="p">,</span> <span class="nv">queue</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="p">{</span> <span class="p">[</span><span class="k">weak</span> <span class="k">self</span><span class="p">]</span> <span class="n">time</span> <span class="k">in</span>
<span class="c1">// 4.</span>
<span class="k">guard</span>
<span class="k">let</span> <span class="nv">self</span> <span class="o">=</span> <span class="k">self</span><span class="p">,</span>
<span class="k">let</span> <span class="nv">subscriber</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="n">subscriber</span><span class="p">,</span>
<span class="k">self</span><span class="o">.</span><span class="n">requested</span> <span class="o">></span> <span class="o">.</span><span class="k">none</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
<span class="c1">// 5.</span>
<span class="k">self</span><span class="o">.</span><span class="n">requested</span> <span class="o">-=</span> <span class="o">.</span><span class="nf">max</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="c1">// 6.</span>
<span class="k">let</span> <span class="nv">newDemand</span> <span class="o">=</span> <span class="n">subscriber</span><span class="o">.</span><span class="nf">receive</span><span class="p">(</span><span class="n">time</span><span class="o">.</span><span class="n">seconds</span><span class="p">)</span>
<span class="k">self</span><span class="o">.</span><span class="n">requested</span> <span class="o">+=</span> <span class="n">newDemand</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Okay, that’s a lot of new code, let’s go over the changes:</p>
<ol>
<li>When the Subscriber requests values, it can specify how many values it wants by passing the initial demand. The Subscription is responsible for keeping track of the demand, so you’ll increment <code class="language-plaintext highlighter-rouge">requested</code> by the received amount.</li>
<li>The goal is to only start emitting events once a Subscriber is attached to a Publisher. If <code class="language-plaintext highlighter-rouge">timeObserverToken</code> is nil, that means that the Subscription hasn’t started producing values yet. Checking the demand is also important: it could be that you’re dealing with a custom Subscriber instance which only requests values if a certain condition is true: your Subscription shouldn’t complete right away, just defer the work until there’s actual demand for values.</li>
<li>At this point the Subscription starts to query the playback progress, with the frequency specified in <code class="language-plaintext highlighter-rouge">interval</code>.</li>
<li>Once there is a new value to emit, the implementation checks if there are values demanded.</li>
<li>Then <code class="language-plaintext highlighter-rouge">requested</code> is decremented to avoid sending more values than needed.</li>
<li>The value is then delivered to the Subscriber (<strong>Step 4</strong> in the event sequence). Upon receiving a value, the Subscriber may choose to update the demand, so the Subscription must update <code class="language-plaintext highlighter-rouge">requested</code> to keep up with the new demand.</li>
</ol>
<blockquote>
<p>In some use cases it might make sense to send a completion event at some point (e.g. if the demand dropts to zero). This implementation however will only complete when the Subscription is cancelled, to work better with Combine’s demand system.</p>
</blockquote>
<p>Notice how it’s entirely up to the Subscribtion implementation to honor the demand. This is a crucial point: if you forget to decrement <code class="language-plaintext highlighter-rouge">requested</code>, the Subscriber may emit more values than requested; if you don’t keep track of the updated demand, the Subscriber can end up delivering fewer values. There is no automatic behavior you can rely on to update the demand, and manual bookkeeping can be error-prone, which is why it’s important to unit test your custom Publishers, which will be covered in Part 2 of the series.</p>
<p>There is one final piece the for the Subscription, which is cancellation. Update the implementation of <code class="language-plaintext highlighter-rouge">cancel()</code>:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">cancel</span><span class="p">()</span> <span class="p">{</span>
<span class="k">if</span> <span class="k">let</span> <span class="nv">timeObserverToken</span> <span class="o">=</span> <span class="n">timeObserverToken</span> <span class="p">{</span>
<span class="n">player</span><span class="o">.</span><span class="nf">removeTimeObserver</span><span class="p">(</span><span class="n">timeObserverToken</span><span class="p">)</span>
<span class="p">}</span>
<span class="n">timeObserverToken</span> <span class="o">=</span> <span class="kc">nil</span>
<span class="n">subscriber</span> <span class="o">=</span> <span class="kc">nil</span>
<span class="p">}</span>
</code></pre></div></div>
<p>These are cleanup steps: the Subscription stops observing the playback progress, and nils out <code class="language-plaintext highlighter-rouge">timeObserverToken</code> and its reference to the Subscriber (the Subscriber alredy retains the Subscription, so this step is necessary to break the retain cycle).</p>
<p>And with that, the implementation of Subscription is complete. Now it’s time to connect the parts.</p>
<h2 id="passing-the-subscription-to-the-subscriber">Passing the Subscription to the Subscriber</h2>
<p>The final piece of the puzzle is to pass your new Subscription implementation to the Subscriber upon subscription. Before doing that, you’ll need to add a few more properties to the Publisher:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="k">let</span> <span class="nv">interval</span><span class="p">:</span> <span class="kt">TimeInterval</span>
<span class="kd">private</span> <span class="k">let</span> <span class="nv">player</span><span class="p">:</span> <span class="kt">AVPlayer</span>
<span class="nf">init</span><span class="p">(</span><span class="nv">interval</span><span class="p">:</span> <span class="kt">TimeInterval</span> <span class="o">=</span> <span class="mf">0.25</span><span class="p">,</span> <span class="nv">player</span><span class="p">:</span> <span class="kt">AVPlayer</span><span class="p">)</span> <span class="p">{</span>
<span class="k">self</span><span class="o">.</span><span class="n">player</span> <span class="o">=</span> <span class="n">player</span>
<span class="k">self</span><span class="o">.</span><span class="n">interval</span> <span class="o">=</span> <span class="n">interval</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Notice how the properties mirror the input parameters of the Subscription. This is no accident, you’ll use them to initialize it. Now update the implementation of <code class="language-plaintext highlighter-rouge">receive(subscriber:)</code>:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="n">receive</span><span class="o"><</span><span class="kt">S</span><span class="o">></span><span class="p">(</span><span class="nv">subscriber</span><span class="p">:</span> <span class="kt">S</span><span class="p">)</span> <span class="k">where</span> <span class="kt">S</span> <span class="p">:</span> <span class="kt">Subscriber</span><span class="p">,</span> <span class="k">Self</span><span class="o">.</span><span class="kt">Failure</span> <span class="o">==</span> <span class="kt">S</span><span class="o">.</span><span class="kt">Failure</span><span class="p">,</span> <span class="k">Self</span><span class="o">.</span><span class="kt">Output</span> <span class="o">==</span> <span class="kt">S</span><span class="o">.</span><span class="kt">Input</span> <span class="p">{</span>
<span class="k">let</span> <span class="nv">subscription</span> <span class="o">=</span> <span class="kt">PlayheadProgressSubscription</span><span class="p">(</span><span class="nv">subscriber</span><span class="p">:</span> <span class="n">subscriber</span><span class="p">,</span>
<span class="nv">interval</span><span class="p">:</span> <span class="n">interval</span><span class="p">,</span>
<span class="nv">player</span><span class="p">:</span> <span class="n">player</span><span class="p">)</span>
<span class="n">subscriber</span><span class="o">.</span><span class="nf">receive</span><span class="p">(</span><span class="nv">subscription</span><span class="p">:</span> <span class="n">subscription</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Here you’ll create a new Subscription and pass it to the Subscriber to kick off the event stream.</p>
<h2 id="trying-it-out">Trying it out</h2>
<p>Now that your custom Publisher is ready to use, it’s time to finally give it a try. To make the new Publisher easier to access, declare the following AVPlayer extension:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">extension</span> <span class="kt">AVPlayer</span> <span class="p">{</span>
<span class="kd">func</span> <span class="nf">playheadProgressPublisher</span><span class="p">(</span><span class="nv">interval</span><span class="p">:</span> <span class="kt">TimeInterval</span> <span class="o">=</span> <span class="mf">0.25</span><span class="p">)</span> <span class="o">-></span> <span class="kt">Publishers</span><span class="o">.</span><span class="kt">PlayheadProgressPublisher</span> <span class="p">{</span>
<span class="kt">Publishers</span><span class="o">.</span><span class="kt">PlayheadProgressPublisher</span><span class="p">(</span><span class="nv">interval</span><span class="p">:</span> <span class="n">interval</span><span class="p">,</span> <span class="nv">player</span><span class="p">:</span> <span class="k">self</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Now you can start observing the playback progress:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">var</span> <span class="nv">subscriptions</span> <span class="o">=</span> <span class="kt">Set</span><span class="o"><</span><span class="kt">AnyCancellable</span><span class="o">></span><span class="p">()</span>
<span class="k">let</span> <span class="nv">videoURL</span> <span class="o">=</span> <span class="kt">URL</span><span class="p">(</span><span class="nv">string</span><span class="p">:</span> <span class="s">"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"</span><span class="p">)</span><span class="o">!</span>
<span class="k">let</span> <span class="nv">player</span> <span class="o">=</span> <span class="kt">AVPlayer</span><span class="p">(</span><span class="nv">url</span><span class="p">:</span> <span class="n">videoURL</span><span class="p">)</span>
<span class="n">player</span><span class="o">.</span><span class="nf">playheadProgressPublisher</span><span class="p">()</span>
<span class="o">.</span><span class="n">sink</span> <span class="p">{</span> <span class="p">(</span><span class="n">time</span><span class="p">)</span> <span class="k">in</span>
<span class="nf">print</span><span class="p">(</span><span class="s">"received playhead progress: </span><span class="se">\(</span><span class="n">time</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
<span class="p">}</span>
<span class="o">.</span><span class="nf">store</span><span class="p">(</span><span class="nv">in</span><span class="p">:</span> <span class="o">&</span><span class="n">subscriptions</span><span class="p">)</span>
</code></pre></div></div>
<h2 id="conclusion">Conclusion</h2>
<p>I hope you enjoyed this guide on custom Publishers. It may seem like a lot to digest at first, so definitely take your time to play around with the concept, try to apply it on some of your own existing code.</p>
<p>If you would like to see these topics in context, check out <a href="https://github.com/jozsef-vesza/AVFoundation-Combine">the full project on GitHub</a>, which contains all the code discussed here, and has an example app you can play around.</p>
<p>As noted previously, it’s definitely a good idea to back your Publisher implementations up with unit tests, and in <a href="https://jozsef-vesza.dev/2020/08/01/unit-testing-publishers/">Part 2</a>, you’ll learn about how to just that.</p>József Veszajozsef.vesza@gmail.comIf you’re just getting started with Combine, the idea of a custom publisher can sound scary, but diving into the topic has many benefits: you’ll understand how parts of the framework work together, and will be able to create your own Combine-powered APIs.