Cocoa with Love https://www.cocoawithlove.com/ Recent content on Cocoa with Love Hugo -- gohugo.io en-us © 2024 Matt Gallagher. All rights reserved. Code may be used in accordance with license at https://www.cocoawithlove.com/about Wed, 25 Dec 2024 21:35:10 +1100 Using Copilot to write a raindrop audio synthesizer using AVAudioEngine https://www.cocoawithlove.com/blog/copilot-raindrop-generator.html Wed, 25 Dec 2024 21:35:10 +1100 https://www.cocoawithlove.com/blog/copilot-raindrop-generator.html <p>I&rsquo;ve largely ignored the use of large language models (LLMs) as programming assistants, despite (or because of) the hype of the last 2 years. I&rsquo;ve had a preconception that an LLM might not work well enough or might not meet my expectations of code quality.</p> <p>Since Microsoft have recently made the lowest tier of <a href="https://code.visualstudio.com/blogs/2024/12/18/free-github-copilot">GitHub Copilot for VS Code free</a>, I wanted to test that preconception and see how helpful LLM-assisted programming might be when implementing a small project in Swift. And maybe this will make an interesting point of comparison to <a href="https://www.apple.com/newsroom/2024/06/apple-empowers-developers-and-fuels-innovation-with-new-tools-and-resources/">Apple&rsquo;s Swift Assist</a> (if and when that&rsquo;s finally available).</p> <p>As for the project itself: I wanted to explore sound synthesis using <code>AVAudioEngine</code> after stumbling across <a href="https://github.com/747745124/Raindrop-Generator">a GitHub repository that implied a raindrop could be synthesized using a very simple waveform</a>. It seems unlikely that a simple waveform would produce a convincing raindrop sound but the project should be well within the &ldquo;few thousand line&rdquo; context limit for an LLM so I think it&rsquo;s an achievable goal.</p> <blockquote> <p><strong>That buzzword</strong>: I won&rsquo;t be using the term &ldquo;AI&rdquo; to describe these assistants. I wrote my engineering thesis on computer vision using neural networks more than 20 years ago and back then, the term &ldquo;AI&rdquo; was reserved for science fiction, &ldquo;artificial general intelligence&rdquo; or for use by people deceptively implying general intelligence when the truth was quite different.</p> </blockquote> <!-- TOC --> <h2 id="the-setup">The setup</h2> <p>I&rsquo;m running Visual Studio Code 1.96.2 and using its built-in connection to Github Copilot (GPT 4o) that is currently free to use if you authorise via your GitHub account. In VS Code, I&rsquo;ve installed the Swift v1.11.4 extension for Swift language server support and SweetPad 0.1.48 for building via Xcode and ensuring VS Code behaves better on Xcode Projects.</p> <p>If you haven&rsquo;t used SweetPad before, you need to set it up for each project by typing <em>Command-Shift-P</em> in VS Code and choosing &ldquo;SweetPad: Generate Build Server Config&rdquo; from the popup that appears and then selecting the build target so it can create a buildServer.json file. Additionally, the Swift extension might ask to configure itself (global is usually fine).</p> <p>If you aren&rsquo;t prompted when you launch VS Code, you may need to <a href="https://code.visualstudio.com/docs/copilot/setup-simplified">enable Copilot Free in VS Code to configure Copilot</a>. When you want to ask Copilot something, press <em>Command-Control-I</em> and that will present a chat window where you can prompt Copilot to write new code or refactor existing code.</p> <p>I haven&rsquo;t really spent enough time configuring VS Code the way I like it so outside of interacting with Copilot, I&rsquo;m likely to do most of my editing, building and running in Xcode itself.</p> <blockquote> <p><strong>Check it out</strong>: you can <a href="https://github.com/mattgallagher/RainGenerator">download the code for this article from the RainGenerator repository</a>. The commit history includes most of the steps discussed in this article.</p> </blockquote> <h3 id="alternative-setup">Alternative setup</h3> <p>Not to offer too much of a peek behind the curtain but I ran this same experiment nearly a month ago using <a href="https://www.cursor.com">Cursor</a> (a fork of Visual Studio Code designed to focus on these LLM-based workflows). Thomas Ricouard has written <a href="https://dimillian.medium.com/how-to-use-cursor-for-ios-development-54b912c23941">a pretty good article on using Cursor for iOS development</a>, if you&rsquo;d like to know more. I think the Claude 3.5 LLM that Cursor uses by default did produce slightly better code than Copilot but the difference between VS Code and Cursor or Copilot and Claude wasn&rsquo;t huge (the broad strokes are identical between each). This article will focus on my Copilot experience since I didn&rsquo;t really document my Cursor experience (although the first commit in the repository still has a date of December 1 from the Cursor timeline).</p> <h2 id="tone-generator">Tone Generator</h2> <h3 id="first-effort">First effort</h3> <p>I wanted to start the project simple so to begin, I thought I&rsquo;d ask Copilot to do something very simple <a href="https://www.cocoawithlove.com/2010/10/ios-tone-generator-introduction-to.html">that I&rsquo;ve written about before: a tone generator</a>. That code involves a huge amount of boilerplate that shouldn&rsquo;t be rerequired in <code>AVAudioEngine</code> (the latest iteration of the same audio processing graph concept).</p> <p>While I&rsquo;ve read about <code>AVAudioEngine</code> (introduced between macOS 10.10 and 10.15) I haven&rsquo;t really had the chance to use it so maybe Copilot can show me how it&rsquo;s done.</p> <blockquote> <p><b>Prompt:</b> Create a Tone Generator that plays a sine, square, or sawtooth waveform and offers sliders to control volume and frequency.</p> </blockquote> <p>The only context I&rsquo;ve provided to Copilot is a <code>ContentView.swift</code> file, so I&rsquo;m hoping it will understand that I&rsquo;d like a SwiftUI interface. But I haven&rsquo;t mentioned <code>AVAudioEngine</code>.</p> <p>How does Copilot fair?</p> <figure><img src="https://www.cocoawithlove.com/assets/blog/copilot-tone-generator-1.png" alt="Copilot&rsquo;s first effort" width="500px"><figcaption> <p>Copilot&rsquo;s first effort</p> </figcaption> </figure> <p>It has created an <code>AudioEngine</code> class that tries to render to a PCM buffer and play that in a loop. But it looks clunky (there&rsquo;s no real need for separate &ldquo;Play&rdquo; and &ldquo;Stop&rdquo; buttons and that &ldquo;Frequency&rdquo; slider is using a step size of 1 with 2000 steps, making it slow and largely unusable) but worst of all, <strong>it doesn&rsquo;t work</strong>. Hitting play throws an exception.</p> <p>Not a great start.</p> <p>Looking at the code, it&rsquo;s pretty simple to see that the exception is because the <code>format</code> in the <code>init</code> function – as used to connect the player to the <code>outputNode</code> – is not the same as the <code>format</code> used in the <code>start</code> function and the mismatch is leading to a runtime exception.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"> <span class="kd">init</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">engine</span><span class="p">.</span><span class="n">attach</span><span class="p">(</span><span class="n">player</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">format</span> <span class="p">=</span> <span class="n">engine</span><span class="p">.</span><span class="n">outputNode</span><span class="p">.</span><span class="n">inputFormat</span><span class="p">(</span><span class="n">forBus</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">engine</span><span class="p">.</span><span class="n">connect</span><span class="p">(</span><span class="n">player</span><span class="p">,</span> <span class="n">to</span><span class="p">:</span> <span class="n">engine</span><span class="p">.</span><span class="n">outputNode</span><span class="p">,</span> <span class="n">format</span><span class="p">:</span> <span class="n">format</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">try</span><span class="p">?</span> <span class="n">engine</span><span class="p">.</span><span class="n">start</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">func</span> <span class="nf">start</span><span class="p">(</span><span class="n">waveform</span><span class="p">:</span> <span class="n">Waveform</span><span class="p">,</span> <span class="n">frequency</span><span class="p">:</span> <span class="nb">Double</span><span class="p">,</span> <span class="n">volume</span><span class="p">:</span> <span class="nb">Double</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">sampleRate</span> <span class="p">=</span> <span class="mi">44100</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">length</span> <span class="p">=</span> <span class="n">AVAudioFrameCount</span><span class="p">(</span><span class="n">sampleRate</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">format</span> <span class="p">=</span> <span class="n">AVAudioFormat</span><span class="p">(</span><span class="n">standardFormatWithSampleRate</span><span class="p">:</span> <span class="nb">Double</span><span class="p">(</span><span class="n">sampleRate</span><span class="p">),</span> <span class="n">channels</span><span class="p">:</span> <span class="mi">1</span><span class="p">)</span><span class="o">!</span> </span></span><span class="line"><span class="cl"> <span class="n">buffer</span> <span class="p">=</span> <span class="n">AVAudioPCMBuffer</span><span class="p">(</span><span class="n">pcmFormat</span><span class="p">:</span> <span class="n">format</span><span class="p">,</span> <span class="n">frameCapacity</span><span class="p">:</span> <span class="n">length</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">buffer</span><span class="p">?.</span><span class="n">frameLength</span> <span class="p">=</span> <span class="n">length</span> </span></span></code></pre></div><p>Let&rsquo;s see if Copilot can sort out this problem.</p> <blockquote> <p><b>Prompt:</b> The AudioEngine isn&rsquo;t making a sound. Can you identify what is wrong?</p> </blockquote> <p>Copilot adds a catch around the creation of the <code>AVAudioEngine</code> (previously it was using a <code>try?</code>):</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="k">do</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">try</span> <span class="n">engine</span><span class="p">.</span><span class="n">start</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="k">catch</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="bp">print</span><span class="p">(</span><span class="s">&#34;Error starting audio engine: </span><span class="si">\(</span><span class="n">error</span><span class="p">.</span><span class="n">localizedDescription</span><span class="si">)</span><span class="s">&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Yeah, it didn&rsquo;t have a clue.</p> <p>Maybe if I give Copilot the exact exception, it can fix the problem.</p> <blockquote> <p><strong>Prompt:</strong> Can you fix the following exception that is thrown when scheduleBuffer is called: &ldquo;required condition is false: [AVAudioPlayerNode.mm:740:ScheduleBuffer: (_outputFormat.channelCount == buffer.format.channelCount)]&rdquo;</p> </blockquote> <p>With this, Copilot does make the audio play – by changing the <code>format</code> line in the <code>start</code> function to:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">format</span> <span class="p">=</span> <span class="n">engine</span><span class="p">.</span><span class="n">outputNode</span><span class="p">.</span><span class="n">inputFormat</span><span class="p">(</span><span class="n">forBus</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span> </span></span></code></pre></div><p>However, even though the audio plays, it is mono, in the left speaker only. A proper fix would have been to take the <code>format</code>, as defined in the <code>start</code> function, and use it when connecting the <code>player</code> in the <code>init</code> function. I can do that manually and if I also remove the <code>step</code> from the Frequency slider in the UI (so the app doesn&rsquo;t hang when trying to draw the slider), then the app &ldquo;works&rdquo;.</p> <h3 id="second-effort">Second effort</h3> <p>I want to see if Copilot can handle a bit of cleanup if I give it the steps. Instead of pre-rendering a PCM buffer and looping that, I prefer to use a render function that generates the audio-on-the-fly.</p> <blockquote> <p><strong>Prompt:</strong> Refactor the AudioEngine to generate audio using an AVAudioSourceNode render block that calls a generateSample function. Move the @State variables from the ContentView into the AudioEngine and make the ContentView observe the AudioEngine.</p> </blockquote> <p>Copilot follows all my instructions and the code now ends up <em>looking</em> better but it has gone from &ldquo;working&rdquo; to &ldquo;not working&rdquo;, again. Now the tone plays immediately on application startup and the &ldquo;Play&rdquo; and &ldquo;Stop&rdquo; buttons no longer work.</p> <p>For some reason, alongside the requested changes, Copilot decided to change the <code>start</code> and <code>stop</code> functions to set the <code>engine.mainMixerNode.outputVolume</code> to 1 or 0 (instead of actually pausing the audio generation). This is a bad idea (you should either pause or stop when audio generation is paused by the user). But it doesn&rsquo;t work because in the current state, the <code>mainMixerNode</code> is not being added to the graph at all, so setting volume on it has no effect.</p> <p>Getting a little exhausted with these issues, I resort to manually editing the code: calling <code>start()</code> and <code>stop()</code> on the engine instead of changing the volume, removing the <code>step</code> parameter from the frequency slider that was making it practically unusable and replacing the separate &ldquo;Play&rdquo; and &ldquo;Stop&rdquo; buttons with a single toggle button that changes its label.</p> <p>So far, I&rsquo;ve spent about 20 minutes of cleanup, plus inspection and LLM prompting, on what&rsquo;s less than 200 lines of code. I could probably have written the code myself in this time so I feel like it&rsquo;s a borderline call about whether the LLM is helping at this point.</p> <h2 id="lets-synthesize-a-raindrop-sound">Let&rsquo;s synthesize a raindrop sound</h2> <p>I had an ulterior motive with this entire exercise. I didn&rsquo;t really want a tone generator. I really wanted to try a raindrop sound synthesizer. I had randomly stumbled across GitHub repository <a href="https://github.com/747745124/Raindrop-Generator">https://github.com/747745124/Raindrop-Generator</a> and wanted to try the simple waveform it suggested might sound like a raindrop.</p> <figure><img src="https://www.cocoawithlove.com/assets/blog/raindrop.png" alt="Schematic of the sound produced by a water droplet falling onto a liquid surface. Credit Stanley J. Miklavcic, Andreas Zita and Per Arvidsson" width="500px"><figcaption> <p>Schematic of the sound produced by a water droplet falling onto a liquid surface. Credit <a href="https://www.dafx.de/paper-archive/details/eRUDT2B3rmJZ6w0yVL-fyw">Stanley J. Miklavcic, Andreas Zita and Per Arvidsson</a></p> </figcaption> </figure> <p>Grabbing the &ldquo;drop_v2.hpp&rdquo; file from that repository, I asked Copilot to swap the tone generator for a raindrop generator.</p> <blockquote> <p><strong>Prompt:</strong> The samples generated by generateSample in AudioEngine are a basic tone with either a sine, square or sawtooth waveform. Replace this tone generation with a raindrop sample generation. Use the algorithm in drop_v2.hpp for the raindrop sound.</p> </blockquote> <p>This change broke the compile since it removed a number of parameters that the <code>ContentView</code> referenced but after cleaning that up, this step in the exercise was by far the most impressive help that Copilot was able to give.</p> <p>Ordinarily, when given a new piece of code, in an unfamiliar language and told to integrate the algorithm into an existing codebase, most programmers would need time to process, time to understand and time to feel out the best way to apply.</p> <p>For an LLM, translating from one context to another is their biggest strength. It&rsquo;s no problem that one codebase is C++ and the other is Swift. You could argue that all an LLM <em>ever</em> really does is take the pattern and structure of content absorbed in one context and re-emitting that content in a way that follows on from a new context.</p> <p>However, it&rsquo;s difficult to tell that this code is working from the audio alone, since it is playing just one 20ms raindrop sound and then stopping.</p> <p>We&rsquo;re going to need more drops.</p> <blockquote> <p><strong>Prompt:</strong> The AVAudioSourceNode render function currently plays a single raindrop sound from the rainDrop variable which stops playing once time reaches tInit + deltaT3. This code should be changed so that instead of a single raindrop, there&rsquo;s an array of raindrops. This array should initially contain just one raindrop but more raindrops should be added to the array after random intervals so that a target number of raindrops are added per minute.</p> </blockquote> <p>For reasons that are not clear, Copilot ignored the request for &ldquo;random&rdquo; and instead the drops are being scheduled precisely <code>sampleInterval</code> apart.</p> <p>After plugging in a rough &ldquo;random&rdquo; concept (I don&rsquo;t think I&rsquo;ve done a great job, here) and hooking up some sliders to the new <code>sampleInterval</code> and <code>randomness</code> properties that Copilot has decided to include, the app now looks like this:</p> <figure><img src="https://www.cocoawithlove.com/assets/blog/raingenerator.png" alt="A raindrop synthesizer that actually works" width="500px"><figcaption> <p>A raindrop synthesizer that actually works</p> </figcaption> </figure> <p>I&rsquo;m not sure what that &ldquo;randomness&rdquo; parameter is supposed to be. It&rsquo;s not randomizing the interval between drops and is instead hooked up to a number of internal parameters in a seemingly haphazard way.</p> <h2 id="charting-the-waveform">Charting the waveform</h2> <p>The drops sometimes sound like raindrops on a tent, sometimes like small drops in a water glass and often like someone playing a toy xylophone. If nothing else, I&rsquo;d like to confirm that my waveforms match that in the &ldquo;A simple water droplet waveform&rdquo; graph that I showed, above.</p> <p>To do this, it would be helpful to plot the waveform.</p> <blockquote> <p><strong>Prompt:</strong> I would like to use Swift Charts in the ContentView to show a sample waveform generated by the AudioEngine that updates as the user adjusts the sliders in the ContentView.</p> </blockquote> <p>This did add a Swift Chart but it showed nothing.</p> <p>A little inspection revealed that it was plotting the first 1000 samples&hellip; of a 44kHz waveform, so just 250 microseconds. Additionally, it was trying to plot all raindrops but I wanted the waveform of a single raindrop.</p> <blockquote> <p><strong>Prompt:</strong> For the generateWaveform function, just generate the waveform from a single raindrop, created locally from the createNewRaindrop function.</p> </blockquote> <p>Again, some cleanup was needed. I also decided to color code things and hook up sliders to all the key variables (asking Copilot to do this just made a mess).</p> <p>The end result though is quite nice:</p> <figure><img src="https://www.cocoawithlove.com/assets/blog/chart-waveform.png" alt="The waveform of the synthesized raindrop" width="500px"><figcaption> <p>The waveform of the synthesized raindrop</p> </figcaption> </figure> <p>This confirms that the synthesized raindrop is approximately the same as the target waveform.</p> <h2 id="adding-some-noise">Adding some noise</h2> <p>In terms of synthesizing the sound of a raindrop – it&rsquo;s not great. I can tweak the parameters but it never really sounds like a raindrop and definitely never sounds like rain.</p> <p>Most people who synthesize rain generally start with a noise generator. There are different types of noise (white, pink, brown). While I know how to write white noise, I&rsquo;m not sure about brown and don&rsquo;t have any knowledge of pink noise.</p> <p>So I make Copilot do it.</p> <blockquote> <p><strong>Prompt:</strong> Add pink noise to the audio generated by the AudioEngine.</p> </blockquote> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">generatePinkNoise</span><span class="p">()</span> <span class="p">-&gt;</span> <span class="nb">Float</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">white</span> <span class="p">=</span> <span class="nb">Float</span><span class="p">.</span><span class="n">random</span><span class="p">(</span><span class="k">in</span><span class="p">:</span> <span class="o">-</span><span class="mf">1.0</span><span class="p">...</span><span class="mf">1.0</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">pinkNoiseState</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="p">=</span> <span class="mf">0.99886</span> <span class="o">*</span> <span class="n">pinkNoiseState</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">+</span> <span class="n">white</span> <span class="o">*</span> <span class="mf">0.0555179</span> </span></span><span class="line"><span class="cl"> <span class="n">pinkNoiseState</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="p">=</span> <span class="mf">0.99332</span> <span class="o">*</span> <span class="n">pinkNoiseState</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">+</span> <span class="n">white</span> <span class="o">*</span> <span class="mf">0.0750759</span> </span></span><span class="line"><span class="cl"> <span class="n">pinkNoiseState</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="p">=</span> <span class="mf">0.96900</span> <span class="o">*</span> <span class="n">pinkNoiseState</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">+</span> <span class="n">white</span> <span class="o">*</span> <span class="mf">0.1538520</span> </span></span><span class="line"><span class="cl"> <span class="n">pinkNoiseState</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span> <span class="p">=</span> <span class="mf">0.86650</span> <span class="o">*</span> <span class="n">pinkNoiseState</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span> <span class="o">+</span> <span class="n">white</span> <span class="o">*</span> <span class="mf">0.3104856</span> </span></span><span class="line"><span class="cl"> <span class="n">pinkNoiseState</span><span class="p">[</span><span class="mi">4</span><span class="p">]</span> <span class="p">=</span> <span class="mf">0.55000</span> <span class="o">*</span> <span class="n">pinkNoiseState</span><span class="p">[</span><span class="mi">4</span><span class="p">]</span> <span class="o">+</span> <span class="n">white</span> <span class="o">*</span> <span class="mf">0.5329522</span> </span></span><span class="line"><span class="cl"> <span class="n">pinkNoiseState</span><span class="p">[</span><span class="mi">5</span><span class="p">]</span> <span class="p">=</span> <span class="o">-</span><span class="mf">0.7616</span> <span class="o">*</span> <span class="n">pinkNoiseState</span><span class="p">[</span><span class="mi">5</span><span class="p">]</span> <span class="o">-</span> <span class="n">white</span> <span class="o">*</span> <span class="mf">0.0168980</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">pink</span> <span class="p">=</span> <span class="n">pinkNoiseState</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">+</span> <span class="n">pinkNoiseState</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">+</span> <span class="n">pinkNoiseState</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">+</span> <span class="n">pinkNoiseState</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span> <span class="o">+</span> <span class="n">pinkNoiseState</span><span class="p">[</span><span class="mi">4</span><span class="p">]</span> <span class="o">+</span> <span class="n">pinkNoiseState</span><span class="p">[</span><span class="mi">5</span><span class="p">]</span> <span class="o">+</span> <span class="n">pinkNoiseState</span><span class="p">[</span><span class="mi">6</span><span class="p">]</span> <span class="o">+</span> <span class="n">white</span> <span class="o">*</span> <span class="mf">0.5362</span> </span></span><span class="line"><span class="cl"> <span class="n">pinkNoiseState</span><span class="p">[</span><span class="mi">6</span><span class="p">]</span> <span class="p">=</span> <span class="n">white</span> <span class="o">*</span> <span class="mf">0.115926</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">pink</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>It got this right, first time. I think. Honestly, I don&rsquo;t know what pink noise is supposed to do.</p> <p>Looking around to see how pink noise generation should look, Copilot appears to have chosen an implementation of the &ldquo;Voss-McCartney algorithm&rdquo; for pink noise and <a href="https://www.firstpr.com.au/dsp/pink-noise/">it has a fairly long history</a>.</p> <p>Further adding brown noise and white noise and hooking up some sliders gives the final form of the app:</p> <figure><img src="https://www.cocoawithlove.com/assets/blog/raingenerator-plusnoise.png" alt="Raindrop and noise generator" width="500px"><figcaption> <p>Raindrop and noise generator</p> </figcaption> </figure> <h2 id="cleaning-up-thread-safety">Cleaning up thread safety</h2> <p>It seems that the <code>AVAudioSourceNode</code> closure is missing an <code>@Sendable</code> annotation so even though Swift 6 doesn&rsquo;t give any warnings, the code at this point is thread unsafe. In fact, if you annotate <code>AudioEngine</code> with <code>@MainActor</code>, you&rsquo;ll get a fatal error at runtime when the render closure is invoked, indicating that Swift 6 is under the (incorrect) assumption that the closure will run in the same context as the surrounding type.</p> <p>I don&rsquo;t think there&rsquo;s anything that would crash the app if the thread safety is left unaddressed – just some parameters that may be invalid or fail to update correctly – but it&rsquo;s not a good idea to write to the parameters on the main thread and read them from the audio render thread without some form of synchronization.</p> <p>I asked Copilot to fix the problem:</p> <blockquote> <p><strong>Prompt:</strong> To ensure thread safety in the AudioEngine class, apply these steps:<br> <br> Add the @MainActor annotation to the AudioEngine class.<br> Add the @Sendable annotation to the AVAudioSourceNode closure.<br> Add the nonisolated keyword to the generateAudio function.<br> Use <code>Atomic</code> values to copy parameters from the main actor to the generateAudio function.</p> </blockquote> <p>Copilot ignored the request for an <code>@Sendable</code> annotation and used an <code>NSLock</code> instead of <code>Atomic</code> so it wasn&rsquo;t exactly what I wanted. I can&rsquo;t <em>really</em> fault Copilot since I&rsquo;m also not sure how to correctly use <code>Atomic</code> to solve this problem. In the end, the final commit in this repository (applied manual during a final cleanup phase) adds thread safety via Swift&rsquo;s new <code>Mutex</code> type. I&rsquo;m not completely happy with that (you don&rsquo;t generally want to deal with locks on the audio processing thread) but I&rsquo;m not sure what the best pattern for exchanging data via <code>Atomic</code> would be.</p> <h2 id="conclusion">Conclusion</h2> <blockquote> <p><strong>Check it out</strong>: the final product plus most of the intermediate steps discussed in this article are available from the <a href="https://github.com/mattgallagher/RainGenerator">RainGenerator repository</a>.</p> </blockquote> <h3 id="is-it-a-good-rain-synthesizer">Is it a good rain synthesizer?</h3> <p>Not really but it was fun to play with, especially once I could see the waveform via the chart.</p> <p>It turns out that the rough waveform <a href="https://www.dafx.de/paper-archive/details/eRUDT2B3rmJZ6w0yVL-fyw">came from a paper by Stanley J. Miklavcic; Andreas Zita; Per Arvidsson</a> which goes more into depth about necessary randomization, distribution and spatial effects in order make the raindrops sound more real and even then concludes that other water, flowing and storm sounds are required to fill out the complete soundscape of rain.</p> <h3 id="what-was-copilot-good-at">What was Copilot good at?</h3> <p>I asked Copilot to perform a number of refactoring passes and it was generally good at following instructions and cleaning up the integration points. This project is less than 500 lines though, so I don&rsquo;t know how well it might scale to a more functional sized app.</p> <p>For SDKs where I wasn&rsquo;t familiar, it was quicker to give Copilot a vague description of intent than to look up the documentation, read the documentation and integrate into the surrounding context. This is a big win and might help to eliminate the trepidation involved in working with new SDKs.</p> <p>Copilot was <em>really</em> good at integrating existing code from another location, even when that code was in a different programming language.</p> <p>It was also <em>really</em> good at looking up how to implement a relatively obscure algorithm like pink noise, even when it&rsquo;s not an algorithm where the first result in a web search will give a good response.</p> <h3 id="ways-in-which-copilot-was-better-than-expected-but-not-great">Ways in which Copilot was better than expected but not great</h3> <p>I never had a syntax error.</p> <p>I expected more hallucinations or references to non-existent symbols. I really only experienced one variable that didn&rsquo;t exist (from the Claude chatbot) and one function that didn&rsquo;t exist (from Copilot). The non-existant function was <code>Float.clamped(to: ClosedRange&lt;Float&gt;)</code> – which I simply implemented as it&rsquo;s likely just an extension that Copilot has trained on without realizing it&rsquo;s not a Swift extension.</p> <p>Other compilation errors I experienced were generally minor (e.g. use of iOS only APIs on the Mac and a use of a variable before the init function had finished initializing) or an update to one file that breaks another file not included in the chat context.</p> <h3 id="what-was-copilot-bad-at">What was Copilot bad at?</h3> <p>I immediately wanted to make minor changes to almost everything Copilot wrote.</p> <p>The most common problems were violations of &ldquo;Don&rsquo;t repeat yourself&rdquo;, &ldquo;Keep it simple, stupid&rdquo;, missing abstractions or unneeded indirection. Just about every common coding problem was there.</p> <p>Copilot would repeat code a lot in SwiftUI with the same set of modifiers applied without any desire to avoid repetition. Copilot would also do goofy things like adding <code>didSet</code> or <code>onChange</code> handlers everywhere instead of simply changing the underlying data without a layer of indirection.</p> <p>I ended up manually adding <code>AudioEngine</code> subtypes – <code>Raindrop</code>, <code>Parameters</code> and <code>GeneratorState</code> – to encapsulate parameters and state and simplify observing and updates because Copilot doesn&rsquo;t seem to do any such tidy ups without explicit instruction.</p> <p>As discussed, Copilot left the audio graph incompletely connected, failed to get audio formats aligned between functions. Unless two functions are directly interfacing, it doesn&rsquo;t seem to check if they&rsquo;re doing anything related that should be made common.</p> <p>In some requests, it would outright ignore part of the instructions like &ldquo;random intervals&rdquo; or &ldquo;add @Sendable&rdquo; to this closure. Other requests like &ldquo;tidy up the user interface so things are more compact and aligned&rdquo; don&rsquo;t seem to have any practical effect (though Copilot might respond by adding a bunch of repetitive modifiers like <code>onChange</code> to each slider for no reason).</p> <p>Copilot never seemed to work out that I was writing a Mac app and kept trying to stick a green background with corner radius inside the Mac &ldquo;bordered&rdquo; buttons (really unsightly) and it tried color everything <code>systemGray6</code> (I&rsquo;m not sure why a color should be iOS-only but it is). Perhaps this is my fault for failing to inform the bot about the compile target but it&rsquo;s just another way you need to work to keep an LLM assistant behaving correctly.</p> <figure><img src="https://www.cocoawithlove.com/assets/blog/ugly-green-button.png" alt="This Mac button already has a border, and even if this was iOS, that&rsquo;s probably too much green" width="500px"><figcaption> <p>This Mac button already has a border, and even if this was iOS, that&rsquo;s probably too much green</p> </figcaption> </figure> <p>Languages and SDKs evolve pretty quickly and Copilot still seems to prefer <code>@ObservableObject</code> over <code>@Observable</code> and seems largely unaware of Swift 6, <code>@Sendable</code>, <code>Atomic</code> and other changes in the last 6 months. You&rsquo;re limited to its training set and that&rsquo;s never going to include as much &ldquo;upcoming&rdquo; code as &ldquo;legacy&rdquo; code.</p> <h3 id="problems-with-vs-code--copilot-for-swift">Problems with VS Code + Copilot for Swift</h3> <p>Rounding out the negatives are a number of ergonomic problems related to VS Code and its current integration of Copilot.</p> <p>Any time I manually made changes to the code there&rsquo;s the problem that this can make the code out-of-sync with the chat window so if you do ask Copilot for more changes, it may behave like your manual changes don&rsquo;t exist and revert them in its next refactor creating a situation where bugs and entire previous states of the app may reappear.</p> <p>With code naturally spanning multiple files, it&rsquo;s easy to forget to include all files in the chat context and have refactoring changes break the connections between two files. What I&rsquo;d really like is an IDE that automatically detected which files and functions are actually relevant to the context and automatically include them with the request.</p> <p>And finally, VS Code isn&rsquo;t Xcode, so it&rsquo;s not an environment focussed on Swift development. Another developer might be familiar with VS Code but I&rsquo;m not and I found myself bouncing between IDEs a lot.</p> <h3 id="was-copilot-worth-it">Was Copilot worth it?</h3> <p>Sometimes, yes. But not always and therein lies an estimation risk. Is it saving me time by writing code I need or wasting my time by making a mess or adding code I don&rsquo;t want?</p> <p>It can be hard to guess whether it would be faster to ask Copilot to improve its own code or to skip Copilot and apply the fix manually. Just typing out a detailed request for changes can take longer than making the changes myself – largely because I&rsquo;m really accustomed to editing code but I find that being accurate in prose is a slow process. Trying to clean up code details by talking in chat with Copilot can start to feel like a sunk cost problem. Combined with the above-mentioned complication that blending Copilot and manual changes can cause chat to lose sync with the code, it becomes exhausting.</p> <p>Working with an LLM assistant really does feel like training a very keen but very messy new developer – totally happy to write 1000 lines of code to solve a problem that should be solved in 100 lines and when the PR comes in, you need to slowly walk them through all the things they need to change before you can feel like the code is maintainable.</p> <p>There is a big difference though: with the new developer, the time taken to help them fix their code isn&rsquo;t time wasted. They&rsquo;ll improve, get accustomed to the patterns and expectations of the codebase and in 6 to 12 months their PRs will be easier to review and they&rsquo;ll start fixing your dumb mistakes.</p> <p>By contrast: Copilot is not learning from my suggestions. There&rsquo;s nothing to be won by making it clean up its own mess if a manual change would have been faster.</p> <br/>Copyright Matt Gallagher, 2024. All rights reserved. Code samples may be use in accordance with the ISC-style license at https://www.cocoawithlove.com/about.html App architecture basics in SwiftUI Part 4: Services https://www.cocoawithlove.com/blog/separated-services-layer.html Tue, 23 Mar 2021 08:37:06 +1000 https://www.cocoawithlove.com/blog/separated-services-layer.html <p>This article is about adding a separated Services-layer to an app. A Services-layer is, in my opinion, the single best app architectural addition you can make, after the basic Model-View separation already implicit in SwiftUI. Model-View separation decouples the Model from the front-end but a Services-layer completes Model separation by separating the Model from the back-end and is one of the few app architectural changes you can point at and say: this change unlocks these capabilities.</p> <p>I&rsquo;ll be changing just 9 lines from the <a href="https://www.cocoawithlove.com/blog/app-submodules.html">127 line CwlFeedReader app from the previous article</a> (plus a few lines of boilerplate in new files). It&rsquo;s a relatively small change but it&rsquo;s a topic with plenty of different opinions about the best approach so there&rsquo;s a lot of discuss.</p> <!-- TOC --> <h2 id="lets-try-to-add-tests-to-cwlfeedreader">Let&rsquo;s try to add tests to CwlFeedReader</h2> <p>Before we look in detail at what I mean by a &ldquo;separated Services-layer&rdquo;, I&rsquo;d like to start by looking at the biggest capability this change will unlock: testing.</p> <p>Let&rsquo;s assume we want to do the bare minimum in testing. This generally means interface tests on the <code>Model</code>. By &ldquo;interface&rdquo;, I&rsquo;m referring to the public functions of the Model (the <em>module&rsquo;s</em> interface, not the user-interface). This type of test can detect major regressions (functionality that is accidentally broken when you add new features) and is a valuable addition to any app project due to its low time cost.</p> <p>The interface to the <code>Model</code> looks like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">public</span> <span class="kd">class</span> <span class="nc">Model</span><span class="p">:</span> <span class="n">ObservableObject</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="p">@</span><span class="n">Published</span> <span class="kd">public</span> <span class="kd">private</span><span class="p">(</span><span class="kr">set</span><span class="p">)</span> <span class="kd">var</span> <span class="nv">feed</span><span class="p">:</span> <span class="n">Feed</span><span class="p">?</span> </span></span><span class="line"><span class="cl"> <span class="p">@</span><span class="n">Published</span> <span class="kd">public</span> <span class="kd">var</span> <span class="nv">error</span><span class="p">:</span> <span class="n">IdentifiableError</span><span class="p">?</span> </span></span><span class="line"><span class="cl"> <span class="p">@</span><span class="n">Published</span> <span class="kd">public</span> <span class="kd">private</span><span class="p">(</span><span class="kr">set</span><span class="p">)</span> <span class="kd">var</span> <span class="nv">isReadStatuses</span><span class="p">:</span> <span class="p">[</span><span class="n">URL</span><span class="p">:</span> <span class="nb">Bool</span><span class="p">]</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">public</span> <span class="kd">init</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="kd">public</span> <span class="kd">func</span> <span class="nf">setIsRead</span><span class="p">(</span><span class="kc">_</span> <span class="n">value</span><span class="p">:</span> <span class="nb">Bool</span><span class="p">,</span> <span class="n">url</span><span class="p">:</span> <span class="n">URL</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">public</span> <span class="kd">func</span> <span class="nf">reload</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>To test this interface, we need tests that call each of:</p> <ol> <li><code>init</code></li> <li><code>setIsRead</code> and</li> <li><code>reload</code></li> </ol> <p>that validate the behavior of these functions by reading the changes to <code>feed</code>, <code>error</code> and <code>isReadStatuses</code>.</p> <p>I&rsquo;ve written tests for all 3 functions in <a href="https://github.com/mattgallagher/CwlFeedReader/tree/part-four-broken-tests">the CwlFeedReader repository</a> but I want to focus on the test for the most important function in the program, <code>reload</code>. There&rsquo;s a complication that <code>reload</code> is called from inside <code>init</code> so I&rsquo;m going to simply construct <code>Model()</code> and – relying on the fact that <code>reload</code> delivers its results <em>asychronously</em> to the main thread – I will then examine the <em>second</em> value received via <code>$feed</code> and treat this as equivalent to the result of calling <code>reload</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">feedFirstURL</span> <span class="p">=</span> <span class="n">URL</span><span class="p">(</span><span class="n">string</span><span class="p">:</span> <span class="s">&#34;https://www.cocoawithlove.com/blog/swiftui-natural-pattern.html&#34;</span><span class="p">)</span><span class="o">!</span> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">testReload</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// Given a newly inited model and an expectation that stops on the second feed value</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">model</span> <span class="p">=</span> <span class="n">Model</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">secondValue</span> <span class="p">=</span> <span class="n">expectation</span><span class="p">(</span><span class="n">description</span><span class="p">:</span> <span class="s">&#34;feed should emit 2 values.&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">cancellable</span> <span class="p">=</span> <span class="n">model</span><span class="p">.</span><span class="err">$</span><span class="n">feed</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="bp">dropFirst</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">sink</span> <span class="p">{</span> <span class="kc">_</span> <span class="k">in</span> <span class="n">secondValue</span><span class="p">.</span><span class="n">fulfill</span><span class="p">()</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// When the automatically invoked `reload()` completes</span> </span></span><span class="line"><span class="cl"> <span class="n">wait</span><span class="p">(</span><span class="k">for</span><span class="p">:</span> <span class="p">[</span><span class="n">secondValue</span><span class="p">],</span> <span class="n">timeout</span><span class="p">:</span> <span class="mf">30.0</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">cancellable</span><span class="p">.</span><span class="n">cancel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// Then the first feed URL should be the expected swiftui-natural-pattern.html</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">model</span><span class="p">.</span><span class="n">feed</span><span class="p">?.</span><span class="n">items</span><span class="p">.</span><span class="bp">map</span><span class="p">(</span><span class="err">\</span><span class="p">.</span><span class="n">url</span><span class="p">).</span><span class="bp">first</span><span class="p">,</span> <span class="n">feedFirstURL</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Everything looks good, right? I mean: there&rsquo;s no bug in the code. Not exactly.</p> <blockquote> <p>These tests are available from the <a href="https://github.com/mattgallagher/CwlFeedReader/tree/part-four-broken-tests">part-four-broken-tests branch of the CwlFeedReader repository</a></p> </blockquote> <p>The test fails.</p> <p>Why? Because that <code>feedFirstURL</code> value was correct in late January when I wrote the test but it has already broken because the Cocoa with Love JSON feed has updated since then. We wanted this test to detect regression in the <code>reload</code> function but instead, we&rsquo;ve merely detected that live-data has changed. This test is a waste of our time.</p> <p>But the test isn&rsquo;t the source of the problem, it is merely a symptom.</p> <p>The true source of the problem is that it is not possible to isolate the code we&rsquo;ve written from dependencies we use (the state of the network and data received from remote servers). If the upstream data changes, our tests fail. If the network is down, our tests fail. There are potentially worse problems that could occur if our tests overwhelm production servers or if our tests mistakenly change live data.</p> <p>And even if live data isn&rsquo;t a problem, our tests are <em>much slower</em> than they should be. The test relies on asynchronous I/O, causing the test to take between a few hundred milliseconds and a few seconds, instead of a couple microseconds. While this may seem like the least significant problem here, this adds up. Even small apps can have thousands of tests. Hundreds of milliseconds per test can become <em>hours</em>.</p> <p>The conclusion here is straightforward: if you can&rsquo;t isolate your application from the outside world, tests are practically useless. Even if you can endure unreliability, the best you can achieve are some basic app-server integration smoke tests. Any other kind of testing will be impossible.</p> <h2 id="the-problem-is-broader-than-testing">The problem is broader than testing</h2> <p>It&rsquo;s not just tests that are facing problems. We can&rsquo;t do any useful work on the app if dependencies are unavailable. And dependencies are often, by their nature, beyond our control. If the server goes down, we&rsquo;re blocked. If the server APIs are not finished, we&rsquo;re blocked. Want to run a demo in a location without network access? You can&rsquo;t.</p> <p>Even the SwiftUI previews in CwlFeedReader construct the <code>Model()</code> and access the network for their content. Our SwiftUI previews can fail or misbehave when the network is unavailable. If we can&rsquo;t usefully edit our <em>views</em> without working dependencies, then we&rsquo;re in trouble.</p> <p>Availability isn&rsquo;t the only problem. Configuration of data is also a limitation. Want to debug a situation that requires specific data or user states? You&rsquo;ll have to manually create those users and states each time.</p> <p>These are the symptoms of an app that we can&rsquo;t isolate from its dependencies but there&rsquo;s another, more conceptual problem: we don&rsquo;t even have a <em>description</em> of our app without its dependencies.</p> <p>Yes, the <em>user</em> sees the Cocoa with Love JSON feed presented as a <code>SwiftUI</code> <code>List</code> and <code>WKWebView</code> but that&rsquo;s not what&rsquo;s in our Model-layer. What the user <em>sees</em> is a product of the data from connected <em>dependencies</em> flowing through the app.</p> <p>If we remove all dependencies from consideration (including the View-layer), we&rsquo;ve written a program that:</p> <ol> <li>fetches a JSON list containing identifiers, including error handling on fetch</li> <li>can re-fetch the JSON list when requested</li> <li>fetches a dictionary of booleans, keyed by the same identifiers in the JSON list</li> <li>can update and save the booleans for each identifier when requested</li> </ol> <p>This could be the description of a to-do list app, an email client or a control app for a network-connected set of light-switches. The only difference is the dependencies we reference.</p> <p>If we control how we reference our dependencies, we can develop, test and use our program with precision, be flexible between problem domains, and we can work without getting blocked.</p> <h2 id="putting-it-in-a-diagram">Putting it in a diagram</h2> <p>Here&rsquo;s a diagram of the CwlFeedReader app, showing the commonly identified Model-View-Controller components. Solid lines show references (use of concrete types or functions), dotted lines show data flow without reference. No solid arrow should ever point away from the Model – that would indicate a violation of Model isolation.</p> <figure><img src="https://www.cocoawithlove.com/assets/blog/mvc_minus_services.svg" alt="MVC representation of CwlFeedReader (solid lines are explicit references, dotted lines are decoupled interactions)" width="500px"><figcaption> <p>MVC representation of CwlFeedReader (solid lines are explicit references, dotted lines are decoupled interactions)</p> </figcaption> </figure> <p>This diagram isn&rsquo;t <em>wrong</em> but it shows only the front-end of the Model-layer. Here&rsquo;s a more complete architectural diagram of the CwlFeedReader app including the back-end:</p> <figure><img src="https://www.cocoawithlove.com/assets/blog/mvc_services.svg" alt="MVC&#43;Services representation of CwlFeedReader (solid lines are explicit references, dotted lines are decoupled interactions)" width="700px"><figcaption> <p>MVC+Services representation of CwlFeedReader (solid lines are explicit references, dotted lines are decoupled interactions)</p> </figcaption> </figure> <p>Immediately, you should be able to see the problem: there&rsquo;s a solid arrow pointing away from the Model. Our Model is not isolated because it directly references <code>URLSession</code> and <code>UserDefaults</code> – sources of side-effects that affect the app.</p> <p>In the same way that we need to maintain separation between the Model and the Controller+View components at the front-end, we need to maintain separation between the Model and these side-effects at the back end or we compromise our ability to test, isolate and reason about our own application.</p> <h2 id="some-clarification-around-the-word-services">Some clarification around the word &ldquo;Services&rdquo;</h2> <p>There&rsquo;s a few definitions of &ldquo;services&rdquo; around. The definition I&rsquo;m using is:</p> <blockquote> <p><strong>Service</strong>: any type or function that provides <em>side effects</em> that can affect the testable behavior of the Model-layer</p> </blockquote> <div class="aside">A <a href="https://en.wikipedia.org/wiki/Side_effect_(computer_science)">side effect</a> is a function (including methods and computed values) that might change its behavior despite being called with the same parameters. Typical examples are anything read from the network, disk, clock or operating system. In some cases, simply asynchronously returning results is side-effect (since it may change the ordering of results).</div> <p>The difficulty with services is that they&rsquo;re dependent on the state of the things (network, disk, other processes running on the system). For testing and development purposes, we want to keep all of this state constant so we can focus on whether our program has changed behavior.</p> <p>Identification of services is only the first step. The second step is to move them into a Services-layer:</p> <blockquote> <p><strong>Services-layer</strong>: a collection of the app&rsquo;s services and third-party dependencies, isolated so they are never directly referenced by the Model-layer, and replaceable at test or debug time with side-effect-free versions that offer robustly repeatable results.</p> </blockquote> <div class="aside">The term &ldquo;dependency injection&rdquo; is commonly used to describe separation from dependencies. The term can accurately be applied to my approach but I tend to downplay this description due to its association with &ldquo;dependency injection frameworks&rdquo; (an approach that I avoid, as I&rsquo;ll discuss later).</div> <p>Notice that I&rsquo;ve added &ldquo;third-party dependencies&rdquo;, along with &ldquo;app&rsquo;s services&rdquo; in the Services-layer. The purpose of a Services-layer is that it lets the app swap components out at launch – good for removing services for testing, also good for removing third-party dependencies for updates, refactoring or replacements.</p> <h2 id="how-can-we-decouple-the-model-from-services">How can we decouple the Model from Services?</h2> <p>I want to separate the Model interface from the Services interface. Separating two interfaces from each other is a process usually called &ldquo;decoupling&rdquo;. Where do we draw a line for this separation?</p> <p>Let&rsquo;s consider the network-data request pipeline that involves <code>URLSession</code> in the CwlFeedReader app. The major features of the pipeline are:</p> <ol> <li>the Cocoa with Love domain name</li> <li>use of the <code>URLSession</code> type</li> <li>use of the relative path for the feed.json file, parameters and JSON encoding/decoding to marshal parameters to <code>URLSession</code></li> </ol> <p>It&rsquo;s possible to separate an app along any of these 3 lines. I consider only one of these choices &ldquo;correct&rdquo; but let&rsquo;s look at the arguments used in favor of each.</p> <h3 id="1-decouple-from-production-server-using-different-environments">1. Decouple from production server using different environments</h3> <p>It&rsquo;s common to find apps that don&rsquo;t change their <em>code</em> but change their <em>data</em> and re-point their app at a stub-server when testing or debugging.</p> <p>For the CwlFeedReader app, you could imagine the <code>Model</code> taking an API base string in its <code>init</code> function (e.g. <code>https://www.cocoawithlove.com</code>) and at test time, passing a value like <code>https://localhost:8080</code> as an alternate argument for this parameter. Assuming you&rsquo;re running a webserver on port 8080 on the same machine, you could place a fixed version of the &ldquo;feed.json&rdquo; file at the same relative path as on the production server.</p> <h4 id="pros">Pros</h4> <p>This approach lets you test every single line of code and, if retro-fitted to an existing codebase, requires the least effort.</p> <h4 id="cons">Cons</h4> <p>This approach has many limitations:</p> <ul> <li>Works for network dependencies but is harder to apply to other dependencies (like <code>UserDefaults</code>) so it isn&rsquo;t really usable for general service isolation.</li> <li>Even for network dependencies, it decouples network data but not network APIs. Our app can&rsquo;t swap to a different network library or data source using this approach.</li> <li>You need to start the stub server before testing, meaning that your tests don&rsquo;t &ldquo;just work&rdquo; without extra setup.</li> <li>Switching configurations on the server for testing requires side-channel configuration of the server.</li> <li>Stub servers can be complex entities, often in another language, so they add additional learning overheads for Swift developers</li> </ul> <h3 id="2-put-a-protocol-around-the-smallest-possible-subset-of-urlsession">2. Put a protocol around the smallest possible subset of URLSession</h3> <p>Another approach is to decouple the dependency in code but to keep this effort as simple as possible. In Swift, this means: create a protocol that describes our direct usage of each dependency.</p> <p>We could define the following protocol:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">public</span> <span class="kd">protocol</span> <span class="nc">NetworkService</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span> <span class="nf">fetchData</span><span class="p">(</span><span class="n">with</span><span class="p">:</span> <span class="n">URLRequest</span><span class="p">,</span> <span class="n">handler</span><span class="p">:</span> <span class="p">@</span><span class="n">escaping</span> <span class="p">(</span><span class="n">Data</span><span class="p">?,</span> <span class="n">URLResponse</span><span class="p">?,</span> <span class="n">Error</span><span class="p">?)</span> <span class="p">-&gt;</span> <span class="nb">Void</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="n">AnyCancellable</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>and provide the following implementation to wrap our use of <code>URLSession</code> completely in a protocol.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">extension</span> <span class="nc">URLSessionDataTask</span><span class="p">:</span> <span class="n">Cancellable</span> <span class="p">{}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">extension</span> <span class="nc">URLSession</span><span class="p">:</span> <span class="n">NetworkService</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">public</span> <span class="kd">func</span> <span class="nf">fetchData</span><span class="p">(</span><span class="n">with</span> <span class="n">request</span><span class="p">:</span> <span class="n">URLRequest</span><span class="p">,</span> <span class="n">handler</span><span class="p">:</span> <span class="p">@</span><span class="n">escaping</span> <span class="p">(</span><span class="n">Data</span><span class="p">?,</span> <span class="n">URLResponse</span><span class="p">?,</span> <span class="n">Error</span><span class="p">?)</span> <span class="p">-&gt;</span> <span class="nb">Void</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="n">AnyCancellable</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">task</span> <span class="p">=</span> <span class="n">dataTask</span><span class="p">(</span><span class="n">with</span><span class="p">:</span> <span class="n">request</span><span class="p">,</span> <span class="n">completionHandler</span><span class="p">:</span> <span class="n">handler</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">task</span><span class="p">.</span><span class="n">resume</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">AnyCancellable</span><span class="p">(</span><span class="n">task</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>If we only use <code>URLSession</code> through this protocol, then we become decoupled from <code>URLSession</code> as a dependency and source of side-effects.</p> <h4 id="pros-1">Pros</h4> <p>Wrapping dependencies in protocols is relatively simple and the resulting code maintains the same basic structure as if it were using <code>URLSession</code>.</p> <h4 id="cons-1">Cons</h4> <p>Doesn&rsquo;t wrap <code>URLRequest</code> or handle other details. We still need to configure details like path and headers and handle work like JSON decoding so it&rsquo;s more like we need to say &ldquo;fetch a JSON list whose location is defined by this URLRequest&rdquo; instead of the simple &ldquo;fetch the feed&rdquo;.</p> <h3 id="3-expand-the-protocol-boundary-to-include-related-busy-work">3. Expand the protocol boundary to include related busy-work</h3> <p>Marshalling parameters for network requests, filling in headers, selecting endpoints, decoding responses; this is all tedious work.</p> <p>We could define the <code>NetworkService</code> to have an interface that matches what our Model needs.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">public</span> <span class="kd">protocol</span> <span class="nc">NetworkService</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span> <span class="nf">fetchFeed</span><span class="p">(</span><span class="n">handler</span><span class="p">:</span> <span class="p">@</span><span class="n">escaping</span> <span class="p">(</span><span class="n">Result</span><span class="p">&lt;</span><span class="n">Feed</span><span class="p">,</span> <span class="n">Error</span><span class="p">&gt;)</span> <span class="p">-&gt;</span> <span class="nb">Void</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="n">AnyCancellable</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>This creates a much simpler interface – no <code>URLRequest</code>, no JSON decoding, just the simple transaction that the Model desires.</p> <h4 id="pros-2">Pros</h4> <p>Matches our Model&rsquo;s &ldquo;fetch feed&rdquo; intent and makes the service feel like a clean interface.</p> <h4 id="cons-2">Cons</h4> <p>Adding different interfaces for each kind of request increases the service size – and remember, most services are implemented <em>twice</em> (once for the production services, once for the testing services) so this can be a significant additional burden.</p> <p>Services themselves aren&rsquo;t testable so putting more code into the service reduces potential test coverage. We&rsquo;re putting Model-logic (correct headers for different endpoints, HTTP message encoding) in the untestable services.</p> <p>Not reusable between different applications because it includes Model-specific logic.</p> <h3 id="personal-opinion">Personal opinion</h3> <p>I think (2) is the best option: wrap dependencies with side-effects in a protocol that <em>minimally</em> wraps the features of the dependency that your app uses. You can change the interface slightly (as I changed <code>dataTask</code> to remove <code>URLSesionDataTask</code> from the interface) but such changes should be focussed on simplification without inclusion of <em>any</em> Model data or logic.</p> <p>Using a stub server, as in option (1), just doesn&rsquo;t solve enough problems and the problems it does solve aren&rsquo;t solved cleanly. It can be good as a stop-gap if you have no other option and can serve other purposes (like cross-platform validation) but keeping it running on test machines is annoying and it will never deliver a clean solution across all dependencies.</p> <p>At the other end, putting app-specific logic into a service (3) is a mistake. Model-logic should be tested but services aren&rsquo;t testable (they&rsquo;re supposed to a dependency, not <em>your</em> code). And the goal of (3) – to improve the abstraction of server communication – can be done by wrapping your services <em>inside</em> your Model-layer, translating from simplified model concepts (like &ldquo;feed&rdquo;) into expanded model details (like the <code>URLRequest</code> and the decoding of its result).</p> <h2 id="using-the-services-layer-from-the-model">Using the Services-layer from the Model</h2> <p>Taking option (2) and wrapping our services as minimally as possible makes including it in existing code relatively painless. We can bundle all of the apps services together in a single struct:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">public</span> <span class="kd">struct</span> <span class="nc">Services</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">networkService</span><span class="p">:</span> <span class="n">NetworkService</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">keyValueService</span><span class="p">:</span> <span class="n">KeyValueService</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>With an instance of this struct available in the <code>Model</code>, we can replace the following use of <code>URLSession</code>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">task</span> <span class="p">=</span> <span class="n">URLSession</span><span class="p">.</span><span class="n">shared</span><span class="p">.</span><span class="n">dataTask</span><span class="p">(</span><span class="n">with</span><span class="p">:</span> <span class="n">request</span><span class="p">)</span> <span class="p">{</span> <span class="n">data</span><span class="p">,</span> <span class="n">response</span><span class="p">,</span> <span class="n">error</span> <span class="k">in</span> <span class="cm">/* ... */</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="n">task</span><span class="p">?.</span><span class="n">resume</span><span class="p">()</span> </span></span></code></pre></div><p>with</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">task</span> <span class="p">=</span> <span class="n">services</span><span class="p">.</span><span class="n">networkService</span><span class="p">.</span><span class="n">fetchData</span><span class="p">(</span><span class="n">with</span><span class="p">:</span> <span class="n">request</span><span class="p">)</span> <span class="p">{</span> <span class="n">data</span><span class="p">,</span> <span class="n">response</span><span class="p">,</span> <span class="n">error</span> <span class="k">in</span> <span class="cm">/* ... */</span> <span class="p">}</span> </span></span></code></pre></div><p>Replacing <code>UserDefaults</code> is similar:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">UserDefaults</span><span class="p">.</span><span class="n">standard</span><span class="p">.</span><span class="kr">set</span><span class="p">(</span><span class="k">try</span><span class="p">?</span> <span class="n">JSONEncoder</span><span class="p">().</span><span class="n">encode</span><span class="p">(</span><span class="n">isReadStatuses</span><span class="p">),</span> <span class="n">forKey</span><span class="p">:</span> <span class="s">&#34;isReadStatuses&#34;</span><span class="p">)</span> </span></span></code></pre></div><p>becomes:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">services</span><span class="p">.</span><span class="n">keyValueService</span><span class="p">[</span><span class="n">key</span><span class="p">:</span> <span class="s">&#34;isReadStatuses&#34;</span><span class="p">,</span> <span class="n">type</span><span class="p">:</span> <span class="p">[</span><span class="n">URL</span><span class="p">:</span> <span class="nb">Bool</span><span class="p">].</span><span class="kc">self</span><span class="p">]</span> <span class="p">=</span> <span class="n">isReadStatuses</span> </span></span></code></pre></div><p>It&rsquo;s a drop-in replacement.</p> <h2 id="constructing-services">Constructing services</h2> <p>What I&rsquo;ve ignored is exactly how and where the <code>Services</code> instance is constructed. The reason is that different developers choose different approaches.</p> <p>Historically, dependency injection frameworks were used to satisfy this problem. Services in a class would be specially named, typed or attributed and at runtime, the dependency injection framework would intercept the construction of services and set them to appropriate values (fully constructed production services or specialized testing versions).</p> <p>I don&rsquo;t recommend dependency injection frameworks. The purpose of service isolation should be to prevent side-effects but using a runtime framework to manipulate the contents of your classes <em>is</em> a side-effect. It&rsquo;s a conceptual conflict of interest and so completely unnecessary since manually managing services is so simple.</p> <p>All we need to do configure the Model-layer with appropriate services is to pass them as a parameter at construction.</p> <p>It&rsquo;s weird when something so straightforward needs to be explained but I&rsquo;ve seen developers go to extraordinary lengths to avoid passing a single parameter around. Here&rsquo;s one approach commonly attempted to avoid passing services into the model – default constructed services:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">public</span> <span class="kd">init</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">services</span><span class="p">:</span> <span class="n">Services</span> <span class="p">=</span> <span class="n">Services</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">networkService</span><span class="p">:</span> <span class="n">URLSession</span><span class="p">.</span><span class="n">shared</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">keyValueService</span><span class="p">:</span> <span class="n">UserDefaults</span><span class="p">.</span><span class="n">standard</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span></code></pre></div><p>At first, it seems highly practical because you don&rsquo;t need to change how the <code>Model</code> class is constructed. The same <code>Model()</code> invocation will now construct the class with the production services and at test time, we can specify a different <code>services</code> argument for specialized testing services.</p> <p>This approach works when <code>Model</code> is the only class in existence but as soon as you need other classes, it falls apart. It is not composable. If <code>Model</code> relies on class <code>Submodel</code>, then <code>Model</code> must construct <code>Submodel</code> with the same <code>Services</code> from <code>Model</code>, otherwise the two won&rsquo;t agree. Letting <code>Submodel</code> default-construct its own <code>Services</code> is a potential source of bugs and should never occur. The only robust solution requires <code>Services</code> are <em>always</em> passed into the type from outside.</p> <p>Here&rsquo;s the construction of <code>Model</code> in the <code>CwlFeedReaderApp</code>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="p">@</span><span class="n">StateObject</span> <span class="kd">var</span> <span class="nv">model</span> <span class="p">=</span> <span class="n">Model</span><span class="p">(</span><span class="n">services</span><span class="p">:</span> <span class="n">Services</span><span class="p">())</span> </span></span></code></pre></div><p>I would be possible to construct all the services inline but the list of services in an app tends to grow, and precisely configuring reusable services can get complex, so it&rsquo;s a good idea to move this work into a dedicated <code>init</code> function for the construction of production services:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">extension</span> <span class="nc">Services</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">public</span> <span class="kd">init</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kc">self</span><span class="p">.</span><span class="kd">init</span><span class="p">(</span><span class="n">networkService</span><span class="p">:</span> <span class="n">URLSession</span><span class="p">.</span><span class="n">shared</span><span class="p">,</span> <span class="n">keyValueService</span><span class="p">:</span> <span class="n">UserDefaults</span><span class="p">.</span><span class="n">standard</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><h2 id="organize-into-modules-to-preserve-separation">Organize into modules to preserve separation</h2> <p>Now that we&rsquo;ve added protocols, some implementations and some construction code, there&rsquo;s a little organizing to consider to maintain the clean isolation we wanted.</p> <p>Service <em>protocols</em> (<code>NetworkService</code> and <code>KeyValueService</code>) and the <code>Services</code> struct which holds them should go into the &ldquo;Model&rdquo; module (they are the Model-layer defining its own interface).</p> <p>Service <em>implementations</em> (<code>extension URLSession: NetworkService</code> and <code>extension UserDefaults: KeyValueService</code>) must remain separated from the Model-layer. They can go into a new module named &ldquo;ServiceImplementations&rdquo;. This new module should be imported into the <code>CwlFeedReaderApp</code> (since it constructs the <code>Services</code> for the <code>Model</code> at startup) but shouldn&rsquo;t be imported anywhere else.</p> <h2 id="a-final-diagram">A final diagram</h2> <blockquote> <p>The complete MVC+Services version of the app is available from the <a href="https://github.com/mattgallagher/CwlFeedReader/tree/part-four">part-four branch of the CwlFeedReader repository</a></p> </blockquote> <p>The result is that Model-layer only references service protocols, never service implementations. This means the Model-layer no longer has any solid references to any other layer:</p> <figure><img src="https://www.cocoawithlove.com/assets/blog/mvc_separate_services.svg" alt="MVC with separated Services-layer" width="700px"><figcaption> <p>MVC with separated Services-layer</p> </figcaption> </figure> <p>The Controller&rsquo;s role in pulling everything together is apparent – it has a solid line to every other layer. Fortunately, despite the many arrows, the Controller&rsquo;s workload is minimal – the connection to the Services-layer is just a single function.</p> <p>The line from Services to the Model has gone from dotted to solid, indicating that Services now concretely references the Model-layer (to access the service protocol definitions). Eventually, moving these service protocols into a Definitions module (to avoid Services becoming coupled with Model details) might be helpful but we&rsquo;ve added enough modules for the moment.</p> <p>The effect on code was minor, particularly if you consider only the impact on existing code: we&rsquo;ve changed just 9 lines of the 127 line program.</p> <p>There are 46 new lines in new files (two new protocols and their implementations, the <code>Services</code> struct and its constructor) but exactly how to count these is debatable. The protocols and their implementations are reusable between apps. Like the <code>WebView</code> and <code>IdentifiableError</code> – that I ignore when counting lines of code – this new code could be considered external to the app. Additionally, service implementations grow only occasionally; we could increase the remaining program by an order of magnitude without needing additional service implementation code.</p> <h2 id="replacing-the-services-layer-at-testing-time">Replacing the Services-layer at testing time</h2> <p>Now the Model-layer is cleanly separated from the Services-layer but we haven&rsquo;t used it for any change in behavior. For this, we need test-friendly implementations of each service and an easy way to construct these test services, instead of the production services.</p> <p>Here&rsquo;s an implementation of <code>NetworkService</code> that pulls its data from a file resource instead of the network:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">class</span> <span class="nc">MockNetworkService</span><span class="p">:</span> <span class="n">NetworkService</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span> <span class="nf">fetchData</span><span class="p">(</span><span class="n">with</span> <span class="n">request</span><span class="p">:</span> <span class="n">URLRequest</span><span class="p">,</span> <span class="n">handler</span><span class="p">:</span> <span class="p">@</span><span class="n">escaping</span> <span class="p">(</span><span class="n">Data</span><span class="p">?,</span> <span class="n">URLResponse</span><span class="p">?,</span> <span class="n">Error</span><span class="p">?)</span> <span class="p">-&gt;</span> <span class="nb">Void</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="n">AnyCancellable</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">guard</span> <span class="kd">let</span> <span class="nv">method</span> <span class="p">=</span> <span class="n">request</span><span class="p">.</span><span class="n">httpMethod</span><span class="p">,</span> <span class="kd">let</span> <span class="nv">url</span> <span class="p">=</span> <span class="n">request</span><span class="p">.</span><span class="n">url</span> <span class="k">else</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">handler</span><span class="p">(</span><span class="kc">nil</span><span class="p">,</span> <span class="kc">nil</span><span class="p">,</span> <span class="n">URLError</span><span class="p">(.</span><span class="n">badURL</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">AnyCancellable</span> <span class="p">{}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">switch</span> <span class="s">&#34;</span><span class="si">\(</span><span class="n">method</span><span class="si">)</span><span class="s"> </span><span class="si">\(</span><span class="n">url</span><span class="p">.</span><span class="n">absoluteString</span><span class="si">)</span><span class="s">&#34;</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="s">&#34;GET https://www.cocoawithlove.com/feed.json&#34;</span><span class="p">:</span> <span class="n">handler</span><span class="p">(</span><span class="n">mockFixture</span><span class="p">(</span><span class="n">name</span><span class="p">:</span> <span class="s">&#34;feed.json&#34;</span><span class="p">),</span> <span class="p">.</span><span class="n">successResponse</span><span class="p">(</span><span class="n">url</span><span class="p">),</span> <span class="kc">nil</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">default</span><span class="p">:</span> <span class="n">handler</span><span class="p">(</span><span class="kc">nil</span><span class="p">,</span> <span class="p">.</span><span class="n">notFoundResponse</span><span class="p">(</span><span class="n">url</span><span class="p">),</span> <span class="n">URLError</span><span class="p">(.</span><span class="n">fileDoesNotExist</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">AnyCancellable</span> <span class="p">{}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><blockquote> <p><strong>Stubs and mocks</strong>: I&rsquo;ve referred to &ldquo;stub servers&rdquo; and now &ldquo;mock services&rdquo;. These names are conventions from projects that I&rsquo;ve worked on. The terms &ldquo;mocks&rdquo; and &ldquo;stubs&rdquo; have specific definitions in testing (&ldquo;stubs&rdquo; record data and provide output data, whereas &ldquo;mocks&rdquo; record the order in which functions are invoked). Just to be confusing, both &ldquo;stub servers&rdquo; and &ldquo;mock services&rdquo; generally break this terminology because they don&rsquo;t record and would more correctly be termed &ldquo;fakes&rdquo; (partially working, isolated implementations).</p> </blockquote> <p>As with running the app against the production services, we can construct <code>Services</code> for testing in a simplified manner:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">public</span> <span class="kd">extension</span> <span class="nc">Services</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">static</span> <span class="kd">var</span> <span class="nv">mock</span><span class="p">:</span> <span class="n">Services</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">Services</span><span class="p">(</span><span class="n">networkService</span><span class="p">:</span> <span class="n">MockNetworkService</span><span class="p">(),</span> <span class="n">keyValueService</span><span class="p">:</span> <span class="n">MockKeyValueService</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Now that we have this <code>Services.mock</code> implementation, all we need to fix the failing <code>testReload()</code> from the start of the article is to replace the <code>let model = Model()</code> with <code>let model = Model(services: Services.mock)</code> and ensure we&rsquo;re testing the result expected from our &ldquo;feed.json&rdquo; file.</p> <p>Finally, we have reliable, fast tests that won&rsquo;t break over time (unless we break our code – which is the breakage we <em>want</em> to detect).</p> <p>We can also run our SwiftUI previews without relying on the network by replacing the old preview code:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="cp">#if</span> <span class="cp">DEBUG</span> </span></span><span class="line"><span class="cl"><span class="kd">struct</span> <span class="nc">ListView_Previews</span><span class="p">:</span> <span class="n">PreviewProvider</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">static</span> <span class="kd">var</span> <span class="nv">previews</span><span class="p">:</span> <span class="n">some</span> <span class="n">View</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">model</span> <span class="p">=</span> <span class="n">Model</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="n">ListView</span><span class="p">(</span><span class="n">model</span><span class="p">:</span> <span class="n">model</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="cp">#endif</span> </span></span></code></pre></div><p>with the new <code>Services.mock</code>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="cp">#if</span> <span class="cp">DEBUG</span> </span></span><span class="line"><span class="cl"><span class="kd">import</span> <span class="nc">MockServiceImplementations</span> </span></span><span class="line"><span class="cl"><span class="kd">struct</span> <span class="nc">ListView_Previews</span><span class="p">:</span> <span class="n">PreviewProvider</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">static</span> <span class="kd">var</span> <span class="nv">previews</span><span class="p">:</span> <span class="n">some</span> <span class="n">View</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">model</span> <span class="p">=</span> <span class="n">Model</span><span class="p">(</span><span class="n">services</span><span class="p">:</span> <span class="n">Services</span><span class="p">.</span><span class="n">mock</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">ListView</span><span class="p">(</span><span class="n">model</span><span class="p">:</span> <span class="n">model</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="cp">#endif</span> </span></span></code></pre></div><p>This will let us pre-populate our SwiftUI previews however we desire, without needing the network to be available.</p> <blockquote> <p>These tests and previews are available from the <a href="https://github.com/mattgallagher/CwlFeedReader/tree/part-four">part-four branch of the CwlFeedReader repository</a></p> </blockquote> <h2 id="some-ugliness-due-to-missing-se-0273-functionality">Some ugliness due to missing SE-0273 functionality</h2> <p>As with &ldquo;ServiceImplementations&rdquo;, the &ldquo;MockServiceImplementations&rdquo; should also be its own module (by now, you should be able to see why moving to a module-based build approach was so necessary in the previous article).</p> <p>At a minimum, the tests will need to import &ldquo;MockServiceImplementations&rdquo; but ideally, the app should be able to import it too, for use in SwiftUI previews. We don&rsquo;t really want to include the mock services module in release builds (since mock fixtures take up a lot of space and we&rsquo;d rather not include our internal testing details in the released product) so we need to pass <code>condition: .when(configuration: .debug)</code> to the Swift Package Manger for the &ldquo;MockServiceImplementations&rdquo; library.</p> <p>Unfortunately, the <code>.when(configuration:)</code> condition I just mentioned is still waiting for the <a href="https://github.com/apple/swift-evolution/blob/master/proposals/0273-swiftpm-conditional-target-dependencies.md">Swift Package Manager developers to finish SE-0273</a>.</p> <p><em>Ahem</em>.</p> <p>Until then, we need some ugly workarounds to make &ldquo;MockServiceImplementations&rdquo; a debug-only dependency.</p> <p>First, the &ldquo;MockServiceImplementations&rdquo; need to be built into a separate library. The Swift <code>PackageDescription</code> begins:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">package</span> <span class="p">=</span> <span class="n">Package</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">name</span><span class="p">:</span> <span class="s">&#34;CwlFeedReaderLib&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">platforms</span><span class="p">:</span> <span class="p">[.</span><span class="n">iOS</span><span class="p">(.</span><span class="n">v14</span><span class="p">),</span> <span class="p">.</span><span class="n">macOS</span><span class="p">(.</span><span class="n">v11</span><span class="p">)],</span> </span></span><span class="line"><span class="cl"> <span class="n">products</span><span class="p">:</span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">library</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">name</span><span class="p">:</span> <span class="s">&#34;CwlFeedReaderLib&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">targets</span><span class="p">:</span> <span class="p">[</span><span class="s">&#34;Model&#34;</span><span class="p">,</span> <span class="s">&#34;ServiceImplementations&#34;</span><span class="p">,</span> <span class="s">&#34;Toolbox&#34;</span><span class="p">,</span> <span class="s">&#34;ViewToolbox&#34;</span><span class="p">]</span> </span></span><span class="line"><span class="cl"> <span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">library</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">name</span><span class="p">:</span> <span class="s">&#34;MockServiceImplementations&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">targets</span><span class="p">:</span> <span class="p">[</span><span class="s">&#34;MockServiceImplementations&#34;</span><span class="p">]</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span></code></pre></div><p>Then in the &ldquo;Build Phases&rdquo; for the app target, we need to add &ldquo;MockServiceImplementations&rdquo; to the &ldquo;Dependencies&rdquo; but <em>DO NOT</em> add it to the list of libraries or linked files. Instead, find the &ldquo;Other Linker Flags&rdquo; setting in the &ldquo;Build Settings&rdquo; and expand it to reveal &ldquo;Debug&rdquo;/&ldquo;Release&rdquo; and under &ldquo;Debug&rdquo; add <code>${BUILT_PRODUCTS_DIR}/MockServiceImplementations.o</code>.</p> <p>That&rsquo;s not <em>too</em> bad but our MockServiceImplementations module contains &ldquo;fixtures&rdquo; (fixed API responses in the form of JSON files) and we need to copy these into the app bundle for debug builds so we need a &ldquo;Run script&rdquo; build phase:</p> <pre tabindex="0"><code>if [ &#34;${CONFIGURATION}&#34; = &#34;Debug&#34; ]; then echo &#34;Copying ${SCRIPT_INPUT_FILE_0}&#34; cp -Rf &#34;${SCRIPT_INPUT_FILE_0}&#34; &#34;${SCRIPT_OUTPUT_FILE_0}&#34; fi </code></pre><p>with input file:</p> <pre tabindex="0"><code>$(BUILT_PRODUCTS_DIR)/CwlFeedReaderLib_MockServiceImplementations.bundle </code></pre><p>and output file</p> <pre tabindex="0"><code>$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/CwlFeedReaderLib_MockServiceImplementations.bundle </code></pre><p>Yuck.</p> <p>Oh and it&rsquo;s not really possible to establish a dependency on the tree of contents inside a folder so if you change the contents of a fixture without changing something that alters the date on the folder itself, you may need to uncheck the &ldquo;Based on dependency analysis&rdquo; checkbox on the build script, briefly, and build again to pick up the changes.</p> <p>Double yuck.</p> <p>Hopefully, a complete SE-0273 implementation is on its way and all of this agony goes away. Until then, if anyone knows a better dependency-observing way to copy Debug-only resources please let me know. Dependency analysis omissions don&rsquo;t cause problems very often but when they do, it&rsquo;s when you&rsquo;re debugging a test failure and you swear you&rsquo;ve fixed the problem but the test is still failing and&hellip; <em>dammit</em> I fixed the problem 20 minutes ago but it just wasn&rsquo;t getting picked up by the build system.</p> <h2 id="conclusion">Conclusion</h2> <p>App architecture is primarily about establishing roles throughout an app. By structuring our code according to roles, we aim to make code easier to write, easier to read and easier to manage. But these roles pertain only to the expression of logic and do not usually affect the output of that logic – so app architecture rarely affects the ultimate behavior of an app.</p> <p>Switching to a separated Services-layer is unusual as an architectural change because, while the app&rsquo;s behavior is unchanged in production, the app can also run in a second mode where the behavior is quite different – controlled and testing-friendly. A Services-layer unlocks real changes, even if these changes are limited to debugging, testing and development on non-production builds of an app.</p> <p>Despite the benefits, service separation is far from widespread in the iOS app developer community. I&rsquo;ve joined multiple projects where a couple tests exist in code repositories but due to lack of service separation the tests are necessarily limited and largely useless for detecting regressions. I&rsquo;ve also worked on codebases where network services were separated but tests still broke each month as they relied on the system clock to select &ldquo;this month&rsquo;s data&rdquo;. Separating the Model from services – including more services than just the network service – is a skill I hope more developers learn.</p> <p>Even though I&rsquo;m strongly advocating for a separate Services-layer, total isolation of all side effects is not necessarily a goal. I identified <code>DispatchQueue.main.async</code> as a &ldquo;service&rdquo; in CwlFeedReader but I chose <em>not</em> to separate it since it wouldn&rsquo;t affect any of the tests I had written (my tests were already waiting for asychronous expectations). Service abstractions can be simple but they&rsquo;re never free. I&rsquo;d like to think that if a test broke due to asychronous behavior, I would take the effort to create a separate <code>SchedulingService</code> (see my previous <a href="https://www.cocoawithlove.com/blog/testing-actions-over-time.html">Testing actions over time</a> for possible approaches). Or would I try to avoid the problem another way? There isn&rsquo;t a wrong answer, only a judgement call about what is right for your project.</p> <br/>Copyright Matt Gallagher, 2024. All rights reserved. Code samples may be use in accordance with the ISC-style license at https://www.cocoawithlove.com/about.html App architecture basics in SwiftUI Part 3: Module-separated layers https://www.cocoawithlove.com/blog/app-submodules.html Fri, 12 Feb 2021 08:37:06 +1000 https://www.cocoawithlove.com/blog/app-submodules.html <p><a href="https://www.cocoawithlove.com/blog/swiftui-natural-pattern.html">In the previous article</a>, I looked at how SwiftUI&rsquo;s data-driven changes force a basic separation between Model and View. The separation is limited in scope, requiring only that there exist a state value or observable object that drives view updates.</p> <p>If a cleaner separation between Model and View is desired, then slicing an app into modules (Swift&rsquo;s name for discrete libraries) is the best technical step you can make. Separating architectural layers (e.g. Model, View, and optionally others) into their own modules lets you establish rules about how to connect your layers, which types and properties should be accessible and keeps the overhead low on adding new layers.</p> <p>If you&rsquo;ve ever wanted to improve your app&rsquo;s architecture, simply moving your Model into a separate module is a step you should take before choosing to make any other architectural change. It can reveal problems you never realized you had and prevent future problems.</p> <!-- TOC --> <h2 id="the-problem">The problem</h2> <p>When I wrote the <a href="https://www.cocoawithlove.com/blog/coding-through-iteration-and-integration.html">CwlFeedReader app, in the first article in this series</a>, the Xcode project navigator looked like this:</p> <figure><img src="https://www.cocoawithlove.com/assets/blog/cwlfeedreader-project-before-separation.png" alt="Starting state of the CwlFeedReader app" width="400px"><figcaption> <p>Starting state of the CwlFeedReader app</p> </figcaption> </figure> <p>The app is really just 5 files in a single folder. I&rsquo;ve placed three &ldquo;Utilities&rdquo; into a separate folder but the primary reason for this is that these are components I didn&rsquo;t really discuss during the article - they are dependencies and I don&rsquo;t consider them part of the app.</p> <p>Is there really any problem that we need to solve, here? The app is very small and simple; we don&rsquo;t <em>need</em> to break it up.</p> <p>The reasons I want to break up this project fall into the following three categories.</p> <h3 id="1-interface-design">1. Interface design</h3> <p>Everything <code>internal</code> on <code>Model</code> is accessible by the View-layer types. The View could do ill-advised things like start its own downloads and store them directly on the <code>URLSessionDataTask</code> or update the <code>feed</code> following user changes and store it back in the same location – potentially conflicting with Model-driven updates.</p> <p>These are the sorts of obviously-bad short-cuts which get written when developers are rushed or aren&rsquo;t thinking clearly about encapsulation. If problems like this aren&rsquo;t cleaned up before committing code, then suddenly the app is full of surprising bugs. Yes, we can prevent this in a simple way by marking these properties as <code>private</code> but then we remove the ability to break the <code>Model</code> class across multiple files (because everything that needs to access <code>private</code> must stay in the same file).</p> <p>We either have a gigantic Model file with everything marked <code>private</code> or a leaky interface requiring constant vigilence.</p> <p>Better to have <code>Model</code> live in a separate module. If something is View-safe, we mark it <code>public</code>. If something requires strong control, we can use <code>private</code> but <code>internal</code> access becomes truly useful – especially since <code>internal</code> is the default.</p> <h3 id="2-connection-rules">2. Connection rules</h3> <p>The Model-layer should never reference any types from the View-layer; the connection arrow must point from View to Model. In a single module, we can&rsquo;t prevent the Model from accessing View types. Once we move the Model to a separate module, we can let the build system enforce that for us.</p> <h3 id="3-unit-testing">3. Unit testing</h3> <p>This is a technical consideration due to how Xcode projects work but everything about adding testing bundles to Xcode projects is higher friction than adding a testing target via the Swift Package Manager.</p> <p>For example, if I were to add a unit testing bundle to this project it would add 130 lines to the pbxproj file and Info.plist. That addition is nearly as big as the Swift code-size of the app. Even though most of those lines are autogenerated, Xcode expects us to maintain all of these settings. The appearance alone of that much boilerplate in a git merge request is mind numbing.</p> <p>Further, if we wanted unit tests for both macOS and iOS (remember, this is a multiplatform project) we would need to add two unit testing bundles. That&rsquo;s 260 lines of Xcode project configuration.</p> <p>The final insult is that trying run unit tests attached directly to the app is practically useless because the entire app will run whenever the testing bundle is loaded. Having an entire app running in the background is likely to interfere with tests and even if it doesn&rsquo;t it will certainly slow down the tests.</p> <h2 id="creating-an-inline-swift-package">Creating an inline Swift Package</h2> <p>Historically, I would solve some of the above-mentioned problems by creating child frameworks. You would go to the &ldquo;File → New → Target&rdquo; menu and create a new iOS Framework. If, for some reason, your project <em>can&rsquo;t</em> use the Swift Package Manager then this remains the best approach.</p> <p>However, separate frameworks have many of the problems listed under &ldquo;Unit Testing&rdquo;, above: there&rsquo;s xcodeproj bloat and you need a separate framework per platform.</p> <p>Fortunately, we have the Swift Package Manager and it offers a much better experience.</p> <p>Documentation on the Swift Package Manager in Xcode is almost entirely focussed on adding dependencies from outside your project or creating libraries to be shared outside your project. Neither of these are features that interest us, here.</p> <p>You don&rsquo;t need to use the dependency management features of Swift Package Manager for it to be useful.</p> <p>For the CwlFeedReader app, I selected &ldquo;File → New → Swift Package&hellip;&rdquo; from the menubar. When the &ldquo;Save As:&rdquo; dialog appears, I went to the same folder that contains the CwlFeedReader.xcodeproj file for the project, typed &ldquo;CwlFeedReaderLib&rdquo; in the &ldquo;Save As:&rdquo; name field and selected the &ldquo;CwlFeedReader&rdquo; project in the &ldquo;Add to:&rdquo; and &ldquo;Group:&rdquo; popup menus.</p> <figure><img src="https://www.cocoawithlove.com/assets/blog/cwlfeedreader-create-package.png" alt="Creating an inline Swift Package" width="750px"><figcaption> <p>Creating an inline Swift Package</p> </figcaption> </figure> <p>After the package was created, I went to the CwlFeedReader app&rsquo;s Target and added CwlFeedReaderLib under the list of Frameworks, Libraries and Embedded Content.</p> <h2 id="establishing-the-desired-structure">Establishing the desired structure</h2> <p>Now, I can reorganize files in the project browser to place &ldquo;Model.swift&rdquo; under &ldquo;CwlFeedReaderLib/Sources/Model&rdquo;, the &ldquo;IdentifiableError.swift&rdquo; under &ldquo;CwlFeedReaderLib/Sources/Toolbox&rdquo; and &ldquo;View+PlatformCompatibility.swift&rdquo; and &ldquo;WebView.swift&rdquo; under &ldquo;CwlFeedReaderLib/Sources/ViewToolbox&rdquo;.</p> <figure><img src="https://www.cocoawithlove.com/assets/blog/cwlfeedreader-project-after-separation.png" alt="Desired state of the CwlFeedReader app" width="400px"><figcaption> <p>Desired state of the CwlFeedReader app</p> </figcaption> </figure> <p>Updating the Package.swift file to include these locations gives the following:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="c1">// swift-tools-version:5.3</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">import</span> <span class="nc">PackageDescription</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">package</span> <span class="p">=</span> <span class="n">Package</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">name</span><span class="p">:</span> <span class="s">&#34;CwlFeedReaderLib&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">platforms</span><span class="p">:</span> <span class="p">[.</span><span class="n">iOS</span><span class="p">(.</span><span class="n">v14</span><span class="p">),</span> <span class="p">.</span><span class="n">macOS</span><span class="p">(.</span><span class="n">v11</span><span class="p">)],</span> </span></span><span class="line"><span class="cl"> <span class="n">products</span><span class="p">:</span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">library</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">name</span><span class="p">:</span> <span class="s">&#34;CwlFeedReaderLib&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">targets</span><span class="p">:</span> <span class="p">[</span><span class="s">&#34;Model&#34;</span><span class="p">,</span> <span class="s">&#34;Toolbox&#34;</span><span class="p">,</span> <span class="s">&#34;ViewToolbox&#34;</span><span class="p">]</span> </span></span><span class="line"><span class="cl"> <span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="p">],</span> </span></span><span class="line"><span class="cl"> <span class="n">targets</span><span class="p">:</span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">target</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">name</span><span class="p">:</span> <span class="s">&#34;Model&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">dependencies</span><span class="p">:</span> <span class="p">[</span><span class="s">&#34;Toolbox&#34;</span><span class="p">]</span> </span></span><span class="line"><span class="cl"> <span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">target</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">name</span><span class="p">:</span> <span class="s">&#34;Toolbox&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">dependencies</span><span class="p">:</span> <span class="p">[]</span> </span></span><span class="line"><span class="cl"> <span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">target</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">name</span><span class="p">:</span> <span class="s">&#34;ViewToolbox&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">dependencies</span><span class="p">:</span> <span class="p">[</span><span class="s">&#34;Toolbox&#34;</span><span class="p">]</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">]</span> </span></span><span class="line"><span class="cl"><span class="p">)</span> </span></span></code></pre></div><p>Notice that I&rsquo;m already using the Swift Package Manager to create a few addditional modules with &ldquo;Toolbox&rdquo; and &ldquo;ViewToolbox&rdquo; keeping reusable dependencies outside the &ldquo;Model&rdquo; and &ldquo;View&rdquo; components.</p> <p>The reason for this type of separation is to identify files that should contain no app-specific logic – it&rsquo;s telling any reader of the code what to expect and promoting code re-use. Neither of these points is essential but since the effort is low, they are both good to promote.</p> <h2 id="imports-and-access-modifiers">Imports and access modifiers</h2> <p>Once we&rsquo;ve actually separated our Model and Toolbox dependencies, everything will break.</p> <p>The &ldquo;Model.swift&rdquo; file will need to <code>import Toolbox</code>. All four files remaining in the App folder will need to <code>import Model</code> and &ldquo;DetailView.swift&rdquo; will need to <code>import ViewToolbox</code>.</p> <p>Once that&rsquo;s done, everything the View needs to access in the Model module will need to be marked <code>public</code>.</p> <p>I won&rsquo;t list everything here but you can examine the diffs in the final commit on the <a href="https://github.com/mattgallagher/CwlFeedReader/commits/part-three">&ldquo;Part 3&rdquo; branch of the CwlFeedReader code repository</a>.</p> <h2 id="submodules">Submodules?</h2> <p>I&rsquo;ve taken an Xcode project in a single repository and broken it into multiple internal modules.</p> <p>Submodules is a proposed language feature that would be able to achieve a similar effect without the Swift Package Manager. All the code would reside in a single Swift module but you would be able to &ldquo;namespace&rdquo; some sections as residing in a separate submodule. <a href="https://forums.swift.org/t/proposal-discussion-modular-swift/5271">Draft proposals exist to add them to the language</a>. However, submodules has never moved past the draft proposal stage and it&rsquo;s not clearly going anywhere.</p> <p>It&rsquo;s possible submodules will never be implemented but that doesn&rsquo;t worry me. I usually dislike touching build-systems but I think this is one problem that might be more cleanly solved at the build-system level than the language level. Build-system enforced isolation makes accidental violations of encapsulation harder and therefore separation is cleaner.</p> <p>However, potential changes to either Swift or Xcode do make me wonder what the lifetime of this article will be. The advice to &ldquo;isolate your layers&rdquo; will never be wrong but the implementation could be superceded.</p> <p>Submodules could be implemented in Swift and introduce confusion about whether they&rsquo;re better or worse for the job.</p> <p>The xcodeproj format in Xcode could be replaced by Swift Packages with apps created as a single Swift Package with the basic &ldquo;Model&rdquo; and &ldquo;View&rdquo; modules immediately created by the &ldquo;New Project&rdquo; app template. Of course, I&rsquo;ve been hoping for xcodeproj deprecation since 2015; I have no reason to believe it will change in the next couple years.</p> <h2 id="conclusion">Conclusion</h2> <blockquote> <p>The full code for this article is available from the <a href="https://github.com/mattgallagher/CwlFeedReader/tree/part-three">part-three branch of the CwlFeedReader repository.</a></p> </blockquote> <p>As with most of this &ldquo;App architecture basics in SwiftUI&rdquo; series, I&rsquo;m not doing anything revolutionary, here. I&rsquo;ve spent about half this article giving a tutorial on how to add a Swift Package to an Xcode project.</p> <p>The bigger message that I want to convey is that layer-isolation requires a tiny amount of up-front work but it yields clear benefits. When applying this type of separation to established projects, it&rsquo;s common to discover large numbers of bugs and undesired behavior due to Model and View accessing each others <code>internal</code> details. It&rsquo;s a change that informs us about our own laziness.</p> <p>It&rsquo;s also a change that has productivity benefits moving forward. New modules are now trivial to add. Remember the 130 lines times 2 platforms increase when adding a testing target? Now it&rsquo;s 4 lines, total, without the possibility of the app running and interfering in the background.</p> <h3 id="looking-forward">Looking forward&hellip;</h3> <p>While I mentioned &ldquo;tests&rdquo; in this article, I never wrote any. That will change in the next article as I highlight the most important architectural inclusion that this app is still missing.</p> <br/>Copyright Matt Gallagher, 2024. All rights reserved. Code samples may be use in accordance with the ISC-style license at https://www.cocoawithlove.com/about.html App architecture basics in SwiftUI, Part 2: SwiftUI's natural pattern https://www.cocoawithlove.com/blog/swiftui-natural-pattern.html Tue, 19 Jan 2021 10:32:11 +1100 https://www.cocoawithlove.com/blog/swiftui-natural-pattern.html <p>In the <a href="https://www.cocoawithlove.com/blog/coding-through-iteration-and-integration.html">previous article</a>, I wrote a simple SwiftUI app. During the writing, I deliberately kept the code simple – writing code only when needed to satisfy user-facing goals. I want to take a closer look at the application architecture that naturally emerges in SwiftUI when following this kind of functionally minimalist approach.</p> <p>Perform a web search for &ldquo;SwiftUI pattern&rdquo; and you&rsquo;ll find numerous discussions of SwiftUI that wonder if its use of model-bindings make it a form of Model-View-ViewModel (MVVM) or if its use of immutable views and view-state make it redux or Elm-like. While SwiftUI does include these components, the reality is much simpler: SwiftUI&rsquo;s natural pattern is a form of Model-View-Controller (MVC), although very different from UIKit MVC.</p> <p>In any case, the precise naming is less important than the components that work together to form the architecture. In this article, I&rsquo;ll identify the different application architectural roles fulfilled by components of SwiftUI and talk about how they work together to form the overall pattern.</p> <!-- TOC --> <h2 id="changes-triggered-by-model-data">Changes triggered by model data</h2> <p>SwiftUI introduces many differences compared to macOS AppKit or iOS UIKit but a single difference has the biggest effect on application architecture: how changes are triggered.</p> <p>Let&rsquo;s start by looking at how UIKit triggers changes. In UIKit, you construct a tree of view-objects. These objects are reference types and you can hold onto those references to trigger a change.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">window</span> <span class="p">=</span> <span class="n">UIWindow</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="n">window</span><span class="p">.</span><span class="n">makeKeyAndVisible</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">DispatchQueue</span><span class="p">.</span><span class="n">main</span><span class="p">.</span><span class="n">asyncAfter</span><span class="p">(</span><span class="n">deadline</span><span class="p">:</span> <span class="p">.</span><span class="n">now</span><span class="p">()</span> <span class="o">+</span> <span class="p">.</span><span class="n">seconds</span><span class="p">(</span><span class="mi">1</span><span class="p">))</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">label</span> <span class="p">=</span> <span class="n">UILabel</span><span class="p">(</span><span class="n">frame</span><span class="p">:</span> <span class="n">window</span><span class="p">.</span><span class="n">frame</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">label</span><span class="p">.</span><span class="n">text</span> <span class="p">=</span> <span class="s">&#34;Boo!&#34;</span> </span></span><span class="line"><span class="cl"> <span class="n">window</span><span class="p">.</span><span class="n">addSubview</span><span class="p">(</span><span class="n">label</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Here, I&rsquo;ve created a view (<code>window</code>) and I&rsquo;ve changed its content after 1 second by holding onto the <code>window</code> reference and adding a new subview which triggers the update.</p> <p>Here&rsquo;s how it might look in SwiftUI:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">struct</span> <span class="nc">ContentView</span><span class="p">:</span> <span class="n">View</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="p">@</span><span class="n">State</span> <span class="kd">var</span> <span class="nv">showSubview</span><span class="p">:</span> <span class="nb">Bool</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">body</span><span class="p">:</span> <span class="n">some</span> <span class="n">View</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">ZStack</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">Color</span><span class="p">.</span><span class="n">white</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">onAppear</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">DispatchQueue</span><span class="p">.</span><span class="n">main</span><span class="p">.</span><span class="n">asyncAfter</span><span class="p">(</span><span class="n">deadline</span><span class="p">:</span> <span class="p">.</span><span class="n">now</span><span class="p">()</span> <span class="o">+</span> <span class="p">.</span><span class="n">seconds</span><span class="p">(</span><span class="mi">1</span><span class="p">))</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kc">self</span><span class="p">.</span><span class="n">showSubview</span> <span class="p">=</span> <span class="kc">true</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="n">showSubview</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">Text</span><span class="p">(</span><span class="s">&#34;Boo!&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Oh look, I&rsquo;ve managed to find one of the few scenarios where UIKit is more syntactically efficient than SwiftUI.</p> <p>Let&rsquo;s ignore that because there&rsquo;s a more important difference.</p> <ul> <li>In UIKit, I changed the <code>subviews</code> of the window – a <strong>UIKit</strong> property – and that triggered the view update.</li> <li>In SwiftUI, I changed <code>showSubview</code> – my <strong>own</strong> property – and that triggered the view update.</li> </ul> <p>What difference does that make?</p> <p>It means my own values are the source of truth for the View, not the state of a view-tree. I must create data which represents the current view-state. This is a Model (or a Model-interface*) and it is mandatory in SwiftUI whereas in UIKit is was <a href="https://www.cocoawithlove.com/blog/worst-possible-application.html">possible to create an app without one</a>.</p> <blockquote> <p>* A Model is a repository of domain <em>logic</em>, not <em>data</em>. Data and definitions exist in the Model only to communicate (interface) with other layers. Please don&rsquo;t look at this <code>@State var showSubview: Bool</code> and think that&rsquo;s all a Model should do. Ideally, the timing details and the assignment would be expressed inside the Model but I&rsquo;m trying to keep things simple.</p> </blockquote> <h2 id="construction-role">Construction role</h2> <p>In UIKit, lots of components can, and do, fulfill parts of the view-construction role. Some of them are scattered across storyboards, <code>UIViewController</code> lifecycle methods, methods in <code>UITableViewDelegate</code> (among others) and <code>UIView</code>s themselves. Nominally, construction is a <code>UIViewController</code> responsibility but implementation-wise, it&rsquo;s a bit of a mess.</p> <p>In SwiftUI, it should be obvious that <code>View</code>s construct themselves however, it&rsquo;s not as simple as that. While SwiftUI doesn&rsquo;t have a direct counterpart to <code>UIViewController</code>, there are some <code>View</code>s that focus on construction and some that focus on layout and drawing.</p> <p>To explain how this works, I want to look a little closer at the two different kinds of <code>View</code>:</p> <ol> <li><strong>built-in views</strong> return <code>Never</code> from their <code>body</code> function</li> <li><strong>compositional views</strong> return another <code>View</code> from their <code>body</code> function because they really just serve to configure and aggregate underlying views and may hold observable state</li> </ol> <p>You could break each of these into sub categories but this is enough for the purposes of this article.</p> <p>We can&rsquo;t write the first kind of <code>View</code> ourselves because they are effectively the rendering primitives in SwiftUI (like CoreGraphics functions in UIKit). They draw the text, fill and stroke the beziers and position elements.</p> <p>By contrast, all of the views we actually write in SwiftUI primarily serve to group, layout, bind to data and construct these built-in views.</p> <p>Let&rsquo;s look at the default Xcode template for the <code>ContentView</code>, used as the placeholder View in the code for the previous article:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">struct</span> <span class="nc">ContentView</span><span class="p">:</span> <span class="n">View</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">body</span><span class="p">:</span> <span class="n">some</span> <span class="n">View</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">Text</span><span class="p">(</span><span class="s">&#34;Hello, world!&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">padding</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>In this example, <code>ContentView</code> is a compositional view but <code>Text</code> and the <code>ModifiedLayout</code> produced by the <code>.padding()</code> call are built-in views and are specially handled by the SwiftUI system.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="p">@</span><span class="n">available</span><span class="p">(</span><span class="n">iOS</span> <span class="mf">13.0</span><span class="p">,</span> <span class="n">macOS</span> <span class="mf">10.15</span><span class="p">,</span> <span class="n">tvOS</span> <span class="mf">13.0</span><span class="p">,</span> <span class="n">watchOS</span> <span class="mf">6.0</span><span class="p">,</span> <span class="o">*</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="kd">extension</span> <span class="nc">Text</span> <span class="p">:</span> <span class="n">View</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">public</span> <span class="kd">typealias</span> <span class="n">Body</span> <span class="p">=</span> <span class="n">Never</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="centeraside">It&rsquo;s not possible to have a <code>Body = Never</code> for our own views. It is feature of built-in views, only.</div> <p>It should be clear that compositional views (particularly the higher level ones) are the scene constructors in SwiftUI and the built-in views fulfilling the rendering role.</p> <h2 id="event-handling-role">Event handling role</h2> <p>For view interaction to work, there must be code that receives &ldquo;events&rdquo; and can trigger code in other parts of the system.</p> <p>In SwiftUI this is handled by <code>action</code> closures, sometimes as part of a visible <code>View</code> (like a <code>Button</code>) and sometimes as a standalone <code>on</code> event-handler.</p> <p>The <code>.onAppear</code> transformation appends an event-handling view – another SwiftUI built-in view – which calls a closure that captures our view and can access its <code>@State</code> variable.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">struct</span> <span class="nc">ContentView</span><span class="p">:</span> <span class="n">View</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="p">@</span><span class="n">State</span> <span class="kd">var</span> <span class="nv">showSubview</span><span class="p">:</span> <span class="nb">Bool</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">body</span><span class="p">:</span> <span class="n">some</span> <span class="n">View</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">ZStack</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">Color</span><span class="p">.</span><span class="n">white</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">onAppear</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">DispatchQueue</span><span class="p">.</span><span class="n">main</span><span class="p">.</span><span class="n">asyncAfter</span><span class="p">(</span><span class="n">deadline</span><span class="p">:</span> <span class="p">.</span><span class="n">now</span><span class="p">()</span> <span class="o">+</span> <span class="p">.</span><span class="n">seconds</span><span class="p">(</span><span class="mi">1</span><span class="p">))</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kc">self</span><span class="p">.</span><span class="n">showSubview</span> <span class="p">=</span> <span class="kc">true</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="cm">/* ... other code omitted ... */</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>In some respects, this is similar to the Target-Action pattern in UIKit. However, the immutable nature of SwiftUI views encourages the notion that destination of an event should be one of the mutable SwiftUI model properties, like the <code>@State</code> in this example.</p> <p>This enforces the notion that events come from view-hierarchy elements and the interaction is sent directly to the Model – an improvement compared to Target-Action in UIKit which typically sent interactions via the Controller-layer.</p> <blockquote> <p>I&rsquo;m making the claim that all <code>@State</code> properties in SwiftUI (like the <code>showSubview</code> property) are Model-layer entities since they are decoupled, observable and directly drive View-updates. SwiftUI&rsquo;s View-state is similar to the Elm architecture where View-state is just a kind of Model-state, compared to UIKit where View-state was an unobserved side-effect.</p> </blockquote> <h2 id="data-binding-role">Data binding role</h2> <p>Where the event-handling role is concerned with incoming changes triggered by the View-layer, data-binding is concerned with outgoing changes from the Model-layer.</p> <p>UIKit traditionally (prior to Combine) had no data-binding beyond key-value observing and <code>NotificationCenter</code>. Cocoa on macOS had Cocoa Bindings, which could be powerful but were difficult to extend, opaque to the reader and have been effectively deprecated for a decade.</p> <p>SwiftUI, on the other hand, is built around data-bindings – tracking which <code>View</code>s are dependent on which <code>DynamicProperty</code> (<code>@State</code>, <code>@ObservedObject</code>, et al) state properties and making sure views are re-generated when the state changes. While the exact machinery inside these property wrapper attributes is opaque to us, we don&rsquo;t typically need to worry – just use the state and the binding is established automatically.</p> <p>Once again, this is a role assigned to our compositional views, since they are the ones which host Model properties like <code>@ObservedObject</code> and <code>@State</code> properties.</p> <p>In the <code>showSubview</code> example, above, the <code>ContentView</code> is responsible for establishing a connection between the <code>showSubview</code> View-state and the appearance of the <code>Text</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">struct</span> <span class="nc">ContentView</span><span class="p">:</span> <span class="n">View</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="p">@</span><span class="n">State</span> <span class="kd">var</span> <span class="nv">showSubview</span><span class="p">:</span> <span class="nb">Bool</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">body</span><span class="p">:</span> <span class="n">some</span> <span class="n">View</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">ZStack</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="cm">/* ... other code omitted ... */</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="n">showSubview</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">Text</span><span class="p">(</span><span class="s">&#34;Boo!&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><h2 id="taken-together-what-does-all-this-mean">Taken together, what does all this mean?</h2> <p>We have:</p> <ol> <li>A Model, enforced by SwiftUI&rsquo;s approach to change management</li> <li>Built-in views handle all the drawing</li> <li>Composite views handle construction</li> <li>Composite views establish Model-to-View bindings</li> <li>Event views route events using Model references provided by composite views</li> </ol> <p>With separate Model and View roles, plus an amalgam of Construction, Event and Binding roles around our composite views, SwiftUI is really just Model-View-Controller, with the composite views acting as the Controller.</p> <p>If you pay attention only to the names <code>State</code> and <code>View</code>, you could be forgiven for thinking that SwiftUI&rsquo;s natural pattern is Model-View but the roles performed by composite views clearly cover the responsibilities of the Controller in MVC, even if SwiftUI makes the syntactic overhead so low that you could overlook it.</p> <p>The boundary that I&rsquo;ve drawn between a composite and built-in view is blurred (many built-in views may be internally composite and many of our own views might be static and behavior-less). However, this blurry boundary is not a new complication in an MVC pattern. The UIKit <code>UIViewController</code> could perform view styling and didn&rsquo;t need to have data or behaviors; and container <code>UIView</code>s would often function as controllers, applying data connections and establishing links between events and the model.</p> <h2 id="why-is-the-pattern-not-mvvm">Why is the pattern not MVVM?</h2> <p>I want to address MVVM for two reasons:</p> <ol> <li>I&rsquo;ve seen commenters make the mistake of claiming that SwiftUI is, or uses, MVVM.</li> <li>I will talk about ViewModels in a later article of this series and I want the distinction to be clear.</li> </ol> <p>In general MVVM programs should aim to follow these two principles:</p> <ol> <li>every data-driven property in the View should be driven by a unique Model property;</li> <li>presentation-logic (like sorting or string formatting) should be applied in the Model before the property is exposed to the View.</li> </ol> <p>Lets focus on two lines from <code>detailView</code> in the code for the <a href="https://www.cocoawithlove.com/blog/coding-through-iteration-and-integration.html">CwlFeedReader app in the previous article</a>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">isRead</span> <span class="p">=</span> <span class="n">model</span><span class="p">.</span><span class="n">isReadStatuses</span><span class="p">[</span><span class="n">article</span><span class="p">.</span><span class="n">url</span><span class="p">]</span> <span class="p">??</span> <span class="kc">false</span> </span></span></code></pre></div><p>and</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">Text</span><span class="p">(</span><span class="n">isRead</span> <span class="p">?</span> <span class="s">&#34;Mark as unread&#34;</span> <span class="p">:</span> <span class="s">&#34;Mark as read&#34;</span><span class="p">)</span> </span></span></code></pre></div><p>These lines clearly show:</p> <ol> <li>the <code>detailView</code> reading <code>model.isReadStatuses</code> (a property used in multiple parts of the program and definitely not unique to this button label in the <code>detailView</code>)</li> <li>the code performs two transformations on the value: <code>x ?? false</code> to give a default value when <code>nil</code> and then <code>isRead ? a : b</code> to transform the <code>Bool</code> into a display <code>String</code>.</li> </ol> <p>Each of these are violations of the principles that an MVVM program should aim to follow.</p> <p>You might point out – particularly with the second point – that this is some very minor presentation-logic. And its true that most implementations of MVVM permit <em>some</em> transformations in the View – often including this type of boolean logic. But I&rsquo;m using this example to highlight in a simple way that SwiftUI allows for an <em>unbounded</em> amount of logic in the View and ultimately this is contrary to MVVM which tries to put all presentation logic in the ViewModel.</p> <p>If SwiftUI wanted to force an MVVM pattern, it could have made <code>@Published</code> properties a single-use, one-to-one binding, to address the first requirement. Upcoming Swift &ldquo;Ownership&rdquo; features like consumable and move-only types would make this possible. They were not used.</p> <p>Similarly, SwiftUI could have taken steps to prevent or limit arbitrary logic in the <code>body</code> of <code>View</code>. In .NET, the domain-specific language XAML is used to achieve this goal. SwiftUI could have introduced a subset of Swift that permitted only combining operators. SwiftUI did introduce a domain-specific language with <code>@ViewBuilder</code> but it is focussed on simplifying composition, not limiting logic.</p> <p>SwiftUI didn&rsquo;t take any of these steps so it&rsquo;s clear that SwiftUI is not MVVM.</p> <h2 id="why-do-people-claim-swiftui-is-mvvm">Why do people claim SwiftUI is MVVM?</h2> <p>In researching this article, I found multiple articles and forum responses claiming that SwiftUI is or uses MVVM. Why would people jump to this conclusion?</p> <h3 id="is-it-the-model-data-bindings">Is it the Model-data bindings?</h3> <p>The <code>@Published</code> and <code>@ObservedObject</code> attributes used in SwiftUI do make observing data changes much easier – especially since some form of data observing is <em>required</em> to perform view updates. Good data observing can significantly improve MVVM where you might have dozens of bindings between each view-model and view.</p> <p>But the reality is that good MVC apps also use data observing – it is not unique to MVVM. UIKit offered some out-of-the-box tools and most developers supplemented this with their own additions.</p> <p>The <code>@Published</code> attributes make no effort to limit observing to one-to-one scenarios so really, this isn&rsquo;t a boost to MVVM as much as it is a rejection of programming without data observing.</p> <h3 id="is-it-combine">Is it Combine?</h3> <p>Apple introduced the Combine framework alongside SwiftUI. Combine is a reactive programming framework and reactive programming frameworks have been commonly used on iOS/macOS to implement bindings as part of an MVVM pattern.</p> <p>The incorrect implication here, is that SwiftUI implies Combine implies MVVM.</p> <p>I stated above that Model-data bindings (even reactive programming bindings) don&rsquo;t imply MVVM but the other implication is also incorrect: just because you&rsquo;re using SwiftUI doesn&rsquo;t mean you have to use Combine. SwiftUI offers good <em>interoperability</em> with Combine but any reactive programming when using SwiftUI is a totally optional addition.</p> <p>Yes, there are some parts of the SwiftUI data flow that <em>internally</em> use Combine (most notably <code>@Published</code> and <code>ObservableObject</code>) but these are internal implementation details. My <a href="https://www.cocoawithlove.com/blog/coding-through-iteration-and-integration.html">CwlFeedReader in the previous article</a> did not import Combine or use reactive programming.</p> <h3 id="is-it-just-because-uiviewcontroller-is-gone">Is it just because UIViewController is gone?</h3> <div class="aside">The original Smalltalk definition of MVC had the Controller filling a role closer to &ldquo;event handling role&rdquo; than anything else in this article. As I discussed in <a href="https://www.cocoawithlove.com/blog/mvc-and-cocoa.html">Looking at Model-View-Controller in Cocoa</a> the precise definition of MVC is platform specific, making a precise declaration of whether or not something is MVC a little difficult.</div> <p>To be honest, I think this might be close to reality.</p> <p>When MVC in UIKit was discussed, people were not discussing MVC in general. MVC on UIKit meant <code>UIViewController</code> and the way it dominated iOS development. <code>UIViewController</code> was the screens, the storyboards, the delegate and 80% of the rest.</p> <p>To many UIKit developers, <code>UIViewController</code> defined the Controller in Model-View-Controller. Without it, what do we have?</p> <h2 id="conclusion">Conclusion</h2> <p>I think SwiftUI&rsquo;s pattern is best described as Model-View-Controller. The Controller role is downplayed and mixed with the View, but that&rsquo;s common in MVC+Bindings approaches like SwiftUI. However, top-level &ldquo;composite&rdquo; views retain the contruction, binding and event routing roles that the Controller in UIKit fulfilled.</p> <p>Even if we call SwiftUI&rsquo;s pattern &ldquo;The SwiftUI pattern&rdquo; to distinguish from UIKit&rsquo;s MVC, I think the more important point is to understand that SwiftUI includes:</p> <ol> <li>Models and view-state</li> <li>Render-primitive-like built-in views</li> <li>Construction</li> <li>Event handling and model-interactions</li> <li>Data observing and binding</li> </ol> <p>and even though all of these can appear in a 10 line SwiftUI <code>View</code>, they are distinct roles and you should be able to identify them separately.</p> <p>As a developer who has focussed heavily on application architecture, I&rsquo;m struck by how straightforward the architecture around the View is in SwiftUI. In AppKit/UIKit, there was a continuous question around how to set up observing, where to perform View construction and configuration and whether to bother with the overhead of bindings and event handling. SwiftUI doesn&rsquo;t eliminate bad architecture but in my experience so far, a good architecture feels much more natural than it ever did in UIKit.</p> <h3 id="looking-forward">Looking forward&hellip;</h3> <p>In SwiftUI there are fewer application architecture problems to solve around the View-layer and View-bindings but there are still plenty of places for improvement. In the next couple articles, I&rsquo;m going to look at some architectural patterns to improve the Model-layer.</p> <br/>Copyright Matt Gallagher, 2024. All rights reserved. Code samples may be use in accordance with the ISC-style license at https://www.cocoawithlove.com/about.html App architecture basics in SwiftUI, Part 1: Coding through iteration and integration https://www.cocoawithlove.com/blog/coding-through-iteration-and-integration.html Thu, 31 Dec 2020 22:29:53 +1100 https://www.cocoawithlove.com/blog/coding-through-iteration-and-integration.html <p>In this series of articles, I&rsquo;ll look at fundamental app architectural concepts and how they apply to an app written in SwiftUI.</p> <p>To begin the series, I want to start with something small: a JSON feed reader app in SwiftUI. I want to focus on the order of the steps, not the code itself and answer a common question: which should you write first, View-code or Model-code?</p> <p>The short answer is &ldquo;both&rdquo; (or &ldquo;neither&rdquo;) but properly answering the question offers a segue into a more important question: how can I make reliable progress as a programmer when I&rsquo;m uncertain about what I&rsquo;m doing or the blocks of work seem too big to manage?</p> <!-- TOC --> <h2 id="where-to-start">Where to start?</h2> <p>The app I want to write is a JSON feed reader for the Cocoa with Love JSON feed. The app will be a macOS/iOS SwiftUI app that looks like this:</p> <figure><img src="https://www.cocoawithlove.com/assets/blog/json_feed_reader.png" alt="End goal for the CwlFeedReader app"><figcaption> <p>End goal for the CwlFeedReader app</p> </figcaption> </figure> <p>You have to start somewhere. Where should you begin? Model? View?</p> <p>Apps are made of multiple components (data, models, views) and these components all depend on each other, leading to a chicken-and-egg problem of which component should be written first.</p> <p>Where you start doesn&rsquo;t matter. What matters is that you work in small steps (<strong>iterate</strong>) and ensure that changes are immediately extended out into every major component in the app (<strong>integrate</strong>). Between iteration and integration, the most important is integration – that&rsquo;s how you find problems and ensure you&rsquo;re progressing in the right direction. Iteration is simply writing small blocks of code so you can get back to integration, again.</p> <p>I&rsquo;m going to follow this approach with some fairly concrete steps:</p> <ol> <li>placeholders</li> <li>stubs</li> <li>implementation</li> <li>iteration</li> </ol> <h2 id="placeholders">Placeholders</h2> <p>If we want early integration, then we need something to act as both Model and the View as early as possible. It shouldn&rsquo;t be the real Model or the real View or have any substantial property of the real layers. It will merely be a name.</p> <p>This is the concept of a placeholder.</p> <h3 id="view-placeholder">View placeholder</h3> <p>Create an application from Xcode&rsquo;s SwiftUI project templates and you&rsquo;ll get a <code>ContentView</code> that looks like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">struct</span> <span class="nc">ContentView</span><span class="p">:</span> <span class="n">View</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">body</span><span class="p">:</span> <span class="n">some</span> <span class="n">View</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">Text</span><span class="p">(</span><span class="s">&#34;Hello, world!&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">padding</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>That&rsquo;s our placeholder View. It has no behavior and it has the wrong structure but it has a name and that&rsquo;s all we need.</p> <blockquote> <p><strong>Answering the question &ldquo;which should you write first, View-code or Model-code&rdquo;</strong>: the View <em>appeared</em> first but I didn&rsquo;t write it. Not the most helpful answer.</p> </blockquote> <h3 id="model-placeholder">Model placeholder</h3> <p>Let&rsquo;s create a placeholder for the Model. The Model for a JSON feed app might initially be just the JSON feed itself.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">articles</span> <span class="p">=</span> <span class="s">&#34;&#34;</span> </span></span></code></pre></div><p>This alone would work as a Model placeholder – it has a name.</p> <p>I think I can take an extra half-step to give it array arrity here because the important part about JSON feed is that it contains an array of articles.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nv">articles</span> <span class="p">=</span> <span class="p">[</span><span class="s">&#34;one&#34;</span><span class="p">,</span> <span class="s">&#34;two&#34;</span><span class="p">,</span> <span class="s">&#34;three&#34;</span><span class="p">]</span> </span></span></code></pre></div><p>This array of <code>articles</code> is not a representation of a JSON feed but it will work as a placeholder. I have ensured that it has equivalent &ldquo;cardinality&rdquo; (it&rsquo;s an array, not a single object) which will help me segue into the next iteration.</p> <blockquote> <p><strong>Answering the question &ldquo;which should you write first, View-code or Model-code&rdquo;</strong>: the first line of code that I wrote was the Model placeholder but it&rsquo;s not going to survive through the next iteration. Maybe it&rsquo;s not the most useful question.</p> </blockquote> <h3 id="placeholder-integration">Placeholder integration</h3> <p>Before we iterate further, we need to integrate – which is nothing more than putting the model inside the view.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">struct</span> <span class="nc">ContentView</span><span class="p">:</span> <span class="n">View</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">articles</span> <span class="p">=</span> <span class="p">[</span><span class="s">&#34;one&#34;</span><span class="p">,</span> <span class="s">&#34;two&#34;</span><span class="p">,</span> <span class="s">&#34;three&#34;</span><span class="p">]</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">body</span><span class="p">:</span> <span class="n">some</span> <span class="n">View</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">Text</span><span class="p">(</span><span class="s">&#34;Hello, world!&#34;</span><span class="p">).</span><span class="n">padding</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>This integration was straightforward. We&rsquo;ll see a more substantial integration in the next section before choosing to change the order and make Model changes before View changes.</p> <h2 id="stubs">Stubs</h2> <h3 id="stub-views">Stub views</h3> <p>This is the start of the second iteration and that means throwing away code from an old iteration. In this case, we throw away the entire contents of the <code>ContentView.body</code> property.</p> <p>In its places, we can stub out a <code>List</code> using the array of <code>articles</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">struct</span> <span class="nc">ContentView</span><span class="p">:</span> <span class="n">View</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">articles</span> <span class="p">=</span> <span class="p">[</span><span class="s">&#34;one&#34;</span><span class="p">,</span> <span class="s">&#34;two&#34;</span><span class="p">,</span> <span class="s">&#34;three&#34;</span><span class="p">]</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">body</span><span class="p">:</span> <span class="n">some</span> <span class="n">View</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">NavigationView</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">List</span><span class="p">(</span><span class="n">articles</span><span class="p">,</span> <span class="n">id</span><span class="p">:</span> <span class="err">\</span><span class="p">.</span><span class="kc">self</span><span class="p">)</span> <span class="p">{</span> <span class="n">article</span> <span class="k">in</span> </span></span><span class="line"><span class="cl"> <span class="n">NavigationLink</span><span class="p">(</span><span class="n">destination</span><span class="p">:</span> <span class="n">Text</span><span class="p">(</span><span class="n">article</span><span class="p">))</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">Text</span><span class="p">(</span><span class="n">article</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="n">Color</span><span class="p">.</span><span class="n">clear</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">navigationViewStyle</span><span class="p">(</span><span class="n">DoubleColumnNavigationViewStyle</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Our stub view has the basic components: a <code>DoubleColumnNavigationViewStyle</code> with <code>List</code> and a <code>NavigationLink</code> but the views have no real content and do nothing.</p> <p>Stubbing out views is probably the best aspect of SwiftUI. In 14 lines of code (5 of which are largely empty) you can see the entire program.</p> <p>This code runs and give the following app:</p> <p><img src="https://www.cocoawithlove.com/assets/blog/json_feed_reader_stubs.png" alt=""></p> <p>As we add refinements and features in SwiftUI, the details can dwarf the basic structure. Hiding details, without hiding structure, should be the goal of any good SwiftUI developer. It&rsquo;s important to remember what your view looks like at the stub stage and continually refactor to resurface this underlying structure.</p> <h3 id="stub-model">Stub model</h3> <p>As we did when we started the stub view, we&rsquo;re throwing away the <em>entire</em> stub model. It was just a name and there&rsquo;s nothing to carry forward (we&rsquo;re not even going to keep the name).</p> <p>What do we want instead? The primary requirement of a stub Model it that it needs to adopt the expected structure of the data, so we&rsquo;re going to add a much larger block of code here.</p> <p>The <code>Feed</code> and <code>Article</code> types are dictated by the format of the <a href="https://jsonfeed.org">JSONFeed standard</a>, so it&rsquo;s not difficult to write.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">struct</span> <span class="nc">Feed</span><span class="p">:</span> <span class="n">Codable</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">items</span><span class="p">:</span> <span class="p">[</span><span class="n">Article</span><span class="p">]</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">struct</span> <span class="nc">Article</span><span class="p">:</span> <span class="n">Codable</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">url</span><span class="p">:</span> <span class="n">URL</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">title</span><span class="p">:</span> <span class="nb">String</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">content</span><span class="p">:</span> <span class="nb">String</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">enum</span> <span class="nc">CodingKeys</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span> <span class="n">CodingKey</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">case</span> <span class="n">url</span><span class="p">,</span> <span class="n">title</span><span class="p">,</span> <span class="n">content</span> <span class="p">=</span> <span class="s">&#34;content_html&#34;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">class</span> <span class="nc">Model</span><span class="p">:</span> <span class="n">ObservableObject</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="p">@</span><span class="n">Published</span> <span class="kd">var</span> <span class="nv">feed</span><span class="p">:</span> <span class="n">Feed</span><span class="p">?</span> <span class="p">=</span> <span class="n">Feed</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">items</span><span class="p">:</span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="n">Article</span><span class="p">(</span><span class="n">url</span><span class="p">:</span> <span class="n">URL</span><span class="p">(</span><span class="n">string</span><span class="p">:</span> <span class="s">&#34;mock://x/a1.html&#34;</span><span class="p">)</span><span class="o">!</span><span class="p">,</span> <span class="n">title</span><span class="p">:</span> <span class="s">&#34;Article1&#34;</span><span class="p">,</span> <span class="n">content</span><span class="p">:</span> <span class="s">&#34;One&#34;</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="n">Article</span><span class="p">(</span><span class="n">url</span><span class="p">:</span> <span class="n">URL</span><span class="p">(</span><span class="n">string</span><span class="p">:</span> <span class="s">&#34;mock://x/a2.html&#34;</span><span class="p">)</span><span class="o">!</span><span class="p">,</span> <span class="n">title</span><span class="p">:</span> <span class="s">&#34;Article2&#34;</span><span class="p">,</span> <span class="n">content</span><span class="p">:</span> <span class="s">&#34;Two&#34;</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="n">Article</span><span class="p">(</span><span class="n">url</span><span class="p">:</span> <span class="n">URL</span><span class="p">(</span><span class="n">string</span><span class="p">:</span> <span class="s">&#34;mock://x/a3.html&#34;</span><span class="p">)</span><span class="o">!</span><span class="p">,</span> <span class="n">title</span><span class="p">:</span> <span class="s">&#34;Article3&#34;</span><span class="p">,</span> <span class="n">content</span><span class="p">:</span> <span class="s">&#34;Three&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">]</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><h3 id="stub-integration">Stub integration</h3> <p>In the above code, I made <code>Model</code> conform to <code>ObservableObject</code> and offered <code>feed</code> as a <code>@Published</code> property. This is the technical requirement to connect this version of the Model with the View.</p> <p>On the other end of the connection, we must make the <code>ContentView</code> reference this new <code>Model</code>. We remove the <code>articles</code> stub and instead include our new <code>model</code>.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">struct</span> <span class="nc">ContentView</span><span class="p">:</span> <span class="n">View</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="p">@</span><span class="n">ObservedObject</span> <span class="kd">var</span> <span class="nv">model</span><span class="p">:</span> <span class="n">Model</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// ...</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>All <code>ContentView</code> construction will now need to pass a <code>Model</code> and the <code>List</code> needs to be updated to follow the new structure of the data.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">List</span><span class="p">(</span><span class="n">model</span><span class="p">.</span><span class="n">feed</span><span class="p">?.</span><span class="n">items</span> <span class="p">??</span> <span class="p">[],</span> <span class="n">id</span><span class="p">:</span> <span class="err">\</span><span class="p">.</span><span class="n">url</span><span class="p">)</span> <span class="p">{</span> <span class="n">row</span> <span class="k">in</span> </span></span><span class="line"><span class="cl"> <span class="n">NavigationLink</span><span class="p">(</span><span class="n">destination</span><span class="p">:</span> <span class="n">Text</span><span class="p">(</span><span class="n">row</span><span class="p">.</span><span class="n">content</span><span class="p">))</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">Text</span><span class="p">(</span><span class="n">row</span><span class="p">.</span><span class="n">title</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Running the app now show that our stub model is visible in the view:</p> <p><img src="https://www.cocoawithlove.com/assets/blog/json_feed_reader_stub_integration.png" alt=""></p> <h2 id="functional-implementations">Functional implementations</h2> <h3 id="model-functional-implementation">Model functional implementation</h3> <p>We&rsquo;re now at the point where it&rsquo;s time for the <code>Model</code> to load the actual feed. Since this implementation has no effect on the <code>Model</code>&rsquo;s interface, it doesn&rsquo;t matter whether this step occurs before or after similar changes in the View.</p> <p>The placeholder construction of the <code>feed</code> is thrown away. Instead, we&rsquo;re going to get real data using <code>URLRequest</code>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">var</span> <span class="nv">task</span><span class="p">:</span> <span class="n">URLSessionDataTask</span><span class="p">?</span> </span></span><span class="line"><span class="cl"><span class="kd">init</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">request</span> <span class="p">=</span> <span class="n">URLRequest</span><span class="p">(</span><span class="n">url</span><span class="p">:</span> <span class="n">URL</span><span class="p">(</span><span class="n">string</span><span class="p">:</span> <span class="s">&#34;https://www.cocoawithlove.com/feed.json&#34;</span><span class="p">)</span><span class="o">!</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">task</span> <span class="p">=</span> <span class="n">URLSession</span><span class="p">.</span><span class="n">shared</span><span class="p">.</span><span class="n">dataTask</span><span class="p">(</span><span class="n">with</span><span class="p">:</span> <span class="n">request</span><span class="p">)</span> <span class="p">{</span> <span class="n">data</span><span class="p">,</span> <span class="n">response</span><span class="p">,</span> <span class="n">error</span> <span class="k">in</span> </span></span><span class="line"><span class="cl"> <span class="k">do</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="kd">let</span> <span class="nv">error</span> <span class="p">=</span> <span class="n">error</span> <span class="p">{</span> <span class="k">throw</span> <span class="n">error</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">feed</span> <span class="p">=</span> <span class="k">try</span> <span class="n">JSONDecoder</span><span class="p">().</span><span class="n">decode</span><span class="p">(</span><span class="n">Feed</span><span class="p">.</span><span class="kc">self</span><span class="p">,</span> <span class="n">from</span><span class="p">:</span> <span class="n">data</span> <span class="p">??</span> <span class="n">Data</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="n">DispatchQueue</span><span class="p">.</span><span class="n">main</span><span class="p">.</span><span class="n">async</span> <span class="p">{</span> <span class="kc">self</span><span class="p">.</span><span class="n">feed</span> <span class="p">=</span> <span class="n">feed</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="k">catch</span> <span class="p">{}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="n">task</span><span class="p">?.</span><span class="n">resume</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><h3 id="view-functional-implementation">View functional implementation</h3> <p>The View-equivalent of functional implementation in the Model are display changes unrelated to model data. Again, as with the Model functional implementation, this could happen before or after the Model changes.</p> <p>There&rsquo;s less throwing away code in this implementation, it&rsquo;s more a progressive refactoring pass. I&rsquo;ve added nav bar titles, nav bar styling, and a <code>UIViewRepresentable</code>-wrapped <code>WKWebView</code> for feed content instead of a simple <code>Text</code> view). The <code>ContentView</code> <code>body</code> function is updated as follows:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">detailView</span><span class="p">(</span><span class="kc">_</span> <span class="n">row</span><span class="p">:</span> <span class="n">Article</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="n">some</span> <span class="n">View</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">SharedWebView</span><span class="p">(</span><span class="n">content</span><span class="p">:</span> <span class="n">row</span><span class="p">.</span><span class="n">content</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">navigationTitle</span><span class="p">(</span><span class="n">Text</span><span class="p">(</span><span class="n">row</span><span class="p">.</span><span class="n">title</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">navigationBarTitleDisplayModeIfAvailable</span><span class="p">(.</span><span class="n">inline</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">var</span> <span class="nv">body</span><span class="p">:</span> <span class="n">some</span> <span class="n">View</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">NavigationView</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">List</span><span class="p">(</span><span class="n">model</span><span class="p">.</span><span class="n">feed</span><span class="p">?.</span><span class="n">items</span> <span class="p">??</span> <span class="p">[],</span> <span class="n">id</span><span class="p">:</span> <span class="err">\</span><span class="p">.</span><span class="n">url</span><span class="p">)</span> <span class="p">{</span> <span class="n">row</span> <span class="k">in</span> </span></span><span class="line"><span class="cl"> <span class="n">NavigationLink</span><span class="p">(</span><span class="n">destination</span><span class="p">:</span> <span class="n">detailView</span><span class="p">(</span><span class="n">row</span><span class="p">))</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">Text</span><span class="p">(</span><span class="n">row</span><span class="p">.</span><span class="n">title</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">navigationTitle</span><span class="p">(</span><span class="n">Text</span><span class="p">(</span><span class="s">&#34;Articles&#34;</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">navigationBarTitleDisplayModeIfAvailable</span><span class="p">(.</span><span class="n">inline</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">Color</span><span class="p">.</span><span class="n">clear</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">navigationViewStyle</span><span class="p">(</span><span class="n">DoubleColumnNavigationViewStyle</span><span class="p">())</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Here&rsquo;s how it looks:</p> <p><img src="https://www.cocoawithlove.com/assets/blog/json_feed_reader_functional.png" alt=""></p> <p>At this point, we now have an end-to-end functioning version of the app. By focussing on a single data pipeline (the feed) we&rsquo;ve avoided complications and can now build on the core by adding the additional buttons and visual changes from our goal design.</p> <h2 id="iterate-on-the-implementation">Iterate on the implementation</h2> <p>I want the &ldquo;is-read&rdquo; statuses for each article and I want them to be persistent. Let&rsquo;s add those now.</p> <p>In the Placeholder and Stub sections, I wrote the View before the Model but at each stage, the integration required after the Model changes grew. Now that we have a non-empty View, it becomes easier to start changes at the Model so we can combine updates to the View with any required integration steps.</p> <p>As you fill out a structure and pattern for your program, it becomes easier to handle more complex changes in each step. The key is to observe whether you&rsquo;re able to write your code continuously – without loss of focus, mistakes or backtracking. If you find these problems occurring, it&rsquo;s a sign that you should attempt iterations in smaller steps.</p> <h3 id="model-iteration">Model iteration</h3> <p>In this case, I&rsquo;m going to add the &ldquo;is-read&rdquo; status in one step. The &ldquo;is-read&rdquo; implementation in <code>Model</code> looks like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="p">@</span><span class="n">Published</span> <span class="kd">var</span> <span class="nv">isReadStatuses</span><span class="p">:</span> <span class="p">[</span><span class="n">URL</span><span class="p">:</span> <span class="nb">Bool</span><span class="p">]</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">init</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kc">self</span><span class="p">.</span><span class="n">isReadStatuses</span> <span class="p">=</span> <span class="n">UserDefaults</span><span class="p">.</span><span class="n">standard</span><span class="p">.</span><span class="n">data</span><span class="p">(</span><span class="n">forKey</span><span class="p">:</span> <span class="s">&#34;isReadStatuses&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">flatMap</span> <span class="p">{</span> <span class="k">try</span><span class="p">?</span> <span class="n">JSONDecoder</span><span class="p">().</span><span class="n">decode</span><span class="p">([</span><span class="n">URL</span><span class="p">:</span> <span class="nb">Bool</span><span class="p">].</span><span class="kc">self</span><span class="p">,</span> <span class="n">from</span><span class="p">:</span> <span class="nv">$0</span><span class="p">)</span> <span class="p">}</span> <span class="p">??</span> <span class="p">[:]</span> </span></span><span class="line"><span class="cl"> <span class="c1">// ... feed loading omitted</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">setIsRead</span><span class="p">(</span><span class="kc">_</span> <span class="n">value</span><span class="p">:</span> <span class="nb">Bool</span><span class="p">,</span> <span class="n">url</span><span class="p">:</span> <span class="n">URL</span><span class="p">)</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">isReadStatuses</span><span class="p">[</span><span class="n">url</span><span class="p">]</span> <span class="p">=</span> <span class="n">value</span> </span></span><span class="line"><span class="cl"> <span class="n">UserDefaults</span><span class="p">.</span><span class="n">standard</span><span class="p">.</span><span class="kr">set</span><span class="p">(</span><span class="k">try</span><span class="p">?</span> <span class="n">JSONEncoder</span><span class="p">().</span><span class="n">encode</span><span class="p">(</span><span class="n">isReadStatuses</span><span class="p">),</span> <span class="n">forKey</span><span class="p">:</span> <span class="s">&#34;isReadStatuses&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><h3 id="view-iteration">View iteration</h3> <p>This latest Model change can alter the state of the <code>detailView</code> after it is presented (by changing the &ldquo;is-read&rdquo; status). For a <code>NavigationView</code> to change, it must observe the Model for itself (it cannot rely on the parent&rsquo;s observation or else the old state will remain presented when the state changes) so we need to move the <code>detailView</code> into its own <code>View</code> implementation:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">struct</span> <span class="nc">DetailView</span><span class="p">:</span> <span class="n">View</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="p">@</span><span class="n">ObservedObject</span> <span class="kd">var</span> <span class="nv">model</span><span class="p">:</span> <span class="n">Model</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">article</span><span class="p">:</span> <span class="n">Article</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">body</span><span class="p">:</span> <span class="n">some</span> <span class="n">View</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">isRead</span> <span class="p">=</span> <span class="n">model</span><span class="p">.</span><span class="n">isReadStatuses</span><span class="p">[</span><span class="n">article</span><span class="p">.</span><span class="n">url</span><span class="p">]</span> <span class="p">??</span> <span class="kc">false</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">SharedWebView</span><span class="p">(</span><span class="n">content</span><span class="p">:</span> <span class="n">article</span><span class="p">.</span><span class="n">content</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">navigationTitle</span><span class="p">(</span><span class="n">Text</span><span class="p">(</span><span class="n">article</span><span class="p">.</span><span class="n">title</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">navigationBarItems</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">trailing</span><span class="p">:</span> </span></span><span class="line"><span class="cl"> <span class="n">Button</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">model</span><span class="p">.</span><span class="n">setIsRead</span><span class="p">(</span><span class="o">!</span><span class="n">isRead</span><span class="p">,</span> <span class="n">url</span><span class="p">:</span> <span class="n">article</span><span class="p">.</span><span class="n">url</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> <span class="n">label</span><span class="p">:</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">Text</span><span class="p">(</span><span class="n">isRead</span> <span class="p">?</span> <span class="s">&#34;Mark as unread&#34;</span> <span class="p">:</span> <span class="s">&#34;Mark as read&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">navigationBarTitleDisplayMode</span><span class="p">(.</span><span class="n">inline</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">onAppear</span> <span class="p">{</span> <span class="n">model</span><span class="p">.</span><span class="n">setIsRead</span><span class="p">(</span><span class="kc">true</span><span class="p">,</span> <span class="n">url</span><span class="p">:</span> <span class="n">article</span><span class="p">.</span><span class="n">url</span><span class="p">)</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>and use this new implementation from the primary column:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">List</span><span class="p">(</span><span class="n">rows</span><span class="p">,</span> <span class="n">id</span><span class="p">:</span> <span class="err">\</span><span class="p">.</span><span class="n">url</span><span class="p">)</span> <span class="p">{</span> <span class="n">row</span> <span class="k">in</span> </span></span><span class="line"><span class="cl"> <span class="n">NavigationLink</span><span class="p">(</span><span class="n">destination</span><span class="p">:</span> <span class="n">DetailView</span><span class="p">(</span><span class="n">model</span><span class="p">:</span> <span class="n">model</span><span class="p">,</span> <span class="n">article</span><span class="p">:</span> <span class="n">row</span><span class="p">))</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">HStack</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">isRead</span> <span class="p">=</span> <span class="n">model</span><span class="p">.</span><span class="n">isReadStatuses</span><span class="p">[</span><span class="n">row</span><span class="p">.</span><span class="n">url</span><span class="p">]</span> <span class="p">??</span> <span class="kc">false</span> </span></span><span class="line"><span class="cl"> <span class="n">Image</span><span class="p">(</span><span class="n">systemName</span><span class="p">:</span> <span class="n">isRead</span> <span class="p">?</span> <span class="s">&#34;checkmark.circle&#34;</span> <span class="p">:</span> <span class="s">&#34;circle&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">Text</span><span class="p">(</span><span class="n">row</span><span class="p">.</span><span class="n">title</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><h3 id="further-iterations">Further iterations</h3> <p>As you can see from the this iteration, we&rsquo;ve transitioned from iterating where code is thrown away to simpler feature development where new code is added but little is removed. That&rsquo;s okay; that&rsquo;s a sign that the core is complete.</p> <p>The following features were added in this way to the final code:</p> <ol> <li>Error handling</li> <li>Feed reloading</li> <li>Manually toggling &ldquo;is-read&rdquo; status on each article</li> </ol> <p>and we&rsquo;ve reached the goal state for the app:</p> <p><img src="https://www.cocoawithlove.com/assets/blog/json_feed_reader.png" alt=""></p> <h2 id="isnt-this-all-obvious">Isn&rsquo;t this all obvious?</h2> <p>Reading over what I&rsquo;ve said, I don&rsquo;t feel like I&rsquo;ve said anything that isn&rsquo;t completely obvious. Write a couple lines of code, hit build-and-run, confirm that it runs, then add more. That&rsquo;s what everyone does whenever they program, isn&rsquo;t it?</p> <p>Despite the apparent obviousness, working in tight iterations is one of the best rules that I, too often, fail to follow.</p> <p>If I were perfectly disciplined, I would merge code on my projects roughly twice per day, with each merge being between 100 and 400 lines. When this frequency is maintained, pull requests are manageable and readable, other team members can comment on direction before too much code is written and stories move right at a predictable cadence.</p> <p>Unfortunately, it&rsquo;s very easy to think about the bigger picture and accidentally start work on everything at once. Larger units of code are much slower to work on. It&rsquo;s not an exponential increase but it&rsquo;s worse than linear – integrating a 2000 line change will usually take more than 10 times longer than integrating a 200 line change. Larger code changes are also harder to reason about – team members reviewing your change are more likely to mentally disengage than give useful feedback. And mistakes tend to be bigger, harder to address and, with the time-investment, sunk-cost fallacies are more common.</p> <p>If small iterations and integrations are &ldquo;obvious&rdquo;, why aren&rsquo;t they more natural on real-world projects?</p> <p>The difficulty occurs when you&rsquo;re not applying a clear &ldquo;iteration&rdquo; and &ldquo;integration&rdquo; as much as you&rsquo;re refactoring one shape of code into another. Finding small clear steps becomes a challenge. The stages &ldquo;placeholder&rdquo;, &ldquo;stubs&rdquo;, &ldquo;functional implementation&rdquo; and &ldquo;further iterations&rdquo; become much messier.</p> <p>A few options can help&hellip;</p> <h3 id="parallel-implementations">Parallel implementations</h3> <p>Instead of tearing down 5000 lines of code in one block, start a new, independent implementation alongside the old one and move components from the old implementation to the new implementation, one-at-a-time. In this way, you can apply a large refactor like you might create clean new code. Code and use the new implementation for those components moved across but the old implementation for those components yet to be migrated.</p> <h3 id="feature-toggles">Feature toggles</h3> <p>If the reason you can&rsquo;t commit a new block of code is because it doesn&rsquo;t work, then you can always turn it off with a feature toggle (a buildtime or runtime condition in the code that skips over the new code). This allows you to commit incomplete or non-functional code to the repository, get feedback and track progress. It&rsquo;s not as good an option as a parallel implementation (which can be partially used from the outset) but it can still allow you to maintain momentum.</p> <h3 id="architectural-isolation">Architectural isolation</h3> <p>The best option to keep code changes small is to have a codebase where all definitions and functionality are isolated and no app-specific definition extends across more than a few thousand lines of code.</p> <p>This approach gets difficult when your app is tens or even hundreds of thousands of lines of code but I&rsquo;ll be looking at this topic further in later articles in this series.</p> <h2 id="whats-next">What&rsquo;s next?</h2> <p>In this article, I answered the question &ldquo;which should you write first, View-code or Model-code&rdquo; with a resounding, &ldquo;maybe it&rsquo;s not the most useful question&rdquo;.</p> <p>In the next article, I want to answer the following question: what app design pattern have I used here? I promise the answer will be more straightforward.</p> <blockquote> <p>The code for this article <a href="https://github.com/mattgallagher/CwlFeedReader/tree/part-one">is available on github</a> and each of the iteration phases is available as a separate commit in the history.</p> </blockquote> <br/>Copyright Matt Gallagher, 2024. All rights reserved. Code samples may be use in accordance with the ISC-style license at https://www.cocoawithlove.com/about.html 22 short tests of combine – Part 3: Asynchrony https://www.cocoawithlove.com/blog/twenty-two-short-tests-of-combine-part-3.html Sun, 18 Aug 2019 10:16:01 +1000 https://www.cocoawithlove.com/blog/twenty-two-short-tests-of-combine-part-3.html <p>I wrote some experiments around Combine, Apple&rsquo;s reactive programming framework, to gain insight into how Combine handles edge cases that have caused problems for me in other reactive programming frameworks.</p> <p>Looking at everything in one article got much too long so I broke it into three parts:</p> <ol> <li><a href="https://www.cocoawithlove.com/blog/twenty-two-short-tests-of-combine-part-1.html">re-implementing the core protocols of Combine</a></li> <li><a href="https://www.cocoawithlove.com/blog/twenty-two-short-tests-of-combine-part-2.html">a trio of topics: shared computation, shared reference lifetimes and sharing subscribers</a></li> <li>asynchrony, threading and performance</li> </ol> <p>This final part will look at asynchronous delivery scenarios. What happens in Combine when the next value arrives while the previous is being processed; in what ways is Combine thread-safe? Can values arrive out-of-order? Can delivered values overwhelm the subscriber? I&rsquo;ll finish up with a quick look at Combine&rsquo;s performance.</p> <blockquote> <p><strong>Download</strong>: The code for this series, <a href="https://github.com/mattgallagher/CombineExploration">CombineExploration, is available on github</a>.</p> </blockquote> <blockquote> <p><strong>Warning</strong>: This is not a tutorial for Combine. I won&rsquo;t be using Combine in anything resembling a conventional manner. This is going to be a look at some edge cases in Combine, testing behaviors that aren&rsquo;t really documented and may therefore change in future.</p> </blockquote> <!-- TOC --> <h2 id="asynchronous-problems">Asynchronous problems</h2> <p>Asynchronous problems are those that process events that occur over time. Most programming languages have no representation of time so we&rsquo;re reliant on operating-system mediated trickery (blocking, threads and event queues) to let our programming languages handle asynchrony.</p> <p>Reactive programming offers abstractions over a number of different time-related operating system features but you can primarily treat reactive programming like an event queue, since the events passing through your pipelines will usually be triggered by events from an operating system event queue.</p> <p>What are the problems that affect event queues?</p> <ol> <li>events which overwhelm their consumers</li> <li>consumers which block other processing while waiting on events</li> <li>events on multiple threads triggering memory races</li> <li>events on multiple threads triggering logical races</li> </ol> <p>These problems are what I&rsquo;ll focus on in this article.</p> <p>Reactive programming generally takes a &ldquo;non-blocking&rdquo; approach to its API so I won&rsquo;t consider point (2). While a &ldquo;non-blocking&rdquo; approach largely eliminates blocking as a source of problems, it means that rendezvous-based synchronization is not possible as a means of addressing point (1) (see Go&rsquo;s CSP channels for an example of rendezvous-based synchronization).</p> <h2 id="supply-and-demand">Supply and demand</h2> <p>I&rsquo;m going to start by looking at how Combine handles events which overwhelm their consumers. It&rsquo;s a weird place to start because Swift&rsquo;s primary audience (iOS app developers) rarely ever encounter this problem. In front-end application development, the primary source of events is user-interaction and this event stream is usually slow compared to the application&rsquo;s ability to process it. I&rsquo;ve chosen this as a starting point though because Combine has a lot of quirks around its design to handle supply and demand scenarios. In this case, I consider a quirk to be a feature that&rsquo;s more surprising and difficult to manage than it is helpful.</p> <p>Combine uses a concept called &ldquo;demand&rdquo; (an implementation of what is elsewhere called &ldquo;backpressure&rdquo;) to determine how many values a downstream subscriber will accept. When an upstream event source generates more values than the subscriber will accept, Combine throws the values away.</p> <p>In the first part of this series, I implemented the following <code>receive</code> function (a per-subscription function that passes values to the downstream <code>Subscriber</code> function with the same name) like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">receive</span><span class="p">(</span><span class="kc">_</span> <span class="n">input</span><span class="p">:</span> <span class="n">Input</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="n">Subscribers</span><span class="p">.</span><span class="n">Demand</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="n">demand</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">newDemand</span> <span class="p">=</span> <span class="n">downstream</span><span class="p">.</span><span class="n">receive</span><span class="p">(</span><span class="n">input</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">demand</span> <span class="p">=</span> <span class="n">newDemand</span> <span class="o">+</span> <span class="p">(</span><span class="n">demand</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">newDemand</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">Subscribers</span><span class="p">.</span><span class="n">Demand</span><span class="p">.</span><span class="kr">none</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>You can see that if the current known downstream <code>demand</code> is zero, any received <code>input</code> is simply discarded. All subscriptions are expected to behave this way: if they don&rsquo;t <em>know</em> that the downstream subscriber wants a value, they must discard it.</p> <p>In synchronous scenarios, it&rsquo;s uncommon to see this causing a problem because most subscribers will immediately request <code>Demand.unlimited</code>. To see this happening in a controlled scenario, I&rsquo;ve created a special <code>Sink</code> implementation that allows a custom demand value and subsequent custom increases in demand.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">testDemand</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">received</span> <span class="p">=</span> <span class="p">[</span><span class="n">Subscribers</span><span class="p">.</span><span class="n">Event</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">subject</span> <span class="p">=</span> <span class="n">PassthroughSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">sink</span> <span class="p">=</span> <span class="n">CustomDemandSink</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;(</span> </span></span><span class="line"><span class="cl"> <span class="n">demand</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveCompletion</span><span class="p">:</span> <span class="p">{</span> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">complete</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveValue</span><span class="p">:</span> <span class="p">{</span> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">value</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">subject</span><span class="p">.</span><span class="n">subscribe</span><span class="p">(</span><span class="n">sink</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">subject</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">sequence</span><span class="p">:</span> <span class="mf">1.</span><span class="p">..</span><span class="mi">3</span><span class="p">,</span> <span class="n">completion</span><span class="p">:</span> <span class="kc">nil</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">sink</span><span class="p">.</span><span class="n">increaseDemand</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">subject</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">sequence</span><span class="p">:</span> <span class="mf">4.</span><span class="p">..</span><span class="mi">6</span><span class="p">,</span> <span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">sink</span><span class="p">.</span><span class="n">increaseDemand</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">received</span><span class="p">,</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">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">))</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>The initial demand is for 2 values (i.e. the <code>1, 2</code> in the received array). The <code>3</code> value sent in the first sequence exceeds that demand and is discarded. The demand is increased by 2, 3 more values are sent and again, the last value is discarded.</p> <h3 id="is-discarding-values-ever-a-good-idea">Is discarding values ever a good idea?</h3> <p>As a quick look at demand shows: Combine handles inputs overwhelming consumers by discarding inputs. The problem here is that the discarding of data is not accompanied by an error response indicating &ldquo;server/queue busy&rdquo; or anything to that effect. It&rsquo;s a silent failure.</p> <p>There are very few situations where throwing away data without some kind of special handling of the discarded data is a good idea. The two scenarios where it would make sense to me are when:</p> <ol> <li>we want only the &ldquo;most recent update&rdquo; to be handled</li> <li>we can respond to the sender with a &ldquo;server busy error&rdquo;</li> </ol> <p>If you carefully construct your Combine graph, you can handle the first scenario:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">testDemandWithBuffer</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">subject</span> <span class="p">=</span> <span class="n">PassthroughSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;()</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">received</span> <span class="p">=</span> <span class="p">[</span><span class="n">Subscribers</span><span class="p">.</span><span class="n">Event</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">sink</span> <span class="p">=</span> <span class="n">CustomDemandSink</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;(</span> </span></span><span class="line"><span class="cl"> <span class="n">demand</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveCompletion</span><span class="p">:</span> <span class="p">{</span> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">complete</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveValue</span><span class="p">:</span> <span class="p">{</span> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">value</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">subject</span><span class="p">.</span><span class="n">buffer</span><span class="p">(</span><span class="n">size</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="n">prefetch</span><span class="p">:</span> <span class="p">.</span><span class="n">byRequest</span><span class="p">,</span> <span class="n">whenFull</span><span class="p">:</span> <span class="p">.</span><span class="n">dropOldest</span><span class="p">).</span><span class="n">subscribe</span><span class="p">(</span><span class="n">sink</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">subject</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">sequence</span><span class="p">:</span> <span class="mf">1.</span><span class="p">..</span><span class="mi">3</span><span class="p">,</span> <span class="n">completion</span><span class="p">:</span> <span class="kc">nil</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">sink</span><span class="p">.</span><span class="n">increaseDemand</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">subject</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">sequence</span><span class="p">:</span> <span class="mf">4.</span><span class="p">..</span><span class="mi">6</span><span class="p">,</span> <span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">sink</span><span class="p">.</span><span class="n">increaseDemand</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">received</span><span class="p">,</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">6</span><span class="p">].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="kc">nil</span><span class="p">))</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><div class="aside">Bizarrely, as indicated by the <code>.asEvents(completion: nil)</code> at the end, the <code>.buffer</code> prevents completion being emitted when used with <code>.dropOldest</code> (which seems like a bug). The completion is emitted when <code>.dropNewest</code> is used.</div> <p>The <code>buffer</code> lets us ensure &ldquo;scenario 1&rdquo; (&ldquo;most recent update&rdquo;) is correctly handled.</p> <p>However, &ldquo;scenario 2&rdquo; (responding with a &ldquo;server busy error&rdquo;) is much harder. Yes, <code>buffer</code> supports a <code>.customError</code> case for the <code>whenFull</code> parameter but that causes the entire pipeline (the &ldquo;server&rdquo;) to fail, rather than the request. Sending errors to the upstream requester would require giving every value in the input a sequence number and triggering a side-effect when values in the sequence are omitted – a careful task that you&rsquo;d need to manually handle.</p> <p>In summary, discarding values due to supply exceeding demand can work but you need to carefully build your reactive pipeline appropriately. It&rsquo;s far more likely that you want to build pipelines that, by design, will <em>never</em> discard values.</p> <h3 id="accidentally-discarded-asynchronous-values">Accidentally discarded asynchronous values</h3> <p>The purpose of this article is to look at asynchronous problems but I haven&rsquo;t written any asynchronous code. I will, I promise, but let&rsquo;s look at one last synchronous test case, to establish a baseline:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">testReceiveOnImmediate</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">e</span> <span class="p">=</span> <span class="n">expectation</span><span class="p">(</span><span class="n">description</span><span class="p">:</span> <span class="s">&#34;&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">subject</span> <span class="p">=</span> <span class="n">PassthroughSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;()</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">received</span> <span class="p">=</span> <span class="p">[</span><span class="n">Subscribers</span><span class="p">.</span><span class="n">Event</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">c</span> <span class="p">=</span> <span class="n">subject</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">receive</span><span class="p">(</span><span class="n">on</span><span class="p">:</span> <span class="n">ImmediateScheduler</span><span class="p">.</span><span class="n">shared</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">sink</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveCompletion</span><span class="p">:</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">complete</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="n">e</span><span class="p">.</span><span class="n">fulfill</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveValue</span><span class="p">:</span> <span class="p">{</span> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">value</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">subject</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">subject</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">wait</span><span class="p">(</span><span class="k">for</span><span class="p">:</span> <span class="p">[</span><span class="n">e</span><span class="p">],</span> <span class="n">timeout</span><span class="p">:</span> <span class="mf">5.0</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">received</span><span class="p">,</span> <span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">c</span><span class="p">.</span><span class="n">cancel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>This is close to the original Combine test I showed in the first part of this series. It sends a value (<code>1</code>) and completes. The test calls <code>.receive(on: ImmediateScheduler.shared)</code> and waits on an expectation that is fulfilled at the completion of the signal but these steps do practically nothing here because everything completes synchronously.</p> <p>Let&rsquo;s try to use <code>receive(on:)</code> to perform this work on a background thread:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">testReceiveOnFailure</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">queue</span> <span class="p">=</span> <span class="n">DispatchQueue</span><span class="p">(</span><span class="n">label</span><span class="p">:</span> <span class="s">&#34;test&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">e</span> <span class="p">=</span> <span class="n">expectation</span><span class="p">(</span><span class="n">description</span><span class="p">:</span> <span class="s">&#34;&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">subject</span> <span class="p">=</span> <span class="n">PassthroughSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;()</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">received</span> <span class="p">=</span> <span class="p">[</span><span class="n">Subscribers</span><span class="p">.</span><span class="n">Event</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">c</span> <span class="p">=</span> <span class="n">subject</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">receive</span><span class="p">(</span><span class="n">on</span><span class="p">:</span> <span class="n">queue</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">sink</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveCompletion</span><span class="p">:</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">complete</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="n">e</span><span class="p">.</span><span class="n">fulfill</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveValue</span><span class="p">:</span> <span class="p">{</span> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">value</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">subject</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">queue</span><span class="p">.</span><span class="n">async</span> <span class="p">{</span> <span class="n">subject</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">)</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="n">wait</span><span class="p">(</span><span class="k">for</span><span class="p">:</span> <span class="p">[</span><span class="n">e</span><span class="p">],</span> <span class="n">timeout</span><span class="p">:</span> <span class="mf">5.0</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">received</span><span class="p">,</span> <span class="p">[].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">c</span><span class="p">.</span><span class="n">cancel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>I&rsquo;ve added a custom <code>DispatchQueue</code>, used by the <code>.receive(on: queue)</code> line and I&rsquo;m sending the completion on this same queue.</p> <p>The important result to notice is that the <code>received</code> array in the <code>XCTAssertEqual</code> contains the <code>completion: .finished</code> but the value (<code>1</code>) has disappeared. If I hadn&rsquo;t changed the completion to be sent asynchronously on the <code>queue</code>, then even the completion would fail to be received.</p> <p>What happened to the value we sent? Why does trying to send values asynchronously in Combine cause them to fail?</p> <p>The answer gets back to &ldquo;demand&rdquo;, again.</p> <p>Earlier in this article, I showed &ldquo;demand&rdquo; being sent synchronously via the <code>receive(_ input: Input) -&gt; Subscribers.Demand</code> function. However, when values are asynchronously received, the demand is communicated asynchronously, via the separate <code>request(_ demand: Subscribers.Demand)</code> function on the specified queue.</p> <p>This is the cause of our lost values: the <code>request(_ demand: Subscribers.Demand)</code> function had not run on the background <code>queue</code> by the time we called <code>subject.send(1)</code>, so the &ldquo;demand&rdquo; known to the <code>PassthroughSubject</code> is at a default of zero and it simply discarded our value.</p> <p>Let&rsquo;s look at this in detail with the <code>.debug()</code> publisher:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">testReceiveWithLogging</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">subject</span> <span class="p">=</span> <span class="n">PassthroughSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="bp">print</span><span class="p">(</span><span class="s">&#34;Start...&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">received</span> <span class="p">=</span> <span class="p">[</span><span class="n">Subscribers</span><span class="p">.</span><span class="n">Event</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">cancellable</span> <span class="p">=</span> <span class="n">subject</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">debug</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">receive</span><span class="p">(</span><span class="n">on</span><span class="p">:</span> <span class="n">DispatchQueue</span><span class="p">.</span><span class="n">main</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">sink</span><span class="p">(</span><span class="n">receiveValue</span><span class="p">:</span> <span class="p">{</span> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">value</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">})</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="bp">print</span><span class="p">(</span><span class="s">&#34;Phase 1...&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">subject</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">received</span><span class="p">,</span> <span class="p">[].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="kc">nil</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="bp">print</span><span class="p">(</span><span class="s">&#34;Phase 2...&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">RunLoop</span><span class="p">.</span><span class="n">current</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">until</span><span class="p">:</span> <span class="n">Date</span><span class="p">(</span><span class="n">timeIntervalSinceNow</span><span class="p">:</span> <span class="mf">0.001</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">received</span><span class="p">,</span> <span class="p">[].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="kc">nil</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="bp">print</span><span class="p">(</span><span class="s">&#34;Phase 3...&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">subject</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">received</span><span class="p">,</span> <span class="p">[].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="kc">nil</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="bp">print</span><span class="p">(</span><span class="s">&#34;Phase 4...&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">RunLoop</span><span class="p">.</span><span class="n">current</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">until</span><span class="p">:</span> <span class="n">Date</span><span class="p">(</span><span class="n">timeIntervalSinceNow</span><span class="p">:</span> <span class="mf">0.001</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">received</span><span class="p">,</span> <span class="p">[</span><span class="mi">2</span><span class="p">].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="kc">nil</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">cancellable</span><span class="p">.</span><span class="n">cancel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Instead of running on a background <code>DispatchQueue</code>, I&rsquo;m running everything on the main thread so I can run the <code>RunLoop</code> at controlled points.</p> <p>The <code>debug()</code> line here logs each lifecycle event through it so we can see what happens at each point:</p> <pre tabindex="0"><code>Start... testReceiveWithLogging(), line 163: subscription PassthroughSubject Phase 1... Phase 2... testReceiveWithLogging(), line 163: request unlimited Phase 3... testReceiveWithLogging(), line 163: output 2 Phase 4... testReceiveWithLogging(), line 163: cancelled </code></pre><p>The subscription occurs immediately but the <code>request</code> for unlimited values happens only after we let the main scheduler run. Until that point, values we might expect to be sent are completely lost.</p> <p>This behavior of <code>receive(on:)</code> in Combine is dangerously bad to the point where I don&rsquo;t think <code>receive(on:)</code> should ever be used in isolation in its current state. When you create a pipeline of publishers and subscribers, you should be able to immediately send values.</p> <h3 id="safe-use-of-receiveon">Safe use of receive(on:)</h3> <p>Bluntly, I think that <code>receive(on:)</code> should <em>always</em> include <code>.buffer</code> behavior. Here&rsquo;s a construction that lets <code>receive(on:)</code> work as I would expect it to work:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">testBufferedReceiveOn</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">subject</span> <span class="p">=</span> <span class="n">PassthroughSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">e</span> <span class="p">=</span> <span class="n">expectation</span><span class="p">(</span><span class="n">description</span><span class="p">:</span> <span class="s">&#34;&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">received</span> <span class="p">=</span> <span class="p">[</span><span class="n">Subscribers</span><span class="p">.</span><span class="n">Event</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">c</span> <span class="p">=</span> <span class="n">subject</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">buffer</span><span class="p">(</span><span class="n">size</span><span class="p">:</span> <span class="nb">Int</span><span class="p">.</span><span class="bp">max</span><span class="p">,</span> <span class="n">prefetch</span><span class="p">:</span> <span class="p">.</span><span class="n">byRequest</span><span class="p">,</span> <span class="n">whenFull</span><span class="p">:</span> <span class="p">.</span><span class="n">dropNewest</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">receive</span><span class="p">(</span><span class="n">on</span><span class="p">:</span> <span class="n">DispatchQueue</span><span class="p">(</span><span class="n">label</span><span class="p">:</span> <span class="s">&#34;test&#34;</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">sink</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveCompletion</span><span class="p">:</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">complete</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="n">e</span><span class="p">.</span><span class="n">fulfill</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveValue</span><span class="p">:</span> <span class="p">{</span> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">value</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">subject</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">subject</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">wait</span><span class="p">(</span><span class="k">for</span><span class="p">:</span> <span class="p">[</span><span class="n">e</span><span class="p">],</span> <span class="n">timeout</span><span class="p">:</span> <span class="mf">5.0</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">received</span><span class="p">,</span> <span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">c</span><span class="p">.</span><span class="n">cancel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><h2 id="thread-safety">Thread safety</h2> <p>Now that we see how &ldquo;demand&rdquo; and asynchronous receiving work, we can see that the <code>testReceiveOnFailure</code> test case was really an example of a race condition. There was no <strong>memory race</strong> (multiple threads accessing the same memory at the same time) but there was a <strong>logical race</strong> (where steps were not performed in the expected order due to thread timing). We fixed this race by including a buffer (which establishes demand synchronously) but this does raise the question: what actions are thread-safe in Combine? Are any actions explicitly thread unsafe?</p> <p>It&rsquo;s difficult to know what thread safety guarantees exist in Combine because the documentation is surprisingly empty of thread safety discussion. A web search across the Combine documentation reveals only: &ldquo;Canceling a Subscription must be thread-safe&rdquo; on the description of the <code>Subscription</code> protocol.</p> <p>Let&rsquo;s try some crazy things and see if we can get some thread-unsafety.</p> <h3 id="is-cancelling-a-subscription-thread-safe">Is cancelling a subscription thread-safe?</h3> <p>As I showed at the end of the <a href="https://www.cocoawithlove.com/blog/twenty-two-short-tests-of-combine-part-2.html#looking-forward">previous article</a>, cancelling a subscription is not asynchronously safe:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">testSinkCancellationPlusAsyncDelivery</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">received</span> <span class="p">=</span> <span class="p">[</span><span class="n">Subscribers</span><span class="p">.</span><span class="n">Event</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">sink</span> <span class="p">=</span> <span class="n">Just</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">receive</span><span class="p">(</span><span class="n">on</span><span class="p">:</span> <span class="n">DispatchQueue</span><span class="p">.</span><span class="n">main</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">sink</span><span class="p">(</span><span class="n">event</span><span class="p">:</span> <span class="p">{</span> <span class="n">e</span> <span class="k">in</span> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">e</span><span class="p">)</span> <span class="p">})</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">sink</span><span class="p">.</span><span class="n">cancel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">received</span><span class="p">,</span> <span class="p">[].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="kc">nil</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">RunLoop</span><span class="p">.</span><span class="n">main</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">until</span><span class="p">:</span> <span class="n">Date</span><span class="p">(</span><span class="n">timeIntervalSinceNow</span><span class="p">:</span> <span class="mf">0.001</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">received</span><span class="p">,</span> <span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">))</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>This test shows us cancelling a sink, at which point we have received no values. Later a value arrives, even though we our cancelled sink.</p> <p>This isn&rsquo;t a memory race between threads (the narrowest definition of thread-safety) but this is a failure to preserve semantics when threads act in a valid but unexpected order could be considered thread-unsafe broader definitions. Certainly, the problem is likely to cause headaches in a number of threaded and asynchronous scenarios.</p> <p>This is probably just a bug but it speaks of a lack of testing in Combine around asynchronous scenarios. The problem itself is easily fixed once you&rsquo;re aware that the problem can occur. I&rsquo;ve implemented one possible solution (by checking for a <code>subscribed</code> state when receiving) in my <code>CustomSink</code>, so the test will pass when swapping <code>customSink</code> in place of <code>sink</code>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"> <span class="kd">func</span> <span class="nf">testSinkCancellationPlusImmediateAsyncDelivery</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">received</span> <span class="p">=</span> <span class="p">[</span><span class="n">Subscribers</span><span class="p">.</span><span class="n">Event</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">sequence</span> <span class="p">=</span> <span class="n">Just</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">receive</span><span class="p">(</span><span class="n">on</span><span class="p">:</span> <span class="n">DispatchQueue</span><span class="p">.</span><span class="n">main</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">customSink</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveCompletion</span><span class="p">:</span> <span class="p">{</span> <span class="n">c</span> <span class="k">in</span> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">complete</span><span class="p">(</span><span class="n">c</span><span class="p">))</span> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveValue</span><span class="p">:</span> <span class="p">{</span> <span class="n">v</span> <span class="k">in</span> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">value</span><span class="p">(</span><span class="n">v</span><span class="p">))</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">sequence</span><span class="p">.</span><span class="n">cancel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">received</span><span class="p">,</span> <span class="p">[].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="kc">nil</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">RunLoop</span><span class="p">.</span><span class="n">main</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">until</span><span class="p">:</span> <span class="n">Date</span><span class="p">(</span><span class="n">timeIntervalSinceNow</span><span class="p">:</span> <span class="mf">0.001</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">received</span><span class="p">,</span> <span class="p">[].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="kc">nil</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span></code></pre></div><h3 id="mutual-exclusion">Mutual exclusion</h3> <p>Let&rsquo;s simulate a possible cause of memory races by trying to run a closure simultaneously on multiple threads.</p> <p>What happens if we simultaneously send a value to a <code>Subject</code>/<code>Subscriber</code> pair from a hundred different threads at the same time? Will our <code>Subscriber</code>&rsquo;s handler closure be concurrently invoked, causing thread safety problems?</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">testSubjectOrder</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">sequenceLength</span> <span class="p">=</span> <span class="mi">100</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">subject</span> <span class="p">=</span> <span class="n">PassthroughSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">semaphore</span> <span class="p">=</span> <span class="n">DispatchSemaphore</span><span class="p">(</span><span class="n">value</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">total</span> <span class="p">=</span> <span class="n">AtomicBox</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">&gt;(</span><span class="mi">0</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">collision</span> <span class="p">=</span> <span class="kc">false</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">c</span> <span class="p">=</span> <span class="n">subject</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">sink</span><span class="p">(</span><span class="n">receiveValue</span><span class="p">:</span> <span class="p">{</span> <span class="n">value</span> <span class="k">in</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="n">total</span><span class="p">.</span><span class="n">isMutating</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="c1">// Check to see if this closure is concurrently invoked</span> </span></span><span class="line"><span class="cl"> <span class="n">collision</span> <span class="p">=</span> <span class="kc">true</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="n">total</span><span class="p">.</span><span class="n">mutate</span> <span class="p">{</span> <span class="n">total</span> <span class="k">in</span> </span></span><span class="line"><span class="cl"> <span class="c1">// Make sure we&#39;re in the handler for enough time to get a concurrent invocation</span> </span></span><span class="line"><span class="cl"> <span class="n">Thread</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="n">forTimeInterval</span><span class="p">:</span> <span class="mf">0.001</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">total</span> <span class="o">+=</span> <span class="n">value</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="n">total</span> <span class="p">==</span> <span class="n">sequenceLength</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">semaphore</span><span class="p">.</span><span class="n">signal</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// Try to send from a hundred different threads at once</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="kc">_</span> <span class="k">in</span> <span class="mf">1.</span><span class="p">..</span><span class="n">sequenceLength</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">DispatchQueue</span><span class="p">.</span><span class="n">global</span><span class="p">().</span><span class="n">async</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">subject</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">semaphore</span><span class="p">.</span><span class="n">wait</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="n">c</span><span class="p">.</span><span class="n">cancel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">total</span><span class="p">.</span><span class="n">value</span><span class="p">,</span> <span class="n">sequenceLength</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertFalse</span><span class="p">(</span><span class="n">collision</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>This test confirms the <code>receiveValue</code> closure is never invoked simultaneously on multiple threads.</p> <p>Inspecting the call to <code>receiveValue</code> in the debugger reveals that the <code>PassthroughSubject.Conduit.offer</code> function appears to block in an <code>os_unfair_lock</code> if another thread is sending through the <code>PassthroughSubject</code>.</p> <p>Short answer: Combine does appear to be thread-safe due to mutexes applied by <code>Subject</code>s.</p> <h3 id="sequence-ordering">Sequence ordering</h3> <p>What happens if we do something thread unsafe in the middle of a pipeline?</p> <p>Instead of sending values from 100 threads at one pipeline, lets send 100 values down one pipeline via a concurrent (and therefore non-ordered) scheduler.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">testDeliveryOrder</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">received</span> <span class="p">=</span> <span class="p">[</span><span class="n">Subscribers</span><span class="p">.</span><span class="n">Event</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">sequence</span> <span class="p">=</span> <span class="n">Publishers</span><span class="p">.</span><span class="n">Sequence</span><span class="p">&lt;</span><span class="n">ClosedRange</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">&gt;,</span> <span class="n">Never</span><span class="p">&gt;(</span><span class="n">sequence</span><span class="p">:</span> <span class="mf">1.</span><span class="p">..</span><span class="mi">10</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">e</span> <span class="p">=</span> <span class="n">expectation</span><span class="p">(</span><span class="n">description</span><span class="p">:</span> <span class="s">&#34;&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">c</span> <span class="p">=</span> <span class="n">sequence</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">receive</span><span class="p">(</span><span class="n">on</span><span class="p">:</span> <span class="n">DispatchQueue</span><span class="p">.</span><span class="n">global</span><span class="p">())</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">receive</span><span class="p">(</span><span class="n">on</span><span class="p">:</span> <span class="n">DispatchQueue</span><span class="p">.</span><span class="n">main</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">sink</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveCompletion</span><span class="p">:</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">complete</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="n">e</span><span class="p">.</span><span class="n">fulfill</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveValue</span><span class="p">:</span> <span class="p">{</span> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">value</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="bp">withExtendedLifetime</span><span class="p">(</span><span class="n">c</span><span class="p">)</span> <span class="p">{</span> <span class="n">wait</span><span class="p">(</span><span class="k">for</span><span class="p">:</span> <span class="p">[</span><span class="n">e</span><span class="p">],</span> <span class="n">timeout</span><span class="p">:</span> <span class="mf">5.0</span><span class="p">)</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertNotEqual</span><span class="p">(</span><span class="n">received</span><span class="p">,</span> <span class="p">(</span><span class="mf">1.</span><span class="p">..</span><span class="mi">10</span><span class="p">).</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">))</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>If you use a concurrent queue, you lose your sequence ordering. I guess that makes sense but it&rsquo;s worth keeping in mind that if ordering is important, then don&rsquo;t use <code>DispatchQueue.global()</code>.</p> <h3 id="re-entrancy">Re-entrancy</h3> <p>Does Combine allow same-thread re-entrancy?</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">testReentrancy</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">subject</span> <span class="p">=</span> <span class="n">PassthroughSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;()</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">received</span> <span class="p">=</span> <span class="p">[</span><span class="n">Subscribers</span><span class="p">.</span><span class="n">Event</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">subscriber</span> <span class="p">=</span> <span class="n">Subscribers</span><span class="p">.</span><span class="n">Sink</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;(</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveCompletion</span><span class="p">:</span> <span class="p">{</span> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">complete</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveValue</span><span class="p">:</span> <span class="p">{</span> <span class="n">v</span> <span class="k">in</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="n">v</span> <span class="o">&lt;</span> <span class="mi">3</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">subject</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">v</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">value</span><span class="p">(</span><span class="n">v</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">subject</span><span class="p">.</span><span class="n">subscribe</span><span class="p">(</span><span class="n">subscriber</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">subject</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">sequence</span><span class="p">:</span> <span class="mf">1.</span><span class="p">..</span><span class="mi">1</span><span class="p">,</span> <span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">received</span><span class="p">,</span> <span class="p">[</span><span class="mi">3</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">1</span><span class="p">].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">subscriber</span><span class="p">.</span><span class="n">cancel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Yes, it does. Despite using an <code>os_unfair_lock</code> (usually non-reentrant) as its mutex, <code>PassthroughSubject</code> allows same-thread re-entrancy. This operates as classic functional recursion and has the expected consequences (here, the values are appended in the reverse order to which they arrive).</p> <p>For comparison, here&rsquo;s another test that attempts re-entrancy via a second thread (attempting a send from the main thread onto a background thread for a <code>Subject</code> that is already sending):</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="cp">#if</span> <span class="kc">false</span> </span></span><span class="line"><span class="cl"><span class="c1">// NOTE: this test deadlocks</span> </span></span><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">testDeadlock</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">subject</span> <span class="p">=</span> <span class="n">PassthroughSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">semaphore</span> <span class="p">=</span> <span class="n">DispatchSemaphore</span><span class="p">(</span><span class="n">value</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">sequenceLength</span> <span class="p">=</span> <span class="mi">100</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">total</span> <span class="p">=</span> <span class="mi">0</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">t</span> <span class="p">=</span> <span class="n">mach_absolute_time</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">c</span> <span class="p">=</span> <span class="n">subject</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">sink</span><span class="p">(</span><span class="n">receiveValue</span><span class="p">:</span> <span class="p">{</span> <span class="n">value</span> <span class="k">in</span> </span></span><span class="line"><span class="cl"> <span class="n">total</span> <span class="o">+=</span> <span class="n">value</span> </span></span><span class="line"><span class="cl"> <span class="k">if</span> <span class="n">total</span> <span class="o">&lt;</span> <span class="n">sequenceLength</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">DispatchQueue</span><span class="p">.</span><span class="n">main</span><span class="p">.</span><span class="n">sync</span> <span class="p">{</span> <span class="n">subject</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="n">DispatchQueue</span><span class="p">.</span><span class="n">global</span><span class="p">().</span><span class="n">async</span> <span class="p">{</span> <span class="n">subject</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="k">while</span> <span class="n">total</span> <span class="o">&lt;</span> <span class="n">sequenceLength</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">RunLoop</span><span class="p">.</span><span class="n">current</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">until</span><span class="p">:</span> <span class="n">Date</span><span class="p">(</span><span class="n">timeIntervalSinceNow</span><span class="p">:</span> <span class="mf">0.001</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="n">c</span><span class="p">.</span><span class="n">cancel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">total</span><span class="p">,</span> <span class="n">sequenceLength</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="cp">#endif</span> </span></span></code></pre></div><p>As you&rsquo;d expect, given a recursive mutex, it deadlocks.</p> <h2 id="performance">Performance</h2> <p>Do you need maximum performance in your reactive programming framework? Probably not.</p> <p>For the reactive programming framework to cause performance problems, you need to be sending a <em>lot</em> of messages between components. Tens thousand messages per second is the point where you might start to care about the speed of the reactive programming framework, rather than the code it invokes. In these cases, it would be possible for a poorly implemented message transport mechanism to become a bottleneck.</p> <p>This is <em>not</em> common when using reactive programming for bindings in user-applications. User-initiated events rarely exceed hundreds per second. However, if you&rsquo;re using reactive programming as part of a processing queue then it becomes easily possible.</p> <p>Even with a poorly performing reactive programming framework, you can usually avoid bottlenecks in your transport mechanism by aggregating inter-component messages. However, it&rsquo;s nice to know that your transport is high performance so you can wait longer before needing to take optimizing steps and you won&rsquo;t have to make decisions that trade latency for throughput.</p> <h3 id="a-few-scenarios">A few scenarios</h3> <p>I ran a three-way performance test between Combine, RxSwift and CwlSignal.</p> <table> <thead> <tr> <th style="text-align: right"></th> <th style="text-align: center">Combine</th> <th style="text-align: center">RxSwift</th> <th style="text-align: center">CwlSignal</th> </tr> </thead> <tbody> <tr> <td style="text-align: right">Subject send</td> <td style="text-align: center"><strong>5.021</strong></td> <td style="text-align: center">3.826</td> <td style="text-align: center">4.495</td> </tr> <tr> <td style="text-align: right">Sequence send</td> <td style="text-align: center"><strong>1.154</strong></td> <td style="text-align: center">0.288</td> <td style="text-align: center">1.052</td> </tr> <tr> <td style="text-align: right">Async send</td> <td style="text-align: center">0.200</td> <td style="text-align: center">0.359</td> <td style="text-align: center"><strong>1.280</strong></td> </tr> <tr> <td style="text-align: right">Deep pipeline</td> <td style="text-align: center"><strong>2.624</strong></td> <td style="text-align: center">1.814</td> <td style="text-align: center">0.176</td> </tr> </tbody> </table> <figcaption>Performance on macOS 10.15 beta 6 in millions of values sent per second (higher is better)</figcaption> <p>What do these numbers tell us? Frankly, none of these numbers are too bad.</p> <p>The &ldquo;async send&rdquo; number (critical for most operations queues where performance really matters) is a little low for Combine. Part of this slow performance is because I put a <code>.buffer</code> in the pipeline ahead of the <code>receive(on:)</code> (without this <code>.buffer</code> the number would be closer to 0.5 than 0.2 but that&rsquo;s still a little slow).</p> <p>Annoying workarounds and async issues notwithstanding, Combine is a good, high performance framework. While modern CPUs can perform billions of operations per second, it is <em>difficult</em> to reach 5 million iterations per second in code involving generics and mutexes, so Combine has done well.</p> <p>These figures are from macOS 10.15 beta 6, running on my Mac Book Pro 2018. In beta 4, the Combine numbers looked like this:</p> <table> <thead> <tr> <th style="text-align: right"></th> <th style="text-align: center">Combine</th> <th style="text-align: center">RxSwift</th> <th style="text-align: center">CwlSignal</th> </tr> </thead> <tbody> <tr> <td style="text-align: right">Subject send</td> <td style="text-align: center">0.891</td> <td style="text-align: center">3.782</td> <td style="text-align: center"><strong>4.512</strong></td> </tr> <tr> <td style="text-align: right">Sequence send</td> <td style="text-align: center"><strong>1.011</strong></td> <td style="text-align: center">0.277</td> <td style="text-align: center">0.998</td> </tr> <tr> <td style="text-align: right">Async send</td> <td style="text-align: center">0.140</td> <td style="text-align: center">0.359</td> <td style="text-align: center"><strong>1.112</strong></td> </tr> <tr> <td style="text-align: right">Deep pipeline</td> <td style="text-align: center">0.954</td> <td style="text-align: center"><strong>1.888</strong></td> <td style="text-align: center">0.180</td> </tr> </tbody> </table> <figcaption>Performance on macOS 10.15 beta 4 in millions of values sent per second (higher is better)</figcaption> <p>You can see that since beta 4, Combine has dramatically improved on most tests. However, these numbers have been bouncing around from beta to beta (in macOS 10.15 beta 5, Combine briefly peaked at 7 million values per second on the &ldquo;Subject send&rdquo; test and has since dropped down to 5 million).</p> <h2 id="conclusion">Conclusion</h2> <blockquote> <p><strong>Download</strong>: The code for this series, <a href="https://github.com/mattgallagher/CombineExploration">CombineExploration, is available on github</a>.</p> </blockquote> <p>I&rsquo;ve been hard on Combine in these three articles. I don&rsquo;t think Combine is a bad framework but in its current state, I think there are a few critical areas where it works badly. I think these problem areas are going to cause reliability issues for many developers until they are addressed.</p> <p>To me, the most important improvements Combine should make are:</p> <ol> <li><code>Subscription</code> and other &ldquo;black boxes&rdquo; should be fully documented (we shouldn&rsquo;t be guessing about thread safety and graph lifecycles)</li> <li>support buffered subjects and other ways of sharing cached computations</li> <li>support scenarios where demand must never be zero</li> <li><code>receive(on:)</code> should synchronously establish initial demand (only <code>subscribe(on:)</code> should asynchronously complete construction)</li> </ol> <p>I&rsquo;ve shown that we can work around these problems but proper fixes will need to occur in Combine itself.</p> <p>It&rsquo;s apparent that Combine has kept to many of the overall goals of the <a href="https://www.reactive-streams.org">Reactive Streams initiative</a>. This initiative is promoted by groups that work with networked, multi-client, server-limited processing. The strict mandates for subscriptions and asynchronous backpressure support that use case but in my use cases (view-bindings and single-client operation queues), I&rsquo;ve found the entire concepts of backpressure and subscriptions to be a hinderance and a source of spurious failures and design challenges.</p> <p>What I want from Combine is to better support scenarios where multi-subscribing and demand are effectively disabled (have no measurable effect) and to focus more on caching sequences emitted by hot publishers for sharing between dynamically changing subscribers because this is the natural way to program in an imperative programming language.</p> <br/>Copyright Matt Gallagher, 2024. All rights reserved. Code samples may be use in accordance with the ISC-style license at https://www.cocoawithlove.com/about.html 22 short tests of combine – Part 2: Sharing https://www.cocoawithlove.com/blog/twenty-two-short-tests-of-combine-part-2.html Sat, 17 Aug 2019 10:16:01 +1000 https://www.cocoawithlove.com/blog/twenty-two-short-tests-of-combine-part-2.html <p>I wrote some experiments around Combine, Apple&rsquo;s reactive programming framework, to gain insight into how Combine handles edge cases that have caused problems for me in other reactive programming frameworks.</p> <p>Looking at everything in one article got much too long so I broke it into three parts:</p> <ol> <li><a href="https://www.cocoawithlove.com/blog/twenty-two-short-tests-of-combine-part-1.html">re-implementing the core protocols of Combine</a></li> <li>shared computation, shared reference lifetimes and sharing subscribers</li> <li><a href="https://www.cocoawithlove.com/blog/twenty-two-short-tests-of-combine-part-3.html">asynchrony, threading and performance</a></li> </ol> <p>This article will be the middle third, an investigation spanning a trio of topics with &ldquo;shared&rdquo; in the name: shared computation, shared reference lifetimes and sharing subscribers.</p> <blockquote> <p><strong>Download</strong>: The code for this series, <a href="https://github.com/mattgallagher/CombineExploration">CombineExploration, is available on github</a>.</p> </blockquote> <blockquote> <p><strong>Warning</strong>: This is not a tutorial for Combine. I won&rsquo;t be using Combine in anything resembling a conventional manner. This is going to be a look at some edge cases in Combine, testing behaviors that aren&rsquo;t really documented and may therefore change in future.</p> </blockquote> <!-- TOC --> <h2 id="hot-and-cold-publishers">Hot and cold publishers</h2> <p>In <a href="https://www.cocoawithlove.com/blog/twenty-two-short-tests-of-combine-part-1.html">the previous article</a>, I focussed on Combine&rsquo;s largely hidden <code>Subscription</code> type and the fact that Combine creates a new graph of <code>Subscription</code> types for every new subscriber. Using this independent subscription graph allows Combine to follow the &ldquo;cold publisher&rdquo; model:</p> <blockquote> <p>A <strong>cold publisher</strong> is one where the emitted sequence of values is lazily constructed and traversed when requested by a downstream subscriber. Asynchronous tasks begin on subscription, not construction of the publisher. The graph may not change structure for the entire duration of the sequence.</p> </blockquote> <div class="aside">The terms "cold publisher" and "hot publisher" are usually "cold observable" and "hot observable" in other Rx frameworks. I've changed the terminology to match Combine's terms but the ideas are equivalent.</div> <p>In a &ldquo;cold publisher&rdquo; graph, you don&rsquo;t need to cache values because a value exists only at the moment it is requested by a subscriber and immediately handed over. A subscriber cannot re-request old values and any request for values made by a new subscriber is really a request to calculate the values again. Caching values emitted from a &ldquo;cold publisher&rdquo; graph doesn&rsquo;t make sense.</p> <p>However, this model isn&rsquo;t practical outside of strict functional programming languages. Aside from the fact that we don&rsquo;t want to waste time calculating the same values, there is also the problem of network data, the filesystem, host time, user interactions and other side effects that may start before our program is ready and run independent of any Combine graph we may have created and will never play nicely with a &ldquo;cold publisher&rdquo; model.</p> <p>In imperative programming, we need &ldquo;hot publishers&rdquo;.</p> <blockquote> <p>A <strong>hot publisher</strong> may produce values at any time and at any rate, regardless of the demand from downstream subscribers. The graph may continue to change in structure during the sequence. Publishers must encode rules about what to do with values that exceed downstream demand or how to handle new subscribers joining an existing value sequence.</p> </blockquote> <p>In almost all cases, a &ldquo;hot publisher&rdquo; in Combine will use a <code>Subject</code> (either externally or internally). <code>Subject</code>s play an important role in &ldquo;hot&rdquo; graphs because a <code>Subject</code> is a publisher with a single shared identity (most publishers create a new independent identity for each downstream subscription).</p> <h2 id="sharing-via-multicast">Sharing via multicast</h2> <p>The essential part of working with a graph that contains &ldquo;hot publishers&rdquo; is that you must be able to handle new subscribers joining in the middle of a sequence.</p> <p>In the previous article, I discussed the idea that there were 5 different kinds of approach to handling multiple subscribers:</p> <ol> <li>multicast</li> <li>caching</li> <li>latest value</li> <li>custom caching</li> <li>resubscribe</li> </ol> <p>but I looked only at resubscribe because this is the &ldquo;cold publisher&rdquo; favored option in Combine.</p> <p>In my <code>testOverlappingABCD</code> example showing &ldquo;resubscribe&rdquo; behavior, I used <code>Deferred</code> to simulate a &ldquo;cold publisher&rdquo; while still offering the ability to manually send values into each subscription. Let&rsquo;s change this example to remove the independent <code>PassthroughSubject</code>s created inside a <code>Deferred</code> closure and instead use a single, shared <code>PassthroughSubject</code> (without <code>Deferred</code>).</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">testSharedSubjectABCD</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">subjectA</span> <span class="p">=</span> <span class="n">PassthroughSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">scanB</span> <span class="p">=</span> <span class="n">subjectA</span><span class="p">.</span><span class="n">scan</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span> <span class="p">{</span> <span class="n">state</span><span class="p">,</span> <span class="n">next</span> <span class="k">in</span> <span class="n">state</span> <span class="o">+</span> <span class="n">next</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">receivedC</span> <span class="p">=</span> <span class="p">[</span><span class="n">Subscribers</span><span class="p">.</span><span class="n">Event</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">sinkC</span> <span class="p">=</span> <span class="n">scanB</span><span class="p">.</span><span class="n">sink</span><span class="p">(</span><span class="n">event</span><span class="p">:</span> <span class="p">{</span> <span class="n">e</span> <span class="k">in</span> <span class="n">receivedC</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">e</span><span class="p">)</span> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="n">subjectA</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">sequence</span><span class="p">:</span> <span class="mf">1.</span><span class="p">..</span><span class="mi">2</span><span class="p">,</span> <span class="n">completion</span><span class="p">:</span> <span class="kc">nil</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">receivedD</span> <span class="p">=</span> <span class="p">[</span><span class="n">Subscribers</span><span class="p">.</span><span class="n">Event</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">sinkD</span> <span class="p">=</span> <span class="n">scanB</span><span class="p">.</span><span class="n">sink</span><span class="p">(</span><span class="n">event</span><span class="p">:</span> <span class="p">{</span> <span class="n">e</span> <span class="k">in</span> <span class="n">receivedD</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">e</span><span class="p">)</span> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="n">subjectA</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">sequence</span><span class="p">:</span> <span class="mf">3.</span><span class="p">..</span><span class="mi">4</span><span class="p">,</span> <span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">receivedC</span><span class="p">,</span> <span class="p">[</span><span class="mi">11</span><span class="p">,</span> <span class="mi">13</span><span class="p">,</span> <span class="mi">16</span><span class="p">,</span> <span class="mi">20</span><span class="p">].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">receivedD</span><span class="p">,</span> <span class="p">[</span><span class="mi">13</span><span class="p">,</span> <span class="mi">17</span><span class="p">].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">sinkC</span><span class="p">.</span><span class="n">cancel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="n">sinkD</span><span class="p">.</span><span class="n">cancel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>This no longer behaves like &ldquo;resubscribe&rdquo;. Now, <code>D</code> receives only 2 values – related to the two values sent after it connected.</p> <p>This is almost what we&rsquo;d expect in a &ldquo;multicast&rdquo; scenario except that <code>D</code> doesn&rsquo;t receive the same <code>16, 20</code> that <code>C</code> receives. Instead, it receives <code>13, 17</code>. What happened?</p> <p>This graph has two conflicting ideas:</p> <ol> <li>The shared <code>PassthroughSubject</code> is a shared &ldquo;hot&rdquo; publisher</li> <li><code>scan</code> is a &ldquo;cold&rdquo; publisher and a separate value of the <code>state</code> is created for each subscriber</li> </ol> <p>While the <code>PassthroughSubject</code> is shared between <code>C</code> and <code>D</code>, there are two separate <code>Subscription</code> instances created for the <code>scan</code> (with different values of <code>state</code>) so we get different outputs.</p> <p>This is an example of hot publishers and stateful cold publishers playing poorly together. To eliminate this strangeness, we need to entirely enclose the <code>scan</code> publisher in &ldquo;hot&rdquo; endpoints so that only one <code>scan</code> subscription is ever created.</p> <p>We can do this by putting a <code>Publishers.Multicast</code> after the <code>scan</code> publisher:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">testMulticastABCD</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">subjectA</span> <span class="p">=</span> <span class="n">PassthroughSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">multicastB</span> <span class="p">=</span> <span class="n">subjectA</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">scan</span><span class="p">(</span><span class="mi">10</span><span class="p">)</span> <span class="p">{</span> <span class="n">state</span><span class="p">,</span> <span class="n">next</span> <span class="k">in</span> <span class="n">state</span> <span class="o">+</span> <span class="n">next</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">multicast</span> <span class="p">{</span> <span class="n">PassthroughSubject</span><span class="p">()</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">cancelB</span> <span class="p">=</span> <span class="n">multicastB</span><span class="p">.</span><span class="n">connect</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">receivedC</span> <span class="p">=</span> <span class="p">[</span><span class="n">Subscribers</span><span class="p">.</span><span class="n">Event</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">sinkC</span> <span class="p">=</span> <span class="n">multicastB</span><span class="p">.</span><span class="n">sink</span><span class="p">(</span><span class="n">event</span><span class="p">:</span> <span class="p">{</span> <span class="n">e</span> <span class="k">in</span> <span class="n">receivedC</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">e</span><span class="p">)</span> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="n">subjectA</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">sequence</span><span class="p">:</span> <span class="mf">1.</span><span class="p">..</span><span class="mi">2</span><span class="p">,</span> <span class="n">completion</span><span class="p">:</span> <span class="kc">nil</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">receivedD</span> <span class="p">=</span> <span class="p">[</span><span class="n">Subscribers</span><span class="p">.</span><span class="n">Event</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">sinkD</span> <span class="p">=</span> <span class="n">multicastB</span><span class="p">.</span><span class="n">sink</span><span class="p">(</span><span class="n">event</span><span class="p">:</span> <span class="p">{</span> <span class="n">e</span> <span class="k">in</span> <span class="n">receivedD</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">e</span><span class="p">)</span> <span class="p">})</span> </span></span><span class="line"><span class="cl"> <span class="n">subjectA</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">sequence</span><span class="p">:</span> <span class="mf">3.</span><span class="p">..</span><span class="mi">4</span><span class="p">,</span> <span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">receivedC</span><span class="p">,</span> <span class="p">[</span><span class="mi">11</span><span class="p">,</span> <span class="mi">13</span><span class="p">,</span> <span class="mi">16</span><span class="p">,</span> <span class="mi">20</span><span class="p">].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">receivedD</span><span class="p">,</span> <span class="p">[</span><span class="mi">16</span><span class="p">,</span> <span class="mi">20</span><span class="p">].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">sinkC</span><span class="p">.</span><span class="n">cancel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="n">sinkD</span><span class="p">.</span><span class="n">cancel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="n">cancelB</span><span class="p">.</span><span class="n">cancel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>We now have <code>C</code> and <code>D</code> correctly receiving a &ldquo;multicast&rdquo; version of the same stream. <code>D</code> receives the same <code>16, 20</code> values that <code>C</code> receives after it subscribes.</p> <h2 id="caching-computation">Caching computation</h2> <p>The other forms of &ldquo;shared computation&rdquo; that I mentioned (&ldquo;caching&rdquo;, &ldquo;latest value&rdquo; and &ldquo;custom caching&rdquo;) are all forms of holding onto recently emitted values.</p> <p>Combine offers just one built-in way of doing this: <code>CurrentValueSubject</code>, which offers &ldquo;latest value&rdquo; caching. We can use this by replacing the <code>PassthroughSubject&lt;Int, Never&gt;()</code> in the previous test with <code>CurrentValueSubject&lt;Int, Never&gt;(0)</code>. Running the test in this way confirms the following values:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">receivedC</span><span class="p">,</span> <span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">11</span><span class="p">,</span> <span class="mi">13</span><span class="p">,</span> <span class="mi">16</span><span class="p">,</span> <span class="mi">20</span><span class="p">].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">))</span> </span></span><span class="line"><span class="cl"><span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">receivedD</span><span class="p">,</span> <span class="p">[</span><span class="mi">11</span><span class="p">,</span> <span class="mi">13</span><span class="p">,</span> <span class="mi">17</span><span class="p">].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">))</span> </span></span></code></pre></div><p>A <code>CurrentValueSubject</code> is great for user-interfaces that always need a value and need only the latest &ldquo;state&rdquo; from an publisher or service.</p> <p>Beyond user-interfaces though, we often need more of the stream than the latest value.</p> <p>You might think that the <code>Publishers.Buffer</code> operator in Combine could help with this, however, this operator is actually for managing demand between a hot publisher and a downstream subscriber and doesn&rsquo;t apply to buffering between downstream subscriber. The <code>Record</code> publisher also doesn&rsquo;t seem quite right as it doesn&rsquo;t really stream values from an upstream publisher. Unless I&rsquo;m missing something, there <em>isn&rsquo;t</em> a way to multicast with a playback buffer greater than one value in Combine.</p> <p>What would be needed is a <code>Subject</code>, like <code>CurrentValueSubject</code>, that can buffer more than just the latest value (and doesn&rsquo;t force an initial value, if none has yet been received).</p> <p>I&rsquo;ve implemented a custom subject to handle this scenario: <code>BufferSubject</code>. Dropping <code>BufferSubject(limit: Int.max)</code> in place of the subject constructor in the last test confirms the following values:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">receivedC</span><span class="p">,</span> <span class="p">[</span><span class="mi">11</span><span class="p">,</span> <span class="mi">13</span><span class="p">,</span> <span class="mi">16</span><span class="p">,</span> <span class="mi">20</span><span class="p">].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">))</span> </span></span><span class="line"><span class="cl"><span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">receivedD</span><span class="p">,</span> <span class="p">[</span><span class="mi">11</span><span class="p">,</span> <span class="mi">13</span><span class="p">,</span> <span class="mi">16</span><span class="p">,</span> <span class="mi">20</span><span class="p">].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">))</span> </span></span></code></pre></div><p>This is a completely cached, &ldquo;playback&rdquo; sequence, shared between <code>C</code> and <code>D</code>. Values pass through <code>scan</code> and are sent immediately to <code>C</code> but buffered and replayed for <code>D</code> when it subscribes.</p> <h2 id="reference-lifetimes">Reference lifetimes</h2> <p>In the previous test cases, I&rsquo;ve been carefully calling <code>cancel()</code> on the <code>Subscribers.Sink</code> and the <code>connect()</code> results. This behavior is not because I wanted to cancel these values but instead because I wanted to guarantee that I was keeping their references alive so their associated subscriptions don&rsquo;t get cancelled until the end of the function.</p> <p>Is carefully keeping references alive strictly necessary?</p> <p>The answer is complicated to test for two reasons:</p> <ol> <li>In Release builds, Swift may release references in the middle of a scope (like a function) but in Debug builds, Swift usually won&rsquo;t release until the end of the scope.</li> <li><code>AnyCancellable</code> is a reference that calls <code>cancel()</code> automatically on <code>deinit</code>. Other types of <code>Cancellable</code> will usually not but might (you never know).</li> </ol> <p>Neither of these rules make accurate analysis easy. Let&rsquo;s start with some examples that use <code>AnyCancellable</code> (avoiding complications with point 2) and deliberately create our own dummy scopes (which will avoid most complications with point 1).</p> <p>This is how <code>AnyCancellable</code> behaves:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">testAnyCancellable</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">subject</span> <span class="p">=</span> <span class="n">PassthroughSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;()</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">received</span> <span class="p">=</span> <span class="p">[</span><span class="n">Subscribers</span><span class="p">.</span><span class="n">Event</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kr">weak</span> <span class="kd">var</span> <span class="nv">weakCancellable</span><span class="p">:</span> <span class="n">AnyCancellable</span><span class="p">?</span> </span></span><span class="line"><span class="cl"> <span class="k">do</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">anyCancellable</span> <span class="p">=</span> <span class="n">subject</span><span class="p">.</span><span class="n">sink</span><span class="p">(</span><span class="n">event</span><span class="p">:</span> <span class="p">{</span> <span class="n">e</span> <span class="k">in</span> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">e</span><span class="p">)</span> <span class="p">}</span> <span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">weakCancellable</span> <span class="p">=</span> <span class="n">anyCancellable</span> </span></span><span class="line"><span class="cl"> <span class="n">subject</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertNil</span><span class="p">(</span><span class="n">weakCancellable</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">subject</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">received</span><span class="p">,</span> <span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="kc">nil</span><span class="p">))</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>The <code>anyCancellable</code> falls out of scope at the end of the <code>do {}</code>, cancelling the subscription so sending the second value (<code>send(2)</code>) has no effect.</p> <p>This is probably the behavior that is most in-line with expectations but let&rsquo;s look instead at raw use of <code>Subscribers.Sink</code>, which conforms to <code>Cancellable</code> but does not offer the same &ldquo;auto-cancel when released&rdquo; guarantee:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">testSinkCancellation</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">subject</span> <span class="p">=</span> <span class="n">PassthroughSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;()</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">received</span> <span class="p">=</span> <span class="p">[</span><span class="n">Subscribers</span><span class="p">.</span><span class="n">Event</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kr">weak</span> <span class="kd">var</span> <span class="nv">weakSink</span><span class="p">:</span> <span class="n">Subscribers</span><span class="p">.</span><span class="n">Sink</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;?</span> </span></span><span class="line"><span class="cl"> <span class="k">do</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">sink</span> <span class="p">=</span> <span class="n">Subscribers</span><span class="p">.</span><span class="n">Sink</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;(</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveCompletion</span><span class="p">:</span> <span class="p">{</span> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">complete</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveValue</span><span class="p">:</span> <span class="p">{</span> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">value</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">weakSink</span> <span class="p">=</span> <span class="n">sink</span> </span></span><span class="line"><span class="cl"> <span class="n">subject</span><span class="p">.</span><span class="n">subscribe</span><span class="p">(</span><span class="n">sink</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">subject</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertNotNil</span><span class="p">(</span><span class="n">weakSink</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">subject</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">weakSink</span><span class="p">?.</span><span class="n">cancel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="n">subject</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">received</span><span class="p">,</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="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="kc">nil</span><span class="p">))</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>The <code>Sink</code> is not strongly referenced outside the <code>do {}</code> but is still alive when the <code>send(2)</code> value reaches the <code>received</code> array. It is only when we <em>explicitly</em> call <code>cancel()</code> that further values stop being delivered.</p> <p>If we drop all our strong references to <code>Sink</code>, it can continue to receive values while there is an active subscription. In fact, we can drop our <code>PassthroughSubject</code>, too:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">testOwnership</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">received</span> <span class="p">=</span> <span class="p">[</span><span class="n">Subscribers</span><span class="p">.</span><span class="n">Event</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kr">weak</span> <span class="kd">var</span> <span class="nv">weakSubject</span><span class="p">:</span> <span class="n">PassthroughSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;?</span> </span></span><span class="line"><span class="cl"> <span class="kr">weak</span> <span class="kd">var</span> <span class="nv">weakSink</span><span class="p">:</span> <span class="n">Subscribers</span><span class="p">.</span><span class="n">Sink</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;?</span> </span></span><span class="line"><span class="cl"> <span class="k">do</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">subject</span> <span class="p">=</span> <span class="n">PassthroughSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;()</span> </span></span><span class="line"><span class="cl"> <span class="n">weakSubject</span> <span class="p">=</span> <span class="n">subject</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">sink</span> <span class="p">=</span> <span class="n">Subscribers</span><span class="p">.</span><span class="n">Sink</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;(</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveCompletion</span><span class="p">:</span> <span class="p">{</span> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">complete</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveValue</span><span class="p">:</span> <span class="p">{</span> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">value</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">weakSink</span> <span class="p">=</span> <span class="n">sink</span> </span></span><span class="line"><span class="cl"> <span class="n">subject</span><span class="p">.</span><span class="n">subscribe</span><span class="p">(</span><span class="n">sink</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertNotNil</span><span class="p">(</span><span class="n">weakSubject</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertNotNil</span><span class="p">(</span><span class="n">weakSink</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">weakSubject</span><span class="p">?.</span><span class="n">send</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">weakSubject</span><span class="p">?.</span><span class="n">send</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertNil</span><span class="p">(</span><span class="n">weakSubject</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertNil</span><span class="p">(</span><span class="n">weakSink</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">received</span><span class="p">,</span> <span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">))</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>We don&rsquo;t have to hold <em>any</em> references to the graph. It continues to stay alive, all on its own, until the current subscription completes.</p> <p>This is a dangerous scenario. In short, to have sane memory management with Combine, you <em>must</em> ensure there is at least one <code>AnyCancellable</code> connected to your graph.</p> <h2 id="multiple-subscriptions">Multiple subscriptions</h2> <p>Many of these test cases have focussed on what happens when there are multiple <code>Subscriber</code>s connected to the same <code>Publisher</code>.</p> <p>What happens if you do the inverse: take a single <code>Subscribers.Sink</code> and subscribe it to multiple <code>Publisher</code>s?</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">testMultipleSubscribe</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">subject1</span> <span class="p">=</span> <span class="n">PassthroughSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">subject2</span> <span class="p">=</span> <span class="n">PassthroughSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;()</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">received</span> <span class="p">=</span> <span class="p">[</span><span class="n">Subscribers</span><span class="p">.</span><span class="n">Event</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">sink</span> <span class="p">=</span> <span class="n">Subscribers</span><span class="p">.</span><span class="n">Sink</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;(</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveCompletion</span><span class="p">:</span> <span class="p">{</span> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">complete</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveValue</span><span class="p">:</span> <span class="p">{</span> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">value</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">subject1</span><span class="p">.</span><span class="n">subscribe</span><span class="p">(</span><span class="n">sink</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">subject2</span><span class="p">.</span><span class="n">subscribe</span><span class="p">(</span><span class="n">sink</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">subject1</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">sequence</span><span class="p">:</span> <span class="mf">1.</span><span class="p">..</span><span class="mi">2</span><span class="p">,</span> <span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">subject2</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">sequence</span><span class="p">:</span> <span class="mf">3.</span><span class="p">..</span><span class="mi">4</span><span class="p">,</span> <span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">received</span><span class="p">,</span> <span class="p">(</span><span class="mf">1.</span><span class="p">..</span><span class="mi">2</span><span class="p">).</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">))</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Short answer: the <code>Sink</code> remains subscribed to the first subject and ignores everything from the second. However, it&rsquo;s not an error of any kind, just a silent failure. It seems like sharing a <code>Subscriber</code> isn&rsquo;t something you should ever do.</p> <p>This test isn&rsquo;t just abstract interface abuse. I want something that offers the ability to subscribe multiple times to different upstream subscriptions with this kind of simplicity. For the first subscription to a <code>Publisher</code>, the downstream <code>Subscriber</code> will own the subscription and will <code>cancel</code> it if the downstream nodes are cancelled. It&rsquo;s elegant and self-contained but handles just one subscription.</p> <p>The only multi-subscribable interface in Combine, by default, is <code>Subject</code>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">testMultiSubjectSubscribe</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">subject1</span> <span class="p">=</span> <span class="n">PassthroughSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">subject2</span> <span class="p">=</span> <span class="n">PassthroughSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">multiInputSubject</span> <span class="p">=</span> <span class="n">PassthroughSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">cancellable1</span> <span class="p">=</span> <span class="n">subject1</span><span class="p">.</span><span class="n">subscribe</span><span class="p">(</span><span class="n">multiInputSubject</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">cancellable2</span> <span class="p">=</span> <span class="n">subject2</span><span class="p">.</span><span class="n">subscribe</span><span class="p">(</span><span class="n">multiInputSubject</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">received</span> <span class="p">=</span> <span class="p">[</span><span class="n">Subscribers</span><span class="p">.</span><span class="n">Event</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">multiInputCancellable</span> <span class="p">=</span> <span class="n">multiInputSubject</span><span class="p">.</span><span class="n">sink</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveCompletion</span><span class="p">:</span> <span class="p">{</span> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">complete</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveValue</span><span class="p">:</span> <span class="p">{</span> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">value</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">subject1</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">sequence</span><span class="p">:</span> <span class="mf">1.</span><span class="p">..</span><span class="mi">2</span><span class="p">,</span> <span class="n">completion</span><span class="p">:</span> <span class="kc">nil</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">subject2</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">sequence</span><span class="p">:</span> <span class="mf">3.</span><span class="p">..</span><span class="mi">4</span><span class="p">,</span> <span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">received</span><span class="p">,</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="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">cancellable1</span><span class="p">.</span><span class="n">cancel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="n">cancellable2</span><span class="p">.</span><span class="n">cancel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="n">multiInputCancellable</span><span class="p">.</span><span class="n">cancel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>This works – the <code>multiInputCancellable</code> receives from both <code>subject</code> and <code>subject2</code> – but you must hold onto each <code>AnyCancellable</code> and there&rsquo;s no implicit cancellation of upstream subscriptions the subject falls out of scope.</p> <p>Fortunately, it&rsquo;s very easy to set up a better approach where these upstream <code>Cancellable</code>s are held internally and automatically cancelled when the downstream is cancelled. It involves little more than a combination of the subjects and sinks from the previous test, hidden behind a clean interface:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">testMergeSink</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">received</span> <span class="p">=</span> <span class="p">[</span><span class="n">Subscribers</span><span class="p">.</span><span class="n">Event</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">subject1</span> <span class="p">=</span> <span class="n">PassthroughSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">subject2</span> <span class="p">=</span> <span class="n">PassthroughSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">input</span> <span class="p">=</span> <span class="n">MergeInput</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">&gt;()</span> </span></span><span class="line"><span class="cl"> <span class="n">subject1</span><span class="p">.</span><span class="n">merge</span><span class="p">(</span><span class="n">into</span><span class="p">:</span> <span class="n">input</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">subject2</span><span class="p">.</span><span class="n">merge</span><span class="p">(</span><span class="n">into</span><span class="p">:</span> <span class="n">input</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">cancellable</span> <span class="p">=</span> <span class="n">input</span><span class="p">.</span><span class="n">sink</span><span class="p">(</span><span class="n">receiveValue</span><span class="p">:</span> <span class="p">{</span> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">value</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">})</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">subject1</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">sequence</span><span class="p">:</span> <span class="mf">1.</span><span class="p">..</span><span class="mi">2</span><span class="p">,</span> <span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">subject2</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">sequence</span><span class="p">:</span> <span class="mf">3.</span><span class="p">..</span><span class="mi">4</span><span class="p">,</span> <span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">received</span><span class="p">,</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="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="kc">nil</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">cancellable</span><span class="p">.</span><span class="n">cancel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>This <code>MergeInput</code> interface does not receive completion from upstream publishers. This is an intentional choice: for this type of multi-input scenario, you generally don&rsquo;t want one input to close the <code>MergeInput</code> and cut off all other inputs.</p> <h2 id="subscriber-reactivation">Subscriber reactivation</h2> <p>The previous tests revealed that a <code>Subscriber</code> will not accept new subscriptions while it already has an active subscription. A related question is: what happens if you subscribe to a second <code>Publisher</code> after the first subscription completes? Can you &ldquo;reactivate&rdquo; a completed subscriber?</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">testSinkReactivation</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">received</span> <span class="p">=</span> <span class="p">[</span><span class="n">Subscribers</span><span class="p">.</span><span class="n">Event</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">sink</span> <span class="p">=</span> <span class="n">Subscribers</span><span class="p">.</span><span class="n">Sink</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;(</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveCompletion</span><span class="p">:</span> <span class="p">{</span> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">complete</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveValue</span><span class="p">:</span> <span class="p">{</span> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">value</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kr">weak</span> <span class="kd">var</span> <span class="nv">weakSubject</span><span class="p">:</span> <span class="n">PassthroughSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;?</span> </span></span><span class="line"><span class="cl"> <span class="k">do</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">subject</span> <span class="p">=</span> <span class="n">PassthroughSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;()</span> </span></span><span class="line"><span class="cl"> <span class="n">weakSubject</span> <span class="p">=</span> <span class="n">subject</span> </span></span><span class="line"><span class="cl"> <span class="n">subject</span><span class="p">.</span><span class="n">subscribe</span><span class="p">(</span><span class="n">sink</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">subject</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertNotNil</span><span class="p">(</span><span class="n">weakSubject</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">weakSubject</span><span class="p">?.</span><span class="n">send</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// At this point, the first subscription to sink is finished</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertNil</span><span class="p">(</span><span class="n">weakSubject</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">received</span><span class="p">,</span> <span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="c1">// Try to start a new one</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">subject2</span> <span class="p">=</span> <span class="n">PassthroughSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;()</span> </span></span><span class="line"><span class="cl"> <span class="n">subject2</span><span class="p">.</span><span class="n">subscribe</span><span class="p">(</span><span class="n">sink</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">subject2</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="cp">#if</span> <span class="kc">false</span> </span></span><span class="line"><span class="cl"> <span class="c1">// Prior to macOS 10.15 beta 6...</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">received</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">)</span> <span class="o">+</span> <span class="p">[</span><span class="mi">2</span><span class="p">].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="kc">nil</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="cp">#else</span> </span></span><span class="line"><span class="cl"> <span class="c1">// In macOS 10.15 beta 6...</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">received</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="cp">#endif</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>This is an example of behavior that changed while I was writing this series on Combine. Until macOS 10.15 beta 7, it was possible to reuse a subscriber, once any previous subscription completed.</p> <p>Apparently, that changed in beta 7. Now, a <code>Subscriber</code> is strictly a non-shareable object. It lives for the duration of a subscription and subsequently declines to do anything more.</p> <h2 id="conclusion">Conclusion</h2> <blockquote> <p><strong>Download</strong>: The code for this series, <a href="https://github.com/mattgallagher/CombineExploration">CombineExploration, is available on github</a>.</p> </blockquote> <p>I don&rsquo;t personally like cold publishers. I think they are a counter-intuitive solution to some edge cases that can create problems in some common cases. Any interaction between hot publishers and stateful cold publishers needs to be carefully managed to achieve consistent outputs.</p> <p>Combine offers no built-in way to cache more than a single value. Fortunately, I&rsquo;ve shown that it isn&rsquo;t particularly difficult to buffer additional values by creating a custom <code>BufferSubject</code> that you can use with <code>Multicast</code>.</p> <p>The subscriber graph in Combine maintains a reference counted loop while a subscription is active. If you&rsquo;re using <code>.sink</code> (or a handful of other convenience methods) as part of your graph, this won&rsquo;t matter, since the returned <code>AnyCancellable</code> will break the loop. However, more manual constructions – like using <code>Subscribers.Sink</code> – don&rsquo;t offer the same convenience so be careful to wrap types in <code>AnyCancellable</code> as appropriate to avoid memory leaks.</p> <p>Tests reveal <code>Subscribers.Sink</code> will not trigger any kind of error if you subscribe it multiple times but it will not listen to new subscriptions.</p> <p>If you want a sink that handle multiple inputs, you must use a <code>Subject</code>. The default <code>Publisher.subscribe(Subject)</code> is a little fussy (requiring you hold onto additional <code>Cancellable</code> instances) so I provided a convenience <code>merge(into:)</code> for the purpose.</p> <h3 id="looking-forward">Looking forward&hellip;</h3> <p>Given the tested behavior showing <code>Subscriber</code> ignores subsequent subscriptions and attempts at reuse after completion, you might expect that <code>Subscriber</code> will never emit values once it reaches the end of a sequence or is cancelled.</p> <p>You&rsquo;d be wrong.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">testSinkCancellationPlusAsyncDelivery</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">received</span> <span class="p">=</span> <span class="p">[</span><span class="n">Subscribers</span><span class="p">.</span><span class="n">Event</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">sink</span> <span class="p">=</span> <span class="n">Just</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">receive</span><span class="p">(</span><span class="n">on</span><span class="p">:</span> <span class="n">DispatchQueue</span><span class="p">.</span><span class="n">main</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">.</span><span class="n">sink</span><span class="p">(</span><span class="n">event</span><span class="p">:</span> <span class="p">{</span> <span class="n">e</span> <span class="k">in</span> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">e</span><span class="p">)</span> <span class="p">})</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">sink</span><span class="p">.</span><span class="n">cancel</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">received</span><span class="p">,</span> <span class="p">[].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="kc">nil</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">RunLoop</span><span class="p">.</span><span class="n">main</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">until</span><span class="p">:</span> <span class="n">Date</span><span class="p">(</span><span class="n">timeIntervalSinceNow</span><span class="p">:</span> <span class="mf">0.001</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">received</span><span class="p">,</span> <span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">))</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>This test shows us cancelling a sink, at which point we have received no values. Later a value arrives on our cancelled sink.</p> <p>There&rsquo;s a possibility that this behavior is Combine &ldquo;behaving as expected&rdquo; but realistically there is no scenario where this is a good outcome. Combine has some serious rough edges around anything asynchronous and this is not the only scenario that causes problems.</p> <p>In the final part of this series, I&rsquo;ll look at why this behavior occurs and some possible workarounds until Combine fixes its design to eliminate these problems.</p> <br/>Copyright Matt Gallagher, 2024. All rights reserved. Code samples may be use in accordance with the ISC-style license at https://www.cocoawithlove.com/about.html 22 short tests of Combine – Part 1: Protocols https://www.cocoawithlove.com/blog/twenty-two-short-tests-of-combine-part-1.html Fri, 16 Aug 2019 10:16:01 +1000 https://www.cocoawithlove.com/blog/twenty-two-short-tests-of-combine-part-1.html <p>I wrote some experiments around Combine, Apple&rsquo;s reactive programming framework, to gain insight into how Combine handles edge cases that have caused problems for me in other reactive programming frameworks. How do subscriptions work? How do I cache computations? When are publishers and subscribers released? Under what circumstances is Combine thread-safe? Is re-entrancy possible? Does Combine guarantee delivery-order? How does Combine&rsquo;s performance compare to pre-existing reactive frameworks?</p> <p>Looking at everything in one article got much too long so I broke it into three parts:</p> <ol> <li>re-implementing the core protocols of Combine</li> <li><a href="https://www.cocoawithlove.com/blog/twenty-two-short-tests-of-combine-part-2.html">a trio of topics: shared computation, shared reference lifetimes and sharing subscribers</a></li> <li><a href="https://www.cocoawithlove.com/blog/twenty-two-short-tests-of-combine-part-3.html">asynchrony, threading and performance</a></li> </ol> <p>This article will be the first third of my investigation, covering an effort to re-implement the three key protocols of Combine: <code>Publisher</code>, <code>Subscriber</code> and <code>Subscription</code>.</p> <blockquote> <p><strong>Download</strong>: The code for this series, <a href="https://github.com/mattgallagher/CombineExploration">CombineExploration, is available on github</a>.</p> </blockquote> <blockquote> <p><strong>Warning</strong>: This is not a tutorial for Combine. I won&rsquo;t be using Combine in anything resembling a conventional manner. This is going to be a look at some edge cases in Combine, testing behaviors that aren&rsquo;t really documented and may therefore change in future.</p> </blockquote> <!-- TOC --> <h2 id="publisher-and-subscriber">Publisher and Subscriber</h2> <p>Apple&rsquo;s Combine is built around two key protocols, <code>Publisher</code> and <code>Subscriber</code>.</p> <blockquote> <p><strong>The naïve intepretation of Combine</strong> is that a <code>Publisher</code> emits a sequence of values.</p> </blockquote> <p>This common interpretation is not accurate but the distinction between this &ldquo;naïve&rdquo; interpretation and an &ldquo;accurate&rdquo; interpretation is rare enough that we often ignore the difference.</p> <p><code>Publisher</code> is defined as:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">protocol</span> <span class="nc">Publisher</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">associatedtype</span> <span class="n">Output</span> </span></span><span class="line"><span class="cl"> <span class="n">associatedtype</span> <span class="n">Failure</span> <span class="p">:</span> <span class="n">Error</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span> <span class="nf">receive</span><span class="p">&lt;</span><span class="n">S</span><span class="p">&gt;(</span><span class="n">subscriber</span><span class="p">:</span> <span class="n">S</span><span class="p">)</span> <span class="k">where</span> <span class="n">S</span> <span class="p">:</span> <span class="n">Subscriber</span><span class="p">,</span> <span class="kc">Self</span><span class="p">.</span><span class="n">Failure</span> <span class="p">==</span> <span class="n">S</span><span class="p">.</span><span class="n">Failure</span><span class="p">,</span> <span class="kc">Self</span><span class="p">.</span><span class="n">Output</span> <span class="p">==</span> <span class="n">S</span><span class="p">.</span><span class="n">Input</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>According to the protocol, a <code>Publisher</code> does not emit values but receives <code>Subscriber</code>s. Of course, depending on what a <code>Subscriber</code> is, the <code>Publisher</code> might still directly emit values into these <code>Subscriber</code>s that it receives.</p> <p>So let&rsquo;s look at the <code>Subscriber</code> protocol for a clearer picture:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">protocol</span> <span class="nc">Subscriber</span> <span class="p">:</span> <span class="n">CustomCombineIdentifierConvertible</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">associatedtype</span> <span class="n">Input</span> </span></span><span class="line"><span class="cl"> <span class="n">associatedtype</span> <span class="n">Failure</span> <span class="p">:</span> <span class="n">Error</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span> <span class="nf">receive</span><span class="p">(</span><span class="kc">_</span> <span class="n">input</span><span class="p">:</span> <span class="kc">Self</span><span class="p">.</span><span class="n">Input</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="n">Subscribers</span><span class="p">.</span><span class="n">Demand</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span> <span class="nf">receive</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="n">Subscribers</span><span class="p">.</span><span class="n">Completion</span><span class="p">&lt;</span><span class="kc">Self</span><span class="p">.</span><span class="n">Failure</span><span class="p">&gt;)</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span> <span class="nf">receive</span><span class="p">(</span><span class="n">subscription</span><span class="p">:</span> <span class="n">Subscription</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>Ignoring that last function for now, the other functions here on <code>Subscriber</code> appear to support the &ldquo;naïve&rdquo; interpretation: the <code>Publisher</code> receives <code>Subscriber</code>s and can send <code>Input</code> values or <code>Completion</code> events directly to all known <code>Subscriber</code>s.</p> <p>Let&rsquo;s establish a &ldquo;control case&rdquo; to which we can compare other tests, starting with a fairly standard test of the <code>Publisher</code> and <code>Subscriber</code> model where a <code>Subscribers.Sink</code> (a typical <code>Subscriber</code>) is subscribed to a <code>PassthroughSubject</code> (not exactly a typical <code>Publisher</code> but helpful in tests since it lets us inject values easily from outside) and we record the values that pass from the subject to the sink.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">testSubjectSink</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">subject</span> <span class="p">=</span> <span class="n">PassthroughSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;()</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">received</span> <span class="p">=</span> <span class="p">[</span><span class="n">Subscribers</span><span class="p">.</span><span class="n">Event</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">sink</span> <span class="p">=</span> <span class="n">Subscribers</span><span class="p">.</span><span class="n">Sink</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;(</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveCompletion</span><span class="p">:</span> <span class="p">{</span> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">complete</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveValue</span><span class="p">:</span> <span class="p">{</span> <span class="n">received</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">value</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">subject</span><span class="p">.</span><span class="n">subscribe</span><span class="p">(</span><span class="n">sink</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">subject</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">sequence</span><span class="p">:</span> <span class="mf">1.</span><span class="p">..</span><span class="mi">3</span><span class="p">,</span> <span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">received</span><span class="p">,</span> <span class="p">(</span><span class="mf">1.</span><span class="p">..</span><span class="mi">3</span><span class="p">).</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">))</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>This test includes a few of my own additions to make the tests easier:</p> <ul> <li><code>Subscribers.Event</code> is just an &ldquo;either&rdquo; over the <code>Value</code> and <code>Completion</code> types of a Combine sequence</li> <li><code>send(sequence:completion:)</code> sends all values in the sequence and the completion</li> <li><code>asEvents</code> creates an array of <code>Subscribers.Event</code> from an array of <code>Value</code> and a <code>Completion</code>.</li> </ul> <p>This test conforms to the &ldquo;naïve&rdquo; interpretation: values are sent to the subject are received by the closure we passed to the sink.</p> <h2 id="graph-mutations-over-time">Graph mutations over time</h2> <p>Imagine a basic subject, <code>A</code>, that generates values over time (e.g. a network connection), followed by a stateful transforming node <code>B</code> (e.g. a <code>scan</code> or similar streaming processor), followed by an observer <code>C</code> (e.g. a <code>Sink</code>):</p> <p><img src="https://www.cocoawithlove.com/assets/blog/combine1.svg" alt=""></p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">testScan</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">subjectA</span> <span class="p">=</span> <span class="n">PassthroughSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">scanB</span> <span class="p">=</span> <span class="n">Publishers</span><span class="p">.</span><span class="n">Scan</span><span class="p">(</span><span class="n">upstream</span><span class="p">:</span> <span class="n">subjectA</span><span class="p">,</span> <span class="n">initialResult</span><span class="p">:</span> <span class="mi">10</span><span class="p">)</span> <span class="p">{</span> <span class="n">state</span><span class="p">,</span> <span class="n">next</span> <span class="k">in</span> <span class="n">state</span> <span class="o">+</span> <span class="n">next</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">receivedC</span> <span class="p">=</span> <span class="p">[</span><span class="n">Subscribers</span><span class="p">.</span><span class="n">Event</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">sinkC</span> <span class="p">=</span> <span class="n">Subscribers</span><span class="p">.</span><span class="n">Sink</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;(</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveCompletion</span><span class="p">:</span> <span class="p">{</span> <span class="n">receivedC</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">complete</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveValue</span><span class="p">:</span> <span class="p">{</span> <span class="n">receivedC</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">value</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">scanB</span><span class="p">.</span><span class="n">subscribe</span><span class="p">(</span><span class="n">sinkC</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">subjectA</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">sequence</span><span class="p">:</span> <span class="mf">1.</span><span class="p">..</span><span class="mi">4</span><span class="p">,</span> <span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">receivedC</span><span class="p">,</span> <span class="p">[</span><span class="mi">11</span><span class="p">,</span> <span class="mi">13</span><span class="p">,</span> <span class="mi">16</span><span class="p">,</span> <span class="mi">20</span><span class="p">].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">))</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>There&rsquo;s an extra transformation line (the <code>scanB</code> line) but relative to the original control case it&rsquo;s not much different.</p> <p>Now, what happens when, halfway through <code>A</code> streaming its data, a new observer <code>D</code> subscribes to <code>B</code>, totally unaware that <code>B</code> is already in the middle of its output?</p> <p><img src="https://www.cocoawithlove.com/assets/blog/combine2.svg" alt=""></p> <p>Should the new listener <code>D</code> get half the data it expected, even though it doesn&rsquo;t know about <code>C</code> and the fact that the connection is already started?</p> <p>The answer is complicated. Depending on your program&rsquo;s logic, you may want <em>any</em> of the following options:</p> <ol> <li><strong>multicast</strong> – <code>D</code> receives the second half of the values that <code>C</code> receives</li> <li><strong>caching</strong> – the first half is buffered and <code>D</code> immediately receives the first half of the message upon joining and new values like multicast</li> <li><strong>latest value</strong> – <code>D</code> receives the last emitted value immediately and new values like multicast</li> <li><strong>custom caching</strong> – <code>D</code> receives only as much as needed (e.g. since the last keyframe or resume point) and new values like multicast</li> <li><strong>resubscribe</strong> – <code>D</code> should trigger all upstream nodes to restart their work, go all the way back to the network and re-request all data, performing all calculations, again</li> </ol> <p>In this article, I will focus only on the last of these options since it is, arguably, the default behavior in Combine. In the next article, I&rsquo;ll look at the other approaches.</p> <p>For now though, here&rsquo;s an example of resubscribe behavior:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">testSequenceABCD</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">sequenceA</span> <span class="p">=</span> <span class="n">Publishers</span><span class="p">.</span><span class="n">Sequence</span><span class="p">&lt;</span><span class="n">ClosedRange</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">&gt;,</span> <span class="n">Never</span><span class="p">&gt;(</span><span class="n">sequence</span><span class="p">:</span> <span class="mf">1.</span><span class="p">..</span><span class="mi">4</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">scanB</span> <span class="p">=</span> <span class="n">Publishers</span><span class="p">.</span><span class="n">Scan</span><span class="p">(</span><span class="n">upstream</span><span class="p">:</span> <span class="n">sequenceA</span><span class="p">,</span> <span class="n">initialResult</span><span class="p">:</span> <span class="mi">10</span><span class="p">)</span> <span class="p">{</span> <span class="n">state</span><span class="p">,</span> <span class="n">next</span> <span class="k">in</span> <span class="n">state</span> <span class="o">+</span> <span class="n">next</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">receivedC</span> <span class="p">=</span> <span class="p">[</span><span class="n">Subscribers</span><span class="p">.</span><span class="n">Event</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">sinkC</span> <span class="p">=</span> <span class="n">Subscribers</span><span class="p">.</span><span class="n">Sink</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;(</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveCompletion</span><span class="p">:</span> <span class="p">{</span> <span class="n">receivedC</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">complete</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveValue</span><span class="p">:</span> <span class="p">{</span> <span class="n">receivedC</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">value</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">receivedD</span> <span class="p">=</span> <span class="p">[</span><span class="n">Subscribers</span><span class="p">.</span><span class="n">Event</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">sinkD</span> <span class="p">=</span> <span class="n">Subscribers</span><span class="p">.</span><span class="n">Sink</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;(</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveCompletion</span><span class="p">:</span> <span class="p">{</span> <span class="n">receivedD</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">complete</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveValue</span><span class="p">:</span> <span class="p">{</span> <span class="n">receivedD</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">value</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">scanB</span><span class="p">.</span><span class="n">subscribe</span><span class="p">(</span><span class="n">sinkC</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">scanB</span><span class="p">.</span><span class="n">subscribe</span><span class="p">(</span><span class="n">sinkD</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">receivedC</span><span class="p">,</span> <span class="p">[</span><span class="mi">11</span><span class="p">,</span> <span class="mi">13</span><span class="p">,</span> <span class="mi">16</span><span class="p">,</span> <span class="mi">20</span><span class="p">].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">receivedD</span><span class="p">,</span> <span class="p">[</span><span class="mi">11</span><span class="p">,</span> <span class="mi">13</span><span class="p">,</span> <span class="mi">16</span><span class="p">,</span> <span class="mi">20</span><span class="p">].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">))</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>None of the nodes here are recreated and most importantly, the <code>B</code> node – the stateful <code>scan</code> processor – is shared between subscriptions, yet each of <code>C</code> and <code>D</code> receive an independent version of the values.</p> <p>In case you think something weird is happening because the sequences don&rsquo;t actually overlap in time, here&rsquo;s an equivalent test where the sequences values are manually delivered in an overlapping fashion:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">testOverlappingABCD</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">subjects</span> <span class="p">=</span> <span class="p">[</span><span class="n">PassthroughSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">deferred</span> <span class="p">=</span> <span class="n">Deferred</span> <span class="p">{</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="n">PassthroughSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;</span> <span class="k">in</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">request</span> <span class="p">=</span> <span class="n">PassthroughSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;()</span> </span></span><span class="line"><span class="cl"> <span class="n">subjects</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">request</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">request</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">scanB</span> <span class="p">=</span> <span class="n">Publishers</span><span class="p">.</span><span class="n">Scan</span><span class="p">(</span><span class="n">upstream</span><span class="p">:</span> <span class="n">deferred</span><span class="p">,</span> <span class="n">initialResult</span><span class="p">:</span> <span class="mi">10</span><span class="p">)</span> <span class="p">{</span> <span class="n">state</span><span class="p">,</span> <span class="n">next</span> <span class="k">in</span> <span class="n">state</span> <span class="o">+</span> <span class="n">next</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">receivedC</span> <span class="p">=</span> <span class="p">[</span><span class="n">Subscribers</span><span class="p">.</span><span class="n">Event</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">sinkC</span> <span class="p">=</span> <span class="n">Subscribers</span><span class="p">.</span><span class="n">Sink</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;(</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveCompletion</span><span class="p">:</span> <span class="p">{</span> <span class="n">receivedC</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">complete</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveValue</span><span class="p">:</span> <span class="p">{</span> <span class="n">receivedC</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">value</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">receivedD</span> <span class="p">=</span> <span class="p">[</span><span class="n">Subscribers</span><span class="p">.</span><span class="n">Event</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">sinkD</span> <span class="p">=</span> <span class="n">Subscribers</span><span class="p">.</span><span class="n">Sink</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;(</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveCompletion</span><span class="p">:</span> <span class="p">{</span> <span class="n">receivedD</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">complete</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveValue</span><span class="p">:</span> <span class="p">{</span> <span class="n">receivedD</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">value</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">scanB</span><span class="p">.</span><span class="n">subscribe</span><span class="p">(</span><span class="n">sinkC</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">subjects</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">send</span><span class="p">(</span><span class="n">sequence</span><span class="p">:</span> <span class="mf">1.</span><span class="p">..</span><span class="mi">2</span><span class="p">,</span> <span class="n">completion</span><span class="p">:</span> <span class="kc">nil</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">scanB</span><span class="p">.</span><span class="n">subscribe</span><span class="p">(</span><span class="n">sinkD</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">subjects</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">send</span><span class="p">(</span><span class="n">sequence</span><span class="p">:</span> <span class="mf">3.</span><span class="p">..</span><span class="mi">4</span><span class="p">,</span> <span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">subjects</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">send</span><span class="p">(</span><span class="n">sequence</span><span class="p">:</span> <span class="mf">1.</span><span class="p">..</span><span class="mi">4</span><span class="p">,</span> <span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">receivedC</span><span class="p">,</span> <span class="p">[</span><span class="mi">11</span><span class="p">,</span> <span class="mi">13</span><span class="p">,</span> <span class="mi">16</span><span class="p">,</span> <span class="mi">20</span><span class="p">].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">receivedD</span><span class="p">,</span> <span class="p">[</span><span class="mi">11</span><span class="p">,</span> <span class="mi">13</span><span class="p">,</span> <span class="mi">16</span><span class="p">,</span> <span class="mi">20</span><span class="p">].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">))</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>This test case shows that the &ldquo;naïve&rdquo; interpretation of Combine cannot properly describe how Combine works in all cases. While there are two <code>PassthroughSubject</code>s, two <code>Subscriber.Sink</code>s, there is only one <code>scanB</code> node in the <code>Publisher</code> graph, yet it behaves like two completely different nodes – one for the <code>sinkC</code> and one for the <code>sinkD</code>.</p> <h2 id="subscription-the-mostly-hidden-type">Subscription, the mostly-hidden type</h2> <p>How does this work?</p> <p>Despite the programmer creating a single graph of <code>Publisher</code>s, there is a shadow graph of other instances that really performs the value processing and sending. We can see this shadow graph in the last function in <code>Subscriber</code> protocol.</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">receive</span><span class="p">(</span><span class="n">subscription</span><span class="p">:</span> <span class="n">Subscription</span><span class="p">)</span> </span></span></code></pre></div><p>Every <code>Publisher</code> in your graph is shadowed by one instance of <code>Subscription</code> per active <code>Subscriber</code>.</p> <p>We didn&rsquo;t see the effects of this shadow <code>Subscription</code> graph in the first <code>testScan</code> example because the shared <code>PassthroughSubject</code> tied all the subscriptions together but when we moved to using <code>Deferred</code>, the graphs become untied and independent and we could see the effects of multiple <code>Subscription</code>s at the <code>scan</code> node.</p> <blockquote> <p><strong>The accurate interpretation of Combine</strong> is that values are sent and processed through a graph of <code>Subscription</code> instances, lazily constructed by <code>Publisher</code> instances on a per-subscribe basis.</p> </blockquote> <p>We don&rsquo;t usually interact with <code>Subscription</code> instances. <code>Subscription</code> instances are created automatically by <code>Publisher</code>s when a <code>Subscriber</code> subscribes. The graph of <code>Subscription</code> instances mirrors the graph of <code>Publisher</code>s.</p> <p>You can see why the distinction between the <code>Publisher</code> graph and the <code>Subscriber</code> graph (the distinction between the &ldquo;naïve&rdquo; and &ldquo;accurate&rdquo; interpretations) can be confusing. Further adding to the confusion is that there are no usable public implementations of <code>Subscription</code> (I&rsquo;m ignoring <code>Subscriptions.empty</code> which is a placeholder that ignores everything).</p> <p>The subscription concept was introduced by the Reactive Extensions for .NET, attempting to make each mutation of the graph behave like a completely separate, unrelated graph – as it might appear in a strict functional programming language. However, strict functional programming languages cache function results, so redundant recalculation of upstream values is avoided. In Swift, if we don&rsquo;t cache it ourselves, everything is repeated.</p> <p>If I wanted to repeat all the processing, I would have recreated the publisher graph.</p> <p>When I wrote my own reactive programming framework, CwlSignal, the main <code>Signal</code> instances were the delivery graph – the &ldquo;naïve&rdquo; interpretation was the same as the &ldquo;accurate&rdquo; interpretation. I handled the problem with multiple subscribers a different way: <code>Signal</code> nodes allowed only a single child to observe. For those specific cases where you need multiple listeners, CwlSignal offered a special <code>SignalMulti</code> node that encoded choices like &ldquo;multicast&rdquo;, &ldquo;continuous&rdquo; (cache latest), &ldquo;playback&rdquo; (cache all). But a re-subscribe option deliberately wasn&rsquo;t offered.</p> <p>In any case, let&rsquo;s look under the hood at the definition of <code>Subscription</code>:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">public</span> <span class="kd">protocol</span> <span class="nc">Subscription</span> <span class="p">:</span> <span class="n">Cancellable</span><span class="p">,</span> <span class="n">CustomCombineIdentifierConvertible</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span> <span class="nf">request</span><span class="p">(</span><span class="kc">_</span> <span class="n">demand</span><span class="p">:</span> <span class="n">Subscribers</span><span class="p">.</span><span class="n">Demand</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>It&rsquo;s pretty terse. If this is the entire definition of the shadow value sending graph, it&rsquo;s not revealing much.</p> <h2 id="custom-implementations">Custom implementations</h2> <p>Fortunately, <code>Subscription</code> isn&rsquo;t impossible to understand. It generally just performs all the roles of the <code>Publisher</code> and <code>Subscriber</code> in the &ldquo;naïve&rdquo; interpretation: it receives values, processes them and passes them down the line.</p> <p>A <code>Subscription</code> should replicate everything important from its associated <code>Publisher</code>, copying any closures and state from the initial values stored in the <code>Publisher</code>. In this way, the <code>Subscription</code> is independent and has everything needed to handle the processing, without further assistance from the <code>Publisher</code>.</p> <p>The trickiest part is working out when to create a <code>Subscriber</code> from a <code>Publisher</code> and getting everything to piece together. I arrived at the following steps, centered on <code>Publisher.receive</code>, after a little experimentation:</p> <blockquote> <p><strong>NOTE</strong>: the words <code>Subscriber</code> and <code>Subscription</code> are very similar. I&rsquo;m sure this is going to get confusing (it was confusing to write).</p> </blockquote> <ol> <li>You invoke Combine&rsquo;s <code>subscribe</code> function on your <code>Publisher</code>, passing your <code>Subscriber</code>.</li> <li>This will call through to your <code>Publisher</code>&rsquo;s <code>receive</code> function passing the <code>Subscriber</code> you provided to the <code>subscribe</code> function</li> <li>In the <code>receive</code> function <code>Publisher</code> creates a custom <code>Subscription</code> instance, which should also conform to <code>Subscriber</code> and should hold a reference to the downstream <code>Subscriber</code>.</li> <li>Your <code>Publisher</code> calls <code>subscribe</code> on its upstream <code>Publisher</code> (if any) passing the custom <code>Subscription</code> (this is why it should conform to <code>Subscriber</code>).</li> <li>The upstream <code>Publisher</code> calls <code>receive</code> on your custom <code>Subscription</code>, passing its own subscription instance.</li> <li>Your <code>Subscriber</code> should call <code>receive</code> on its downstream <code>Subscriber</code></li> <li>The downstream <code>Subscriber</code> will invoke <code>request</code> on your <code>Subscription</code> and your <code>Subscription</code> should invoke <code>request</code> on its upstream <code>Subscription</code>.</li> </ol> <p>The exact steps tend to vary based on whether your <code>Publisher</code> has an upstream <code>Publisher</code> or is a <code>Subject</code>.</p> <p>Let&rsquo;s focus on a transforming <code>Publisher</code> with an upstream <code>Publisher</code>, since that&rsquo;s the canonical case. Such a <code>Publisher</code> would have a <code>receive</code> function that looks like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">public</span> <span class="kd">func</span> <span class="nf">receive</span><span class="p">&lt;</span><span class="n">S</span><span class="p">&gt;(</span><span class="n">subscriber</span><span class="p">:</span> <span class="n">S</span><span class="p">)</span> <span class="k">where</span> <span class="n">S</span><span class="p">:</span> <span class="n">Subscriber</span><span class="p">,</span> <span class="n">Failure</span> <span class="p">==</span> <span class="n">S</span><span class="p">.</span><span class="n">Failure</span><span class="p">,</span> <span class="n">Output</span> <span class="p">==</span> <span class="n">S</span><span class="p">.</span><span class="n">Input</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">downstream</span> <span class="p">=</span> <span class="n">AnySubscriber</span><span class="p">(</span><span class="n">subscriber</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">behavior</span> <span class="p">=</span> <span class="n">CustomTransformer</span><span class="p">.</span><span class="n">Behavior</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">downstream</span><span class="p">:</span> <span class="n">downstream</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">processor</span><span class="p">:</span> <span class="n">processor</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">state</span><span class="p">:</span> <span class="n">initialState</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">subscription</span> <span class="p">=</span> <span class="n">CustomSubscription</span><span class="p">(</span><span class="n">behavior</span><span class="p">:</span> <span class="n">behavior</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">upstreamPublisher</span><span class="p">.</span><span class="n">subscribe</span><span class="p">(</span><span class="n">subscription</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>There&rsquo;s a very important trick here: even though we are a downstream node (and could implement <code>Subscriber</code> for ourselves), we don&rsquo;t pass ourselves to the <code>upstreamPublisher</code>. Instead we pass the newly constructed <code>Subscription</code> instance instead. This is why <code>Subscription</code> implementations are often <code>Subscriber</code> implementations too. The <code>Subscription</code> instances are their own, independent delivery graph, connected only to other <code>Subscription</code> instances.</p> <p>I chose to design my custom subscription in two parts: a wrapper (to apply mutex behaviors) and a behavior protocol (used to apply <code>Publisher</code> specific behaviors inside the mutex). The mutex wrapper is therefore implemented just once and the behavior content is simpler.</p> <p>Here&rsquo;s the wrapper interface:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">public</span> <span class="kd">struct</span> <span class="nc">CustomSubscription</span><span class="p">&lt;</span><span class="n">Content</span><span class="p">:</span> <span class="n">SubscriptionBehavior</span><span class="p">&gt;:</span> <span class="n">Subscriber</span><span class="p">,</span> <span class="n">Subscription</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">public</span> <span class="kd">typealias</span> <span class="n">Input</span> <span class="p">=</span> <span class="n">Content</span><span class="p">.</span><span class="n">Input</span> </span></span><span class="line"><span class="cl"> <span class="kd">public</span> <span class="kd">typealias</span> <span class="n">Failure</span> <span class="p">=</span> <span class="n">Content</span><span class="p">.</span><span class="n">Failure</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">public</span> <span class="kd">var</span> <span class="nv">combineIdentifier</span><span class="p">:</span> <span class="n">CombineIdentifier</span> <span class="p">{</span> <span class="k">return</span> <span class="n">content</span><span class="p">.</span><span class="n">combineIdentifier</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">recursiveMutex</span> <span class="p">=</span> <span class="n">NSRecursiveLock</span><span class="p">()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">content</span><span class="p">:</span> <span class="n">Content</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>and the <code>SubscriptionBehavior</code> inside it looks like this:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">public</span> <span class="kd">protocol</span> <span class="nc">SubscriptionBehavior</span><span class="p">:</span> <span class="kd">class</span><span class="p">,</span> <span class="n">Cancellable</span><span class="p">,</span> <span class="n">CustomCombineIdentifierConvertible</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="n">associatedtype</span> <span class="n">Input</span> </span></span><span class="line"><span class="cl"> <span class="n">associatedtype</span> <span class="n">Failure</span><span class="p">:</span> <span class="n">Error</span> </span></span><span class="line"><span class="cl"> <span class="n">associatedtype</span> <span class="n">Output</span> </span></span><span class="line"><span class="cl"> <span class="n">associatedtype</span> <span class="n">OutputFailure</span><span class="p">:</span> <span class="n">Error</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">demand</span><span class="p">:</span> <span class="n">Subscribers</span><span class="p">.</span><span class="n">Demand</span> <span class="p">{</span> <span class="kr">get</span> <span class="kr">set</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">upstream</span><span class="p">:</span> <span class="n">Subscription</span><span class="p">?</span> <span class="p">{</span> <span class="kr">get</span> <span class="kr">set</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">downstream</span><span class="p">:</span> <span class="n">AnySubscriber</span><span class="p">&lt;</span><span class="n">Output</span><span class="p">,</span> <span class="n">OutputFailure</span><span class="p">&gt;</span> <span class="p">{</span> <span class="kr">get</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="kd">func</span> <span class="nf">request</span><span class="p">(</span><span class="kc">_</span> <span class="n">d</span><span class="p">:</span> <span class="n">Subscribers</span><span class="p">.</span><span class="n">Demand</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span> <span class="nf">receive</span><span class="p">(</span><span class="kc">_</span> <span class="n">input</span><span class="p">:</span> <span class="n">Input</span><span class="p">)</span> <span class="p">-&gt;</span> <span class="n">Subscribers</span><span class="p">.</span><span class="n">Demand</span> </span></span><span class="line"><span class="cl"> <span class="kd">func</span> <span class="nf">receive</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="n">Subscribers</span><span class="p">.</span><span class="n">Completion</span><span class="p">&lt;</span><span class="n">Failure</span><span class="p">&gt;)</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><p>The implementations are then straightforward: values arrive via the <code>receive</code> functions and are processed as appropriate for the publisher that created the instance and emitted to the downstream <code>AnySubscriber</code>.</p> <p>You can see the full implementation of <code>CustomSubject</code>, <code>CustomScan</code>, <code>CustomSubscription</code> and <code>CustomSink</code> <a href="https://github.com/mattgallagher/CombineExploration">in the CombineExploration repository</a>.</p> <p>Is this how the implemention of <code>Subscription</code> looks in Combine? Almost certainly not. As far as I can tell, Combine uses a type called <code>Conduit</code> which applies its mutex once at the start, rather than once for every <code>Publisher</code> stage in the pipeline. <code>Conduit</code> does use a recursive mutex implementation (more on that in part 3 of this series) but it appears to be implemented on top of <code>os_unfair_lock</code> (which is usually a non-recursive mutex).</p> <p>However, these implementations do appear to behave correctly and interoperate correctly with the official Combine implementations.</p> <p>Here&rsquo;s the previous <code>testOverlappingABCD</code> rewritten with these implementations, showing that they function as drop-in replacements for the default implementations:</p> <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-swift" data-lang="swift"><span class="line"><span class="cl"><span class="kd">func</span> <span class="nf">testCustomABCD</span><span class="p">()</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">subjects</span> <span class="p">=</span> <span class="p">[</span><span class="n">CustomSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">deferred</span> <span class="p">=</span> <span class="n">Deferred</span> <span class="p">{</span> <span class="p">()</span> <span class="p">-&gt;</span> <span class="n">CustomSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;</span> <span class="k">in</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">request</span> <span class="p">=</span> <span class="n">CustomSubject</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;()</span> </span></span><span class="line"><span class="cl"> <span class="n">subjects</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">request</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">request</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">scanB</span> <span class="p">=</span> <span class="n">CustomScan</span><span class="p">(</span><span class="n">upstream</span><span class="p">:</span> <span class="n">deferred</span><span class="p">,</span> <span class="n">initialResult</span><span class="p">:</span> <span class="mi">10</span><span class="p">)</span> <span class="p">{</span> <span class="n">state</span><span class="p">,</span> <span class="n">next</span> <span class="k">in</span> <span class="n">state</span> <span class="o">+</span> <span class="n">next</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">receivedC</span> <span class="p">=</span> <span class="p">[</span><span class="n">Subscribers</span><span class="p">.</span><span class="n">Event</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">sinkC</span> <span class="p">=</span> <span class="n">CustomSink</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;(</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveCompletion</span><span class="p">:</span> <span class="p">{</span> <span class="n">receivedC</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">complete</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveValue</span><span class="p">:</span> <span class="p">{</span> <span class="n">receivedC</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">value</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="kd">var</span> <span class="nv">receivedD</span> <span class="p">=</span> <span class="p">[</span><span class="n">Subscribers</span><span class="p">.</span><span class="n">Event</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;]()</span> </span></span><span class="line"><span class="cl"> <span class="kd">let</span> <span class="nv">sinkD</span> <span class="p">=</span> <span class="n">CustomSink</span><span class="p">&lt;</span><span class="nb">Int</span><span class="p">,</span> <span class="n">Never</span><span class="p">&gt;(</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveCompletion</span><span class="p">:</span> <span class="p">{</span> <span class="n">receivedD</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">complete</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="n">receiveValue</span><span class="p">:</span> <span class="p">{</span> <span class="n">receivedD</span><span class="p">.</span><span class="n">append</span><span class="p">(.</span><span class="n">value</span><span class="p">(</span><span class="nv">$0</span><span class="p">))</span> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">scanB</span><span class="p">.</span><span class="n">subscribe</span><span class="p">(</span><span class="n">sinkC</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">subjects</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">send</span><span class="p">(</span><span class="n">sequence</span><span class="p">:</span> <span class="mf">1.</span><span class="p">..</span><span class="mi">2</span><span class="p">,</span> <span class="n">completion</span><span class="p">:</span> <span class="kc">nil</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">scanB</span><span class="p">.</span><span class="n">subscribe</span><span class="p">(</span><span class="n">sinkD</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">subjects</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">send</span><span class="p">(</span><span class="n">sequence</span><span class="p">:</span> <span class="mf">3.</span><span class="p">..</span><span class="mi">4</span><span class="p">,</span> <span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">subjects</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="n">send</span><span class="p">(</span><span class="n">sequence</span><span class="p">:</span> <span class="mf">1.</span><span class="p">..</span><span class="mi">4</span><span class="p">,</span> <span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">receivedC</span><span class="p">,</span> <span class="p">[</span><span class="mi">11</span><span class="p">,</span> <span class="mi">13</span><span class="p">,</span> <span class="mi">16</span><span class="p">,</span> <span class="mi">20</span><span class="p">].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">))</span> </span></span><span class="line"><span class="cl"> <span class="n">XCTAssertEqual</span><span class="p">(</span><span class="n">receivedD</span><span class="p">,</span> <span class="p">[</span><span class="mi">11</span><span class="p">,</span> <span class="mi">13</span><span class="p">,</span> <span class="mi">16</span><span class="p">,</span> <span class="mi">20</span><span class="p">].</span><span class="n">asEvents</span><span class="p">(</span><span class="n">completion</span><span class="p">:</span> <span class="p">.</span><span class="n">finished</span><span class="p">))</span> </span></span><span class="line"><span class="cl"><span class="p">}</span> </span></span></code></pre></div><h2 id="conclusion">Conclusion</h2> <blockquote> <p><strong>Download</strong>: The code for this series, <a href="https://github.com/mattgallagher/CombineExploration">CombineExploration, is available on github</a>.</p> </blockquote> <p>We frequently talk about our <code>Publisher</code> graphs as though they perform a calculation and emit values but this isn&rsquo;t really true. Values in Combine are sent by the <code>Subscription</code> graph and the calculation is repeated for each <code>Subscription</code> graph.</p> <p>The distinction between <code>Publisher</code> and <code>Subscription</code> graphs exists to prevent separate subscribers from interferring with each other. For this to work, <em>all</em> stream processing state you set up in a custom <code>Publisher</code> must be copied into a <code>Subscription</code> and mutated there, exclusively.</p> <h3 id="looking-forward">Looking forward&hellip;</h3> <p>In most cases, we don&rsquo;t want redundant calculations. Where possible, we want values calculated once per <code>Publisher</code> graph and we want the latest value <em>shared</em> between all subscribers.</p> <p>How do we avoid &ldquo;resubscription&rdquo; in Combine? How do we get multicast or cached results? Will we need to use <code>connect</code> or hold redundant <code>subscribe</code> cancellables as we do in RxSwift? For that matter, what is needed, in general, to keep Combine subscriptions alive? What are the rules by which Combine keeps anything (<code>Publishers</code>, <code>Subscribers</code> or <code>Subscriptions</code>) alive?</p> <p>This is what I&rsquo;ll look at in the next article: sharing.</p> <br/>Copyright Matt Gallagher, 2024. All rights reserved. Code samples may be use in accordance with the ISC-style license at https://www.cocoawithlove.com/about.html