<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh-CN"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://sunsetwan.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://sunsetwan.github.io/" rel="alternate" type="text/html" hreflang="zh-CN" /><updated>2026-07-16T01:30:50+08:00</updated><id>https://sunsetwan.github.io/feed.xml</id><title type="html">日落大道</title><subtitle>Sunset Wan 的 Swift、iOS 与软件设计技术博客。</subtitle><author><name>Sunset Wan</name></author><entry><title type="html">初识 The Composable Architecture（二）</title><link href="https://sunsetwan.github.io/2026/02/04/tca-101-p2/" rel="alternate" type="text/html" title="初识 The Composable Architecture（二）" /><published>2026-02-04T21:37:39+08:00</published><updated>2026-02-04T21:37:39+08:00</updated><id>https://sunsetwan.github.io/2026/02/04/tca-101-p2</id><content type="html" xml:base="https://sunsetwan.github.io/2026/02/04/tca-101-p2/"><![CDATA[<p>本文延续上一节对 TCA 的建模思路，聚焦「副作用」该如何在 reducer 中显式表达与测试。我们会把原本隐式的 I/O 操作抽象为 <code class="language-plaintext highlighter-rouge">Effect</code>，让 reducer 返回副作用，并进一步调整 <code class="language-plaintext highlighter-rouge">combine</code>/<code class="language-plaintext highlighter-rouge">pullback</code> 以支持一组 effect 的组合。通过 Favorite Primes 的保存/读取示例，展示从「纯函数」到「可追踪副作用」的演化过程。</p>

<!-- more -->

<h2 id="reducer-的副作用">Reducer 的副作用</h2>

<h3 id="同步副作用">同步副作用</h3>

<p>我们在 <code class="language-plaintext highlighter-rouge">FavoritePrimesAction</code> 中新增一个 action：<code class="language-plaintext highlighter-rouge">saveButtonTapped</code>。</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">enum</span> <span class="kt">FavoritePrimesAction</span> <span class="p">{</span>
  <span class="k">case</span> <span class="nf">deleteFavoritePrimes</span><span class="p">(</span><span class="kt">IndexSet</span><span class="p">)</span>
  <span class="k">case</span> <span class="nf">loadedFavoritePrimes</span><span class="p">([</span><span class="kt">Int</span><span class="p">])</span>
  <span class="k">case</span> <span class="n">saveButtonTapped</span> <span class="c1">// 新增 action</span>
<span class="p">}</span>

<span class="c1">// 在原本的 reducer 中新增对这个 action 的处理</span>
<span class="o">...</span>
<span class="k">case</span> <span class="o">.</span><span class="nv">saveButtonTapped</span><span class="p">:</span>
  <span class="k">let</span> <span class="nv">data</span> <span class="o">=</span> <span class="k">try!</span> <span class="kt">JSONEncoder</span><span class="p">()</span><span class="o">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">state</span><span class="p">)</span>
  <span class="k">let</span> <span class="nv">documentsPath</span> <span class="o">=</span> <span class="kt">NSSearchPathForDirectoriesInDomains</span><span class="p">(</span>
    <span class="o">.</span><span class="n">documentDirectory</span><span class="p">,</span> <span class="o">.</span><span class="n">userDomainMask</span><span class="p">,</span> <span class="kc">true</span>
  <span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
  <span class="k">let</span> <span class="nv">documentsUrl</span> <span class="o">=</span> <span class="kt">URL</span><span class="p">(</span><span class="nv">fileURLWithPath</span><span class="p">:</span> <span class="n">documentsPath</span><span class="p">)</span>
  <span class="k">try!</span> <span class="n">data</span><span class="o">.</span><span class="nf">write</span><span class="p">(</span>
    <span class="nv">to</span><span class="p">:</span> <span class="n">documentsUrl</span><span class="o">.</span><span class="nf">appendingPathComponent</span><span class="p">(</span><span class="s">"favorite-primes.json"</span><span class="p">)</span>
  <span class="p">)</span>
<span class="o">...</span>
</code></pre></div></div>

<p>可以发现：<code class="language-plaintext highlighter-rouge">favoritePrimesReducer</code> 已经不再是纯函数了。它不只是根据输入（<code class="language-plaintext highlighter-rouge">state</code> + <code class="language-plaintext highlighter-rouge">action</code>）计算输出，而是隐式依赖外部世界（<code class="language-plaintext highlighter-rouge">JSONEncoder</code> 等），并且产生隐式输出（写数据至硬盘），这些都没有体现在函数签名里。换句话说，这个 reducer 在收到 action <code class="language-plaintext highlighter-rouge">saveButtonTapped</code> 时执行了副作用：把 <code class="language-plaintext highlighter-rouge">favoritePrimes</code> 数组写入硬盘。当你第一眼看到这个 reducer 时，很难看出它除了更新 <code class="language-plaintext highlighter-rouge">state</code> 之外还会写数据到硬盘。为这个 reducer 写测试会受外部状态影响（比如写入前磁盘已满），导致测试不稳定且成本高。</p>

<p>在 reducer 里直接执行副作用，会让函数有隐藏输入/输出，破坏可理解性与可测试性。</p>

<p>我们看一个简单的例子：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">compute</span><span class="p">(</span><span class="n">_</span> <span class="nv">x</span><span class="p">:</span> <span class="kt">Int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Int</span> <span class="p">{</span>
  <span class="k">let</span> <span class="nv">computation</span> <span class="o">=</span> <span class="n">x</span> <span class="o">*</span> <span class="n">x</span> <span class="o">+</span> <span class="mi">1</span>
  <span class="nf">print</span><span class="p">(</span><span class="s">"Computed </span><span class="se">\(</span><span class="n">computation</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
  <span class="k">return</span> <span class="n">computation</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">compute</code> 函数在计算完成后会返回结果。这样看来，它似乎满足纯函数的定义，但它的副作用是打印出计算结果，这种行为不可测试、也没体现在函数签名里。我们可以让 <code class="language-plaintext highlighter-rouge">compute</code> 函数返回「需要打印的内容」，由调用者决定是否真的打印，这样副作用就变成了显式输出。</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">computeAndPrint</span><span class="p">(</span><span class="n">_</span> <span class="nv">x</span><span class="p">:</span> <span class="kt">Int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="p">(</span><span class="kt">Int</span><span class="p">,</span> <span class="p">[</span><span class="kt">String</span><span class="p">])</span> <span class="p">{</span>
  <span class="k">let</span> <span class="nv">computation</span> <span class="o">=</span> <span class="n">x</span> <span class="o">*</span> <span class="n">x</span> <span class="o">+</span> <span class="mi">1</span>
  <span class="nf">return</span> <span class="p">(</span><span class="n">computation</span><span class="p">,</span> <span class="p">[</span><span class="s">"Computed </span><span class="se">\(</span><span class="n">computation</span><span class="se">)</span><span class="s">"</span><span class="p">])</span>
<span class="p">}</span>
</code></pre></div></div>

<p>回看 <code class="language-plaintext highlighter-rouge">Reducer</code> 的函数签名：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">typealias</span> <span class="kt">Reducer</span><span class="o">&lt;</span><span class="kt">Value</span><span class="p">,</span> <span class="kt">Action</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">(</span><span class="k">inout</span> <span class="kt">Value</span><span class="p">,</span> <span class="kt">Action</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span>
</code></pre></div></div>

<p>我们需要定义一个结构体 <code class="language-plaintext highlighter-rouge">Effect</code>，代表 reducer 所执行的副作用，并显式返回这个副作用。</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">typealias</span> <span class="kt">Effect</span> <span class="o">=</span> <span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">Void</span>
<span class="kd">public</span> <span class="kd">typealias</span> <span class="kt">Reducer</span><span class="o">&lt;</span><span class="kt">Value</span><span class="p">,</span> <span class="kt">Action</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">(</span><span class="k">inout</span> <span class="kt">Value</span><span class="p">,</span> <span class="kt">Action</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Effect</span>
</code></pre></div></div>

<p>更新 <code class="language-plaintext highlighter-rouge">favoritePrimesReducer</code>，这样写入硬盘的副作用就被显式返回。</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">case</span> <span class="o">.</span><span class="nv">saveButtonTapped</span><span class="p">:</span>
  <span class="k">let</span> <span class="nv">state</span> <span class="o">=</span> <span class="n">state</span>
  <span class="k">return</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">data</span> <span class="o">=</span> <span class="k">try!</span> <span class="kt">JSONEncoder</span><span class="p">()</span><span class="o">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">state</span><span class="p">)</span>
    <span class="k">let</span> <span class="nv">documentsPath</span> <span class="o">=</span> <span class="kt">NSSearchPathForDirectoriesInDomains</span><span class="p">(</span>
      <span class="o">.</span><span class="n">documentDirectory</span><span class="p">,</span> <span class="o">.</span><span class="n">userDomainMask</span><span class="p">,</span> <span class="kc">true</span>
    <span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
    <span class="k">let</span> <span class="nv">documentsUrl</span> <span class="o">=</span> <span class="kt">URL</span><span class="p">(</span><span class="nv">fileURLWithPath</span><span class="p">:</span> <span class="n">documentsPath</span><span class="p">)</span>
    <span class="k">try!</span> <span class="n">data</span><span class="o">.</span><span class="nf">write</span><span class="p">(</span>
      <span class="nv">to</span><span class="p">:</span> <span class="n">documentsUrl</span><span class="o">.</span><span class="nf">appendingPathComponent</span><span class="p">(</span><span class="s">"favorite-primes.json"</span><span class="p">)</span>
    <span class="p">)</span>
  <span class="p">}</span>
</code></pre></div></div>

<p>如果我们往 <code class="language-plaintext highlighter-rouge">FavoritePrimesAction</code> 里新增一个 action <code class="language-plaintext highlighter-rouge">loadButtonTapped</code> 呢？在点击 Load 按钮后，从硬盘读取保存的 <code class="language-plaintext highlighter-rouge">favoritePrimes</code> 数组，再通过 action <code class="language-plaintext highlighter-rouge">loadedFavoritePrimes</code> 把结果发送回 <code class="language-plaintext highlighter-rouge">store</code> 中：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">enum</span> <span class="kt">FavoritePrimesAction</span> <span class="p">{</span>
  <span class="k">case</span> <span class="nf">deleteFavoritePrimes</span><span class="p">(</span><span class="kt">IndexSet</span><span class="p">)</span>
  <span class="k">case</span> <span class="n">loadButtonTapped</span>
  <span class="k">case</span> <span class="nf">loadedFavoritePrimes</span><span class="p">([</span><span class="kt">Int</span><span class="p">])</span>
  <span class="k">case</span> <span class="n">saveButtonTapped</span>
<span class="p">}</span>

<span class="o">...</span>
<span class="k">case</span> <span class="o">.</span><span class="nv">loadButtonTapped</span><span class="p">:</span>
  <span class="k">return</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">documentsPath</span> <span class="o">=</span> <span class="kt">NSSearchPathForDirectoriesInDomains</span><span class="p">(</span>
      <span class="o">.</span><span class="n">documentDirectory</span><span class="p">,</span> <span class="o">.</span><span class="n">userDomainMask</span><span class="p">,</span> <span class="kc">true</span>
    <span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
    <span class="k">let</span> <span class="nv">documentsUrl</span> <span class="o">=</span> <span class="kt">URL</span><span class="p">(</span><span class="nv">fileURLWithPath</span><span class="p">:</span> <span class="n">documentsPath</span><span class="p">)</span>
    <span class="k">let</span> <span class="nv">favoritePrimesUrl</span> <span class="o">=</span> <span class="n">documentsUrl</span>
      <span class="o">.</span><span class="nf">appendingPathComponent</span><span class="p">(</span><span class="s">"favorite-primes.json"</span><span class="p">)</span>
    <span class="k">guard</span>
      <span class="k">let</span> <span class="nv">data</span> <span class="o">=</span> <span class="k">try</span><span class="p">?</span> <span class="kt">Data</span><span class="p">(</span><span class="nv">contentsOf</span><span class="p">:</span> <span class="n">favoritePrimesUrl</span><span class="p">),</span>
      <span class="k">let</span> <span class="nv">favoritePrimes</span> <span class="o">=</span> <span class="k">try</span><span class="p">?</span> <span class="kt">JSONDecoder</span><span class="p">()</span>
        <span class="o">.</span><span class="nf">decode</span><span class="p">([</span><span class="kt">Int</span><span class="p">]</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">from</span><span class="p">:</span> <span class="n">data</span><span class="p">)</span>
      <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
    <span class="k">self</span><span class="o">.</span><span class="n">store</span><span class="o">.</span><span class="nf">send</span><span class="p">(</span><span class="o">.</span><span class="nf">loadedFavoritePrimes</span><span class="p">(</span><span class="n">favoritePrimes</span><span class="p">))</span> <span class="c1">// Error: Use of unresolved identifier 'self'</span>
  <span class="p">}</span>
</code></pre></div></div>

<p>我们发现一个问题：reducer 里没有 <code class="language-plaintext highlighter-rouge">self</code> 或 <code class="language-plaintext highlighter-rouge">store</code>，副作用完成后无法把结果送回去更新状态。在视图里时，副作用可以直接 <code class="language-plaintext highlighter-rouge">send</code> action；但在 reducer 里，<code class="language-plaintext highlighter-rouge">Effect</code> 只是 <code class="language-plaintext highlighter-rouge">() -&gt; Void</code>，只能「做完事就消失」，结果无法影响后续 <code class="language-plaintext highlighter-rouge">state</code>。因此需要调整模型：让 <code class="language-plaintext highlighter-rouge">Effect</code> 返回一个 <code class="language-plaintext highlighter-rouge">Action</code>，把副作用的结果封装成 action，再交给 reducer 处理。这样 effect 只负责最小工作量，状态更新仍由 reducer 完成，更清晰也更可测试。</p>

<p>并不是所有 effect 都需要回传数据。例如把 <code class="language-plaintext highlighter-rouge">favoritePrimes</code> 写入磁盘这种「一次性」的操作，并没有新的 action 要交给系统。因此更合理的做法是让 effect 返回一个可选的 action：需要回传时返回具体 action，不需要时返回 <code class="language-plaintext highlighter-rouge">nil</code>，由 <code class="language-plaintext highlighter-rouge">store</code> 忽略即可：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">typealias</span> <span class="kt">Effect</span><span class="o">&lt;</span><span class="kt">Action</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">Action</span><span class="p">?</span>
<span class="kd">public</span> <span class="kd">typealias</span> <span class="kt">Reducer</span><span class="o">&lt;</span><span class="kt">Value</span><span class="p">,</span> <span class="kt">Action</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">(</span><span class="k">inout</span> <span class="kt">Value</span><span class="p">,</span> <span class="kt">Action</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Effect</span><span class="o">&lt;</span><span class="kt">Action</span><span class="o">&gt;</span>
</code></pre></div></div>

<p>回看 <code class="language-plaintext highlighter-rouge">combine</code> 函数：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">func</span> <span class="n">combine</span><span class="o">&lt;</span><span class="kt">Value</span><span class="p">,</span> <span class="kt">Action</span><span class="o">&gt;</span><span class="p">(</span>
  <span class="n">_</span> <span class="nv">reducers</span><span class="p">:</span> <span class="kt">Reducer</span><span class="o">&lt;</span><span class="kt">Value</span><span class="p">,</span> <span class="kt">Action</span><span class="o">&gt;...</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Reducer</span><span class="o">&lt;</span><span class="kt">Value</span><span class="p">,</span> <span class="kt">Action</span><span class="o">&gt;</span> <span class="p">{</span>
  <span class="k">return</span> <span class="p">{</span> <span class="n">value</span><span class="p">,</span> <span class="n">action</span> <span class="k">in</span>
    <span class="k">let</span> <span class="nv">effects</span> <span class="o">=</span> <span class="n">reducers</span><span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="nv">$0</span><span class="p">(</span><span class="o">&amp;</span><span class="n">value</span><span class="p">,</span> <span class="n">action</span><span class="p">)</span> <span class="p">}</span>
    <span class="k">return</span> <span class="p">{</span>
      <span class="k">for</span> <span class="n">effect</span> <span class="k">in</span> <span class="n">effects</span> <span class="p">{</span>
        <span class="nf">effect</span><span class="p">()</span> <span class="c1">// Warning: Result of call to function returning ‘Action?’ is unused</span>
      <span class="p">}</span>
    <span class="p">}</span>
    <span class="o">...</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>假设这里是要组合三个 reducer，那么 <code class="language-plaintext highlighter-rouge">combine</code> 后得出的新 reducer 在收到一个 action 后，至多会产生三个副作用，这就无法只返回一个副作用。因此更合理的方案是让 reducer 返回多个 effect，这样组合 reducer 时就能把所有副作用完整保留：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">typealias</span> <span class="kt">Reducer</span><span class="o">&lt;</span><span class="kt">Value</span><span class="p">,</span> <span class="kt">Action</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">(</span><span class="k">inout</span> <span class="kt">Value</span><span class="p">,</span> <span class="kt">Action</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="p">[</span><span class="kt">Effect</span><span class="o">&lt;</span><span class="kt">Action</span><span class="o">&gt;</span><span class="p">]</span>
</code></pre></div></div>

<p>再看看 <code class="language-plaintext highlighter-rouge">pullback</code> 函数应如何更新：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">func</span> <span class="n">pullback</span><span class="o">&lt;</span><span class="kt">LocalValue</span><span class="p">,</span> <span class="kt">GlobalValue</span><span class="p">,</span> <span class="kt">LocalAction</span><span class="p">,</span> <span class="kt">GlobalAction</span><span class="o">&gt;</span><span class="p">(</span>
  <span class="n">_</span> <span class="nv">reducer</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="kt">Reducer</span><span class="o">&lt;</span><span class="kt">LocalValue</span><span class="p">,</span> <span class="kt">LocalAction</span><span class="o">&gt;</span><span class="p">,</span>
  <span class="nv">value</span><span class="p">:</span> <span class="kt">WritableKeyPath</span><span class="o">&lt;</span><span class="kt">GlobalValue</span><span class="p">,</span> <span class="kt">LocalValue</span><span class="o">&gt;</span><span class="p">,</span> <span class="c1">// GlobalValue -&gt; LocalValue</span>
  <span class="nv">action</span><span class="p">:</span> <span class="kt">WritableKeyPath</span><span class="o">&lt;</span><span class="kt">GlobalAction</span><span class="p">,</span> <span class="kt">LocalAction</span><span class="p">?</span><span class="o">&gt;</span> <span class="c1">// GlobalAction -&gt; LocalAction?</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Reducer</span><span class="o">&lt;</span><span class="kt">GlobalValue</span><span class="p">,</span> <span class="kt">GlobalAction</span><span class="o">&gt;</span> <span class="p">{</span>
  <span class="k">return</span> <span class="p">{</span> <span class="n">globalValue</span><span class="p">,</span> <span class="n">globalAction</span> <span class="k">in</span>
    <span class="k">guard</span> <span class="k">let</span> <span class="nv">localAction</span> <span class="o">=</span> <span class="n">globalAction</span><span class="p">[</span><span class="nv">keyPath</span><span class="p">:</span> <span class="n">action</span><span class="p">]</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">[]</span> <span class="p">}</span>
    <span class="k">let</span> <span class="nv">localEffects</span> <span class="o">=</span> <span class="nf">reducer</span><span class="p">(</span><span class="o">&amp;</span><span class="n">globalValue</span><span class="p">[</span><span class="nv">keyPath</span><span class="p">:</span> <span class="n">value</span><span class="p">],</span> <span class="n">localAction</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">localEffects</span><span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">localEffect</span> <span class="k">in</span>
      <span class="k">return</span> <span class="p">{</span>
        <span class="k">guard</span> <span class="k">let</span> <span class="nv">localAction</span> <span class="o">=</span> <span class="nf">localEffect</span><span class="p">()</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">nil</span> <span class="p">}</span>
        <span class="k">var</span> <span class="nv">globalAction</span> <span class="o">=</span> <span class="n">globalAction</span>
        <span class="n">globalAction</span><span class="p">[</span><span class="nv">keyPath</span><span class="p">:</span> <span class="n">action</span><span class="p">]</span> <span class="o">=</span> <span class="n">localAction</span>
        <span class="k">return</span> <span class="n">globalAction</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">pullback</code> 函数接受一个局部 reducer 和两个 <code class="language-plaintext highlighter-rouge">WritableKeyPath</code>，返回一个全局 reducer。而返回的全局 reducer 需要返回一个副作用数组（<code class="language-plaintext highlighter-rouge">[Effect&lt;GlobalAction&gt;]</code>）。目标是返回 <code class="language-plaintext highlighter-rouge">[Effect&lt;GlobalAction&gt;]</code>，我们拥有的信息是 <code class="language-plaintext highlighter-rouge">[Effect&lt;LocalAction&gt;]</code> 和 <code class="language-plaintext highlighter-rouge">WritableKeyPath&lt;GlobalAction, LocalAction?&gt;</code>。不难想到：利用该 <code class="language-plaintext highlighter-rouge">WritableKeyPath</code> 和 <code class="language-plaintext highlighter-rouge">localEffect()</code> 得出的 <code class="language-plaintext highlighter-rouge">localAction</code> 来计算出 <code class="language-plaintext highlighter-rouge">globalEffect</code> 所需的 <code class="language-plaintext highlighter-rouge">globalAction</code>。</p>

<p><code class="language-plaintext highlighter-rouge">pullback</code> 的实现虽然代码不少，但逻辑其实很清晰：它把「处理局部 state / action 的 reducer」提升为「处理全局 state / action 的 reducer」。之所以能做到这一点，是因为我们用 enum property 生成了 WritableKeyPath，可以从全局取出局部、再把局部改动写回全局。具体流程是：当全局 <code class="language-plaintext highlighter-rouge">state</code> 和 <code class="language-plaintext highlighter-rouge">action</code> 进入时，先用 WritableKeyPath 提取出局部 <code class="language-plaintext highlighter-rouge">state</code> / <code class="language-plaintext highlighter-rouge">action</code>，交给局部 reducer 处理，然后把更新后的局部 <code class="language-plaintext highlighter-rouge">state</code> 写回全局 <code class="language-plaintext highlighter-rouge">state</code>。</p>

<p>同时，局部 reducer 会产生一组局部 effect，这些 effect 可能会返回局部 action。我们再用 key path 把局部 action 嵌回全局 action，从而把局部 effect 变成全局 effect，并对数组逐个转换完成整个过程。</p>

<p>更新 <code class="language-plaintext highlighter-rouge">favoritePrimesReducer</code>：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">enum</span> <span class="kt">FavoritePrimesAction</span> <span class="p">{</span>
  <span class="k">case</span> <span class="nf">deleteFavoritePrimes</span><span class="p">(</span><span class="kt">IndexSet</span><span class="p">)</span>
  <span class="k">case</span> <span class="n">loadButtonTapped</span>
  <span class="k">case</span> <span class="nf">loadedFavoritePrimes</span><span class="p">([</span><span class="kt">Int</span><span class="p">])</span>
  <span class="k">case</span> <span class="n">saveButtonTapped</span>
<span class="p">}</span>

<span class="kd">public</span> <span class="kd">func</span> <span class="nf">favoritePrimesReducer</span><span class="p">(</span><span class="nv">state</span><span class="p">:</span> <span class="k">inout</span> <span class="p">[</span><span class="kt">Int</span><span class="p">],</span> <span class="nv">action</span><span class="p">:</span> <span class="kt">FavoritePrimesAction</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="p">[</span><span class="kt">Effect</span><span class="o">&lt;</span><span class="kt">FavoritePrimesAction</span><span class="o">&gt;</span><span class="p">]</span> <span class="p">{</span>
  <span class="k">switch</span> <span class="n">action</span> <span class="p">{</span>
  <span class="k">case</span> <span class="kd">let</span> <span class="o">.</span><span class="nf">deleteFavoritePrimes</span><span class="p">(</span><span class="n">indexSet</span><span class="p">):</span>
    <span class="k">for</span> <span class="n">index</span> <span class="k">in</span> <span class="n">indexSet</span> <span class="p">{</span>
      <span class="n">state</span><span class="o">.</span><span class="nf">remove</span><span class="p">(</span><span class="nv">at</span><span class="p">:</span> <span class="n">index</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="p">[]</span>

  <span class="k">case</span> <span class="kd">let</span> <span class="o">.</span><span class="nf">loadedFavoritePrimes</span><span class="p">(</span><span class="n">favoritePrimes</span><span class="p">):</span>
    <span class="n">state</span> <span class="o">=</span> <span class="n">favoritePrimes</span>
    <span class="k">return</span> <span class="p">[]</span>

  <span class="k">case</span> <span class="o">.</span><span class="nv">saveButtonTapped</span><span class="p">:</span>
    <span class="k">return</span> <span class="p">[</span><span class="nf">saveEffect</span><span class="p">(</span><span class="nv">favoritePrimes</span><span class="p">:</span> <span class="n">state</span><span class="p">)]</span>

  <span class="k">case</span> <span class="o">.</span><span class="nv">loadButtonTapped</span><span class="p">:</span>
    <span class="k">return</span> <span class="p">[</span><span class="n">loadEffect</span><span class="p">]</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="kd">private</span> <span class="kd">func</span> <span class="nf">saveEffect</span><span class="p">(</span><span class="nv">favoritePrimes</span><span class="p">:</span> <span class="p">[</span><span class="kt">Int</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="kt">Effect</span><span class="o">&lt;</span><span class="kt">FavoritePrimesAction</span><span class="o">&gt;</span> <span class="p">{</span>
  <span class="k">return</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">data</span> <span class="o">=</span> <span class="k">try!</span> <span class="kt">JSONEncoder</span><span class="p">()</span><span class="o">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">favoritePrimes</span><span class="p">)</span>
    <span class="k">let</span> <span class="nv">documentsPath</span> <span class="o">=</span> <span class="kt">NSSearchPathForDirectoriesInDomains</span><span class="p">(</span>
      <span class="o">.</span><span class="n">documentDirectory</span><span class="p">,</span> <span class="o">.</span><span class="n">userDomainMask</span><span class="p">,</span> <span class="kc">true</span>
    <span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
    <span class="k">let</span> <span class="nv">documentsUrl</span> <span class="o">=</span> <span class="kt">URL</span><span class="p">(</span><span class="nv">fileURLWithPath</span><span class="p">:</span> <span class="n">documentsPath</span><span class="p">)</span>
    <span class="k">let</span> <span class="nv">favoritePrimesUrl</span> <span class="o">=</span> <span class="n">documentsUrl</span>
      <span class="o">.</span><span class="nf">appendingPathComponent</span><span class="p">(</span><span class="s">"favorite-primes.json"</span><span class="p">)</span>
    <span class="k">try!</span> <span class="n">data</span><span class="o">.</span><span class="nf">write</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="n">favoritePrimesUrl</span><span class="p">)</span>
    <span class="k">return</span> <span class="kc">nil</span> <span class="c1">// 不需要回传数据给 store，直接返回 nil</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="kd">private</span> <span class="k">let</span> <span class="nv">loadEffect</span><span class="p">:</span> <span class="kt">Effect</span><span class="o">&lt;</span><span class="kt">FavoritePrimesAction</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">{</span>
  <span class="k">let</span> <span class="nv">documentsPath</span> <span class="o">=</span> <span class="kt">NSSearchPathForDirectoriesInDomains</span><span class="p">(</span>
    <span class="o">.</span><span class="n">documentDirectory</span><span class="p">,</span> <span class="o">.</span><span class="n">userDomainMask</span><span class="p">,</span> <span class="kc">true</span>
  <span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
  <span class="k">let</span> <span class="nv">documentsUrl</span> <span class="o">=</span> <span class="kt">URL</span><span class="p">(</span><span class="nv">fileURLWithPath</span><span class="p">:</span> <span class="n">documentsPath</span><span class="p">)</span>
  <span class="k">let</span> <span class="nv">favoritePrimesUrl</span> <span class="o">=</span> <span class="n">documentsUrl</span>
    <span class="o">.</span><span class="nf">appendingPathComponent</span><span class="p">(</span><span class="s">"favorite-primes.json"</span><span class="p">)</span>
  <span class="k">guard</span>
    <span class="k">let</span> <span class="nv">data</span> <span class="o">=</span> <span class="k">try</span><span class="p">?</span> <span class="kt">Data</span><span class="p">(</span><span class="nv">contentsOf</span><span class="p">:</span> <span class="n">favoritePrimesUrl</span><span class="p">),</span>
    <span class="k">let</span> <span class="nv">favoritePrimes</span> <span class="o">=</span> <span class="k">try</span><span class="p">?</span> <span class="kt">JSONDecoder</span><span class="p">()</span><span class="o">.</span><span class="nf">decode</span><span class="p">([</span><span class="kt">Int</span><span class="p">]</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">from</span><span class="p">:</span> <span class="n">data</span><span class="p">)</span>
    <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">nil</span> <span class="p">}</span>
  <span class="k">return</span> <span class="o">.</span><span class="nf">loadedFavoritePrimes</span><span class="p">(</span><span class="n">favoritePrimes</span><span class="p">)</span> <span class="c1">// 需要回传数据 favoritePrimes 数组</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="单向数据流">单向数据流</h3>

<p>从上文的 <code class="language-plaintext highlighter-rouge">loadButtonTapped</code> 可以看出，导致状态变化的过程为：先运行 <code class="language-plaintext highlighter-rouge">reducer</code>、收集 <code class="language-plaintext highlighter-rouge">effect</code>，再执行 <code class="language-plaintext highlighter-rouge">effect</code>，把产出的 <code class="language-plaintext highlighter-rouge">action</code> 重新送回 <code class="language-plaintext highlighter-rouge">reducer</code>。这就是所谓的单向数据流：状态只能通过 <code class="language-plaintext highlighter-rouge">action</code> 进入 <code class="language-plaintext highlighter-rouge">reducer</code> 来改变。如果副作用想影响状态，也必须先生成新的 <code class="language-plaintext highlighter-rouge">action</code>，再由 <code class="language-plaintext highlighter-rouge">reducer</code> 统一处理。这种模式的优点是非常可理解——状态的变化只有一个入口；代价是需要额外的 <code class="language-plaintext highlighter-rouge">action</code> 来回传副作用结果。</p>

<h3 id="异步副作用">异步副作用</h3>

<p>如果想要 <code class="language-plaintext highlighter-rouge">Effect&lt;Action&gt;</code> 支持异步运行，我们还得修改它的函数签名。可以对比 <code class="language-plaintext highlighter-rouge">URLSession</code> 的 API 设计，从中汲取灵感：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">dataTask</span><span class="p">(</span>
  <span class="n">with</span> <span class="nv">url</span><span class="p">:</span> <span class="kt">URL</span><span class="p">,</span>
  <span class="nv">completionHandler</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="kt">Data</span><span class="p">?,</span> <span class="kt">URLResponse</span><span class="p">?,</span> <span class="p">(</span><span class="kd">any</span> <span class="kt">Error</span><span class="p">)?)</span> <span class="o">-&gt;</span> <span class="kt">Void</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">URLSessionDataTask</span>

<span class="kd">public</span> <span class="kd">typealias</span> <span class="kt">Effect</span><span class="o">&lt;</span><span class="kt">Action</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">Action</span><span class="p">?</span> <span class="c1">// 旧</span>

<span class="kd">public</span> <span class="kd">typealias</span> <span class="kt">Effect</span><span class="o">&lt;</span><span class="kt">Action</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">(</span><span class="kd">@escaping</span> <span class="p">(</span><span class="kt">Action</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span> <span class="c1">// 新</span>
</code></pre></div></div>

<p>在异步方法中持有这个闭包 <code class="language-plaintext highlighter-rouge">(@escaping (Action) -&gt; Void) -&gt; Void</code>，组装好 action 时，通过调用这个闭包把 action 传递出去。</p>

<p>再看看 <code class="language-plaintext highlighter-rouge">pullback</code> 函数应如何更新：</p>

<p>它需要把局部 effect 转换成全局 effect：做法是先执行局部 effect 拿到局部 action，再用回调把这个局部 action 嵌入到全局 action 中，从而得到可在全局环境里执行的 effect。</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">func</span> <span class="n">pullback</span><span class="o">&lt;</span><span class="kt">LocalValue</span><span class="p">,</span> <span class="kt">GlobalValue</span><span class="p">,</span> <span class="kt">LocalAction</span><span class="p">,</span> <span class="kt">GlobalAction</span><span class="o">&gt;</span><span class="p">(</span>
  <span class="n">_</span> <span class="nv">reducer</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="kt">Reducer</span><span class="o">&lt;</span><span class="kt">LocalValue</span><span class="p">,</span> <span class="kt">LocalAction</span><span class="o">&gt;</span><span class="p">,</span>
  <span class="nv">value</span><span class="p">:</span> <span class="kt">WritableKeyPath</span><span class="o">&lt;</span><span class="kt">GlobalValue</span><span class="p">,</span> <span class="kt">LocalValue</span><span class="o">&gt;</span><span class="p">,</span>
  <span class="nv">action</span><span class="p">:</span> <span class="kt">WritableKeyPath</span><span class="o">&lt;</span><span class="kt">GlobalAction</span><span class="p">,</span> <span class="kt">LocalAction</span><span class="p">?</span><span class="o">&gt;</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Reducer</span><span class="o">&lt;</span><span class="kt">GlobalValue</span><span class="p">,</span> <span class="kt">GlobalAction</span><span class="o">&gt;</span> <span class="p">{</span>
  <span class="k">return</span> <span class="p">{</span> <span class="n">globalValue</span><span class="p">,</span> <span class="n">globalAction</span> <span class="k">in</span>
    <span class="k">guard</span> <span class="k">let</span> <span class="nv">localAction</span> <span class="o">=</span> <span class="n">globalAction</span><span class="p">[</span><span class="nv">keyPath</span><span class="p">:</span> <span class="n">action</span><span class="p">]</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">[]</span> <span class="p">}</span>
    <span class="k">let</span> <span class="nv">localEffects</span> <span class="o">=</span> <span class="nf">reducer</span><span class="p">(</span><span class="o">&amp;</span><span class="n">globalValue</span><span class="p">[</span><span class="nv">keyPath</span><span class="p">:</span> <span class="n">value</span><span class="p">],</span> <span class="n">localAction</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">localEffects</span><span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">localEffect</span> <span class="k">in</span>
      <span class="p">{</span> <span class="n">callback</span> <span class="k">in</span>
        <span class="n">localEffect</span> <span class="p">{</span> <span class="n">localAction</span> <span class="k">in</span>
          <span class="k">var</span> <span class="nv">globalAction</span> <span class="o">=</span> <span class="n">globalAction</span>
          <span class="n">globalAction</span><span class="p">[</span><span class="nv">keyPath</span><span class="p">:</span> <span class="n">action</span><span class="p">]</span> <span class="o">=</span> <span class="n">localAction</span>
          <span class="nf">callback</span><span class="p">(</span><span class="n">globalAction</span><span class="p">)</span>
        <span class="p">}</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h4 id="使用-combine-优化">使用 Combine 优化</h4>

<p>能不能把 <code class="language-plaintext highlighter-rouge">Effect</code> 变成一个 <code class="language-plaintext highlighter-rouge">Publisher</code>，利用 Combine 来实现异步副作用呢？答案是可以的！</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">struct</span> <span class="kt">Effect</span><span class="o">&lt;</span><span class="kt">Output</span><span class="o">&gt;</span><span class="p">:</span> <span class="kt">Publisher</span> <span class="p">{</span>
  <span class="kd">public</span> <span class="kd">typealias</span> <span class="kt">Failure</span> <span class="o">=</span> <span class="kt">Never</span>

  <span class="k">let</span> <span class="nv">publisher</span><span class="p">:</span> <span class="kt">AnyPublisher</span><span class="o">&lt;</span><span class="kt">Output</span><span class="p">,</span> <span class="kt">Failure</span><span class="o">&gt;</span>

  <span class="kd">public</span> <span class="kd">func</span> <span class="n">receive</span><span class="o">&lt;</span><span class="kt">S</span><span class="o">&gt;</span><span class="p">(</span>
    <span class="nv">subscriber</span><span class="p">:</span> <span class="kt">S</span>
  <span class="p">)</span> <span class="k">where</span> <span class="kt">S</span><span class="p">:</span> <span class="kt">Subscriber</span><span class="p">,</span> <span class="kt">Failure</span> <span class="o">==</span> <span class="kt">S</span><span class="o">.</span><span class="kt">Failure</span><span class="p">,</span> <span class="kt">Output</span> <span class="o">==</span> <span class="kt">S</span><span class="o">.</span><span class="kt">Input</span> <span class="p">{</span>
    <span class="k">self</span><span class="o">.</span><span class="n">publisher</span><span class="o">.</span><span class="nf">receive</span><span class="p">(</span><span class="nv">subscriber</span><span class="p">:</span> <span class="n">subscriber</span><span class="p">)</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>我们接着更新 Store 中的 <code class="language-plaintext highlighter-rouge">send</code> 方法：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="k">var</span> <span class="nv">effectCancellables</span><span class="p">:</span> <span class="kt">Set</span><span class="o">&lt;</span><span class="kt">AnyCancellable</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">[]</span>
<span class="o">...</span>

<span class="kd">public</span> <span class="kd">func</span> <span class="nf">send</span><span class="p">(</span><span class="n">_</span> <span class="nv">action</span><span class="p">:</span> <span class="kt">Action</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">let</span> <span class="nv">effects</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="nf">reducer</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="o">.</span><span class="n">value</span><span class="p">,</span> <span class="n">action</span><span class="p">)</span>
  <span class="n">effects</span><span class="o">.</span><span class="n">forEach</span> <span class="p">{</span> <span class="n">effect</span> <span class="k">in</span>
    <span class="k">var</span> <span class="nv">effectCancellable</span><span class="p">:</span> <span class="kt">AnyCancellable</span><span class="o">!</span>
    <span class="n">effectCancellable</span> <span class="o">=</span> <span class="n">effect</span><span class="o">.</span><span class="nf">sink</span><span class="p">(</span>
      <span class="nv">receiveCompletion</span><span class="p">:</span> <span class="p">{</span> <span class="p">[</span><span class="k">weak</span> <span class="k">self</span><span class="p">]</span> <span class="n">_</span> <span class="k">in</span>
        <span class="k">self</span><span class="p">?</span><span class="o">.</span><span class="n">effectCancellables</span><span class="o">.</span><span class="nf">remove</span><span class="p">(</span><span class="n">effectCancellable</span><span class="p">)</span>
      <span class="p">},</span>
      <span class="nv">receiveValue</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">send</span>
    <span class="p">)</span>
    <span class="k">self</span><span class="o">.</span><span class="n">effectCancellables</span><span class="o">.</span><span class="nf">insert</span><span class="p">(</span><span class="n">effectCancellable</span><span class="p">)</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<ol>
  <li>当 effect 收到结果时，继续调用 <code class="language-plaintext highlighter-rouge">self.send</code>，把副作用结果传递出去。</li>
  <li>当 effect 停止发送元素时，移除掉之前保存下来的订阅 token（<code class="language-plaintext highlighter-rouge">AnyCancellable</code>）。</li>
  <li>当 effect 订阅完毕后，保存订阅 token 至 <code class="language-plaintext highlighter-rouge">effectCancellables</code>。</li>
</ol>

<p>乍一看这个逻辑似乎没问题，但当遇到没有副作用的 action 时，就会有问题了！我们先来看个小例子：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">Combine</span>

<span class="k">let</span> <span class="nv">pub</span><span class="p">:</span> <span class="kt">AnyPublisher</span> <span class="o">=</span> <span class="kt">Empty</span><span class="o">&lt;</span><span class="kt">Void</span><span class="p">,</span> <span class="kt">Never</span><span class="o">&gt;</span><span class="p">(</span><span class="nv">completeImmediately</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span><span class="o">.</span><span class="nf">eraseToAnyPublisher</span><span class="p">()</span>
<span class="k">var</span> <span class="nv">set</span><span class="p">:</span> <span class="kt">Set</span><span class="o">&lt;</span><span class="kt">AnyCancellable</span><span class="o">&gt;</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">var</span> <span class="nv">cancellable</span><span class="p">:</span> <span class="kt">AnyCancellable</span><span class="o">!</span>

<span class="n">cancellable</span> <span class="o">=</span> <span class="n">pub</span><span class="o">.</span><span class="n">sink</span> <span class="p">{</span> <span class="n">_</span> <span class="k">in</span>
  <span class="nf">print</span><span class="p">(</span><span class="s">"Received Completion"</span><span class="p">)</span>
  <span class="k">set</span><span class="o">.</span><span class="nf">remove</span><span class="p">(</span><span class="n">cancellable</span><span class="p">)</span> <span class="c1">// 崩溃发生在此处</span>
<span class="p">}</span> <span class="nv">receiveValue</span><span class="p">:</span> <span class="p">{</span> <span class="n">_</span> <span class="k">in</span>
  <span class="nf">print</span><span class="p">(</span><span class="s">"Do nothing!"</span><span class="p">)</span>
<span class="p">}</span>

<span class="nf">print</span><span class="p">(</span><span class="s">"insert cancellable"</span><span class="p">)</span>
<span class="k">set</span><span class="o">.</span><span class="nf">insert</span><span class="p">(</span><span class="n">cancellable</span><span class="p">)</span>

<span class="c1">// Received Completion</span>
<span class="c1">// __lldb_expr_3/MyPlayground.playground:11: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value</span>

</code></pre></div></div>

<p>当我们订阅一个立即结束的 Publisher 时，在 <code class="language-plaintext highlighter-rouge">sink</code> 函数返回之前，Combine 就会触发 <code class="language-plaintext highlighter-rouge">receiveCompletion</code> 闭包。所以可以考虑把 <code class="language-plaintext highlighter-rouge">cancellable</code> 改为显式可选值 <code class="language-plaintext highlighter-rouge">AnyCancellable?</code>：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">var</span> <span class="nv">effectCancellable</span><span class="p">:</span> <span class="kt">AnyCancellable</span><span class="p">?</span>
<span class="n">effectCancellable</span> <span class="o">=</span> <span class="n">effect</span><span class="o">.</span><span class="nf">sink</span><span class="p">(</span>
  <span class="nv">receiveCompletion</span><span class="p">:</span> <span class="p">{</span> <span class="p">[</span><span class="k">weak</span> <span class="k">self</span><span class="p">]</span> <span class="n">_</span> <span class="k">in</span>
    <span class="k">guard</span> <span class="k">let</span> <span class="nv">effectCancellable</span> <span class="o">=</span> <span class="n">effectCancellable</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
    <span class="k">self</span><span class="p">?</span><span class="o">.</span><span class="n">effectCancellables</span><span class="o">.</span><span class="nf">remove</span><span class="p">(</span><span class="n">effectCancellable</span><span class="p">)</span>
  <span class="p">},</span>
  <span class="nv">receiveValue</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">send</span>
<span class="p">)</span>
<span class="k">if</span> <span class="k">let</span> <span class="nv">effectCancellable</span> <span class="o">=</span> <span class="n">effectCancellable</span> <span class="p">{</span>
  <span class="n">effectCancellables</span><span class="o">.</span><span class="nf">insert</span><span class="p">(</span><span class="n">effectCancellable</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>这样又带来另一个问题：当我们订阅一个立即结束的 Publisher 时，先调用 <code class="language-plaintext highlighter-rouge">receiveCompletion</code>，并不会移除当前订阅 token，因为 <code class="language-plaintext highlighter-rouge">sink</code> 函数还没返回。当 <code class="language-plaintext highlighter-rouge">sink</code> 函数执行完后，我们把当前订阅 token 加入 <code class="language-plaintext highlighter-rouge">effectCancellables</code> 中。不难发现：我们没有移除这个订阅 token 的时机了！我们还得新增一个 flag <code class="language-plaintext highlighter-rouge">didComplete</code> 来标记当前 effect 是否结束：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">var</span> <span class="nv">effectCancellable</span><span class="p">:</span> <span class="kt">AnyCancellable</span><span class="p">?</span>
<span class="k">var</span> <span class="nv">didComplete</span> <span class="o">=</span> <span class="kc">false</span>
<span class="n">effectCancellable</span> <span class="o">=</span> <span class="n">effect</span><span class="o">.</span><span class="nf">sink</span><span class="p">(</span>
  <span class="nv">receiveCompletion</span><span class="p">:</span> <span class="p">{</span> <span class="p">[</span><span class="k">weak</span> <span class="k">self</span><span class="p">]</span> <span class="n">_</span> <span class="k">in</span>
    <span class="n">didComplete</span> <span class="o">=</span> <span class="kc">true</span>
    <span class="k">guard</span> <span class="k">let</span> <span class="nv">effectCancellable</span> <span class="o">=</span> <span class="n">effectCancellable</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
    <span class="k">self</span><span class="p">?</span><span class="o">.</span><span class="n">effectCancellables</span><span class="o">.</span><span class="nf">remove</span><span class="p">(</span><span class="n">effectCancellable</span><span class="p">)</span>
  <span class="p">},</span>
  <span class="nv">receiveValue</span><span class="p">:</span> <span class="p">{</span> <span class="p">[</span><span class="k">weak</span> <span class="k">self</span><span class="p">]</span> <span class="k">in</span> <span class="k">self</span><span class="p">?</span><span class="o">.</span><span class="nf">send</span><span class="p">(</span><span class="nv">$0</span><span class="p">)</span> <span class="p">}</span>
<span class="p">)</span>
<span class="k">if</span> <span class="o">!</span><span class="n">didComplete</span><span class="p">,</span> <span class="k">let</span> <span class="nv">effectCancellable</span> <span class="o">=</span> <span class="n">effectCancellable</span> <span class="p">{</span> <span class="c1">// 在当前 effect 未结束时，且有对应的订阅 token，才把它加入 effectCancellables 中</span>
  <span class="n">effectCancellables</span><span class="o">.</span><span class="nf">insert</span><span class="p">(</span><span class="n">effectCancellable</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>但这里还有 1 处内存泄漏：<code class="language-plaintext highlighter-rouge">effectCancellable</code> 是 <code class="language-plaintext highlighter-rouge">sink</code> 的返回值，因此它强引用订阅者（<code class="language-plaintext highlighter-rouge">Subscribers.Sink</code>）。<code class="language-plaintext highlighter-rouge">Subscribers.Sink</code> 又强引用传入的 <code class="language-plaintext highlighter-rouge">receiveCompletion</code> / <code class="language-plaintext highlighter-rouge">receiveValue</code> 闭包；而这些闭包捕获了 <code class="language-plaintext highlighter-rouge">effectCancellable</code> 本身，于是形成了 <code class="language-plaintext highlighter-rouge">AnyCancellable</code> → <code class="language-plaintext highlighter-rouge">Subscribers.Sink</code> → 闭包 → <code class="language-plaintext highlighter-rouge">AnyCancellable</code> 的强引用环。</p>

<p>我们现在的做法是用 <code class="language-plaintext highlighter-rouge">effectCancellable</code> 自己本身来定位在集合 <code class="language-plaintext highlighter-rouge">effectCancellables</code> 中的位置，因为 <code class="language-plaintext highlighter-rouge">AnyCancellable</code> 遵循 <code class="language-plaintext highlighter-rouge">Hashable</code> 协议。换个思路，我们可以给每个 <code class="language-plaintext highlighter-rouge">effectCancellable</code> 创建独一无二的 <code class="language-plaintext highlighter-rouge">UUID</code>，这样 <code class="language-plaintext highlighter-rouge">receiveCompletion</code> 闭包就不需要强引用 <code class="language-plaintext highlighter-rouge">effectCancellable</code> 本身，从而达到删除 <code class="language-plaintext highlighter-rouge">effectCancellable</code> 的目的！把 <code class="language-plaintext highlighter-rouge">effectCancellables</code> 的类型从 <code class="language-plaintext highlighter-rouge">Set&lt;AnyCancellable&gt;</code> 改为 <code class="language-plaintext highlighter-rouge">[UUID: AnyCancellable]</code> 即可：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="k">var</span> <span class="nv">effectCancellables</span><span class="p">:</span> <span class="p">[</span><span class="kt">UUID</span><span class="p">:</span> <span class="kt">AnyCancellable</span><span class="p">]</span> <span class="o">=</span> <span class="p">[:]</span>

<span class="kd">public</span> <span class="kd">func</span> <span class="nf">send</span><span class="p">(</span><span class="n">_</span> <span class="nv">action</span><span class="p">:</span> <span class="kt">Action</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">let</span> <span class="nv">effects</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="nf">reducer</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="o">.</span><span class="n">value</span><span class="p">,</span> <span class="n">action</span><span class="p">)</span>
  <span class="n">effects</span><span class="o">.</span><span class="n">forEach</span> <span class="p">{</span> <span class="n">effect</span> <span class="k">in</span>
    <span class="k">var</span> <span class="nv">didComplete</span> <span class="o">=</span> <span class="kc">false</span>
    <span class="k">let</span> <span class="nv">uuid</span> <span class="o">=</span> <span class="kt">UUID</span><span class="p">()</span>

    <span class="k">let</span> <span class="nv">effectCancellable</span> <span class="o">=</span> <span class="n">effect</span><span class="o">.</span><span class="nf">sink</span><span class="p">(</span>
      <span class="nv">receiveCompletion</span><span class="p">:</span> <span class="p">{</span> <span class="p">[</span><span class="k">weak</span> <span class="k">self</span><span class="p">]</span> <span class="n">_</span> <span class="k">in</span>
        <span class="n">didComplete</span> <span class="o">=</span> <span class="kc">true</span>
        <span class="k">self</span><span class="p">?</span><span class="o">.</span><span class="n">effectCancellables</span><span class="p">[</span><span class="n">uuid</span><span class="p">]</span> <span class="o">=</span> <span class="kc">nil</span>
      <span class="p">},</span>
      <span class="nv">receiveValue</span><span class="p">:</span> <span class="p">{</span> <span class="p">[</span><span class="k">weak</span> <span class="k">self</span><span class="p">]</span> <span class="k">in</span> <span class="k">self</span><span class="p">?</span><span class="o">.</span><span class="nf">send</span><span class="p">(</span><span class="nv">$0</span><span class="p">)</span> <span class="p">}</span>
    <span class="p">)</span>

    <span class="k">if</span> <span class="o">!</span><span class="n">didComplete</span> <span class="p">{</span>
      <span class="k">self</span><span class="o">.</span><span class="n">effectCancellables</span><span class="p">[</span><span class="n">uuid</span><span class="p">]</span> <span class="o">=</span> <span class="n">effectCancellable</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="结语">结语</h2>

<p>下次再聊 TCA 中的 Dependency Injection 与 Testing。</p>]]></content><author><name>Sunset Wan</name></author><summary type="html"><![CDATA[本文延续上一节对 TCA 的建模思路，聚焦「副作用」该如何在 reducer 中显式表达与测试。我们会把原本隐式的 I/O 操作抽象为 Effect，让 reducer 返回副作用，并进一步调整 combine/pullback 以支持一组 effect 的组合。通过 Favorite Primes 的保存/读取示例，展示从「纯函数」到「可追踪副作用」的演化过程。]]></summary></entry><entry><title type="html">初识 The Composable Architecture（一）</title><link href="https://sunsetwan.github.io/2026/01/26/tca-101-p1/" rel="alternate" type="text/html" title="初识 The Composable Architecture（一）" /><published>2026-01-26T15:22:14+08:00</published><updated>2026-01-26T15:22:14+08:00</updated><id>https://sunsetwan.github.io/2026/01/26/tca-101-p1</id><content type="html" xml:base="https://sunsetwan.github.io/2026/01/26/tca-101-p1/"><![CDATA[<h2 id="reducer">Reducer</h2>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">enum</span> <span class="kt">CounterAction</span> <span class="p">{</span>
  <span class="k">case</span> <span class="n">decrTapped</span>
  <span class="k">case</span> <span class="n">incrTapped</span>
<span class="p">}</span>

<span class="kd">func</span> <span class="nf">counterReducer</span><span class="p">(</span>
  <span class="nv">state</span><span class="p">:</span> <span class="kt">AppState</span><span class="p">,</span> <span class="nv">action</span><span class="p">:</span> <span class="kt">CounterAction</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">AppState</span> <span class="p">{</span>
  <span class="k">var</span> <span class="nv">copy</span> <span class="o">=</span> <span class="n">state</span>
  <span class="k">switch</span> <span class="n">action</span> <span class="p">{</span>
  <span class="k">case</span> <span class="o">.</span><span class="nv">decrTapped</span><span class="p">:</span>
    <span class="n">copy</span><span class="o">.</span><span class="n">count</span> <span class="o">-=</span> <span class="mi">1</span>
  <span class="k">case</span> <span class="o">.</span><span class="nv">incrTapped</span><span class="p">:</span>
    <span class="n">copy</span><span class="o">.</span><span class="n">count</span> <span class="o">+=</span> <span class="mi">1</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="n">copy</span>
<span class="p">}</span>
</code></pre></div></div>

<p>上面的代码是个标准的 reducer 结构：当收到 <code class="language-plaintext highlighter-rouge">incrTapped</code> 时，修改全局状态（计数加一），再返回新的全局状态。为什么称这样的结构为 reducer？</p>

<!-- more -->

<p>参考这样的 <a href="https://developer.apple.com/documentation/swift/sequence/reduce(_:_:)">函数签名</a>：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="n">reduce</span><span class="o">&lt;</span><span class="kt">Result</span><span class="o">&gt;</span><span class="p">(</span>
    <span class="n">_</span> <span class="nv">initialResult</span><span class="p">:</span> <span class="kt">Result</span><span class="p">,</span>
    <span class="n">_</span> <span class="nv">nextPartialResult</span><span class="p">:</span> <span class="p">(</span><span class="kt">Result</span><span class="p">,</span> <span class="k">Self</span><span class="o">.</span><span class="kt">Element</span><span class="p">)</span> <span class="k">throws</span> <span class="o">-&gt;</span> <span class="kt">Result</span>
<span class="p">)</span> <span class="k">rethrows</span> <span class="o">-&gt;</span> <span class="kt">Result</span>
</code></pre></div></div>

<p>其中 <code class="language-plaintext highlighter-rouge">reducer</code> 对应的正是 <code class="language-plaintext highlighter-rouge">reduce</code> 中的 <code class="language-plaintext highlighter-rouge">nextPartialResult</code> 部分，这也是我们将它称为 <code class="language-plaintext highlighter-rouge">reducer</code> 的原因。</p>

<p>Sequence 协议中还有 <a href="https://developer.apple.com/documentation/swift/sequence/reduce(into:_:)">另一种 reduce 签名</a>：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="n">reduce</span><span class="o">&lt;</span><span class="kt">Result</span><span class="o">&gt;</span><span class="p">(</span>
    <span class="n">into</span> <span class="nv">initialResult</span><span class="p">:</span> <span class="kt">Result</span><span class="p">,</span>
    <span class="n">_</span> <span class="nv">updateAccumulatingResult</span><span class="p">:</span> <span class="p">(</span><span class="k">inout</span> <span class="kt">Result</span><span class="p">,</span> <span class="k">Self</span><span class="o">.</span><span class="kt">Element</span><span class="p">)</span> <span class="k">throws</span> <span class="o">-&gt;</span> <span class="p">()</span>
<span class="p">)</span> <span class="k">rethrows</span> <span class="o">-&gt;</span> <span class="kt">Result</span>
</code></pre></div></div>

<p>不难得出：<code class="language-plaintext highlighter-rouge">(A, B) -&gt; A</code> 可以改写为 <code class="language-plaintext highlighter-rouge">(inout A, B) -&gt; Void</code>。在工程实践中，<code class="language-plaintext highlighter-rouge">AppState</code> 迭代一段时间后，可能是个很大的结构体，使用 <code class="language-plaintext highlighter-rouge">inout</code> 可提升性能。</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">counterReducer</span><span class="p">(</span><span class="nv">value</span><span class="p">:</span> <span class="k">inout</span> <span class="kt">AppState</span><span class="p">,</span> <span class="nv">action</span><span class="p">:</span> <span class="kt">CounterAction</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">switch</span> <span class="n">action</span> <span class="p">{</span>
  <span class="k">case</span> <span class="o">.</span><span class="nv">decrTapped</span><span class="p">:</span>
    <span class="n">value</span><span class="o">.</span><span class="n">count</span> <span class="o">-=</span> <span class="mi">1</span>
  <span class="k">case</span> <span class="o">.</span><span class="nv">incrTapped</span><span class="p">:</span>
    <span class="n">value</span><span class="o">.</span><span class="n">count</span> <span class="o">+=</span> <span class="mi">1</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>纯函数（pure function）通常满足两点：</p>

<ol>
  <li>相同输入必然产生相同输出</li>
  <li>没有副作用（不修改外部对象）</li>
</ol>

<p><code class="language-plaintext highlighter-rouge">counterReducer</code> 就是一个典型的纯函数。纯函数（我觉得）最吸引人的特点就是写单元测试很方便：不依赖外部环境，单元测试只要给定输入和断言输出即可，测试简单且稳定。</p>

<p>再看这样两个 reducer：<code class="language-plaintext highlighter-rouge">primeModalReducer</code> 和 <code class="language-plaintext highlighter-rouge">favoritePrimesReducer</code>。</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">enum</span> <span class="kt">PrimeModalAction</span> <span class="p">{</span>
  <span class="k">case</span> <span class="n">saveFavoritePrimeTapped</span>
  <span class="k">case</span> <span class="n">removeFavoritePrimeTapped</span>
<span class="p">}</span>

<span class="kd">public</span> <span class="kd">func</span> <span class="nf">primeModalReducer</span><span class="p">(</span><span class="nv">state</span><span class="p">:</span> <span class="k">inout</span> <span class="kt">AppState</span><span class="p">,</span> <span class="nv">action</span><span class="p">:</span> <span class="kt">PrimeModalAction</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">switch</span> <span class="n">action</span> <span class="p">{</span>
  <span class="k">case</span> <span class="o">.</span><span class="nv">removeFavoritePrimeTapped</span><span class="p">:</span>
    <span class="n">state</span><span class="o">.</span><span class="n">favoritePrimes</span><span class="o">.</span><span class="nf">removeAll</span><span class="p">(</span><span class="nv">where</span><span class="p">:</span> <span class="p">{</span> <span class="nv">$0</span> <span class="o">==</span> <span class="n">state</span><span class="o">.</span><span class="n">count</span> <span class="p">})</span>

  <span class="k">case</span> <span class="o">.</span><span class="nv">saveFavoritePrimeTapped</span><span class="p">:</span>
    <span class="n">state</span><span class="o">.</span><span class="n">favoritePrimes</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="n">state</span><span class="o">.</span><span class="n">count</span><span class="p">)</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="kd">public</span> <span class="kd">enum</span> <span class="kt">FavoritePrimesAction</span> <span class="p">{</span>
  <span class="k">case</span> <span class="nf">deleteFavoritePrimes</span><span class="p">(</span><span class="kt">IndexSet</span><span class="p">)</span>
<span class="p">}</span>

<span class="kd">public</span> <span class="kd">func</span> <span class="nf">favoritePrimesReducer</span><span class="p">(</span><span class="nv">state</span><span class="p">:</span> <span class="k">inout</span> <span class="kt">AppState</span><span class="p">,</span> <span class="nv">action</span><span class="p">:</span> <span class="kt">FavoritePrimesAction</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">switch</span> <span class="n">action</span> <span class="p">{</span>
  <span class="k">case</span> <span class="kd">let</span> <span class="o">.</span><span class="nf">deleteFavoritePrimes</span><span class="p">(</span><span class="n">indexSet</span><span class="p">):</span>
    <span class="n">state</span><span class="o">.</span><span class="n">favoritePrimes</span><span class="o">.</span><span class="nf">remove</span><span class="p">(</span><span class="nv">atOffsets</span><span class="p">:</span> <span class="n">indexSet</span><span class="p">)</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>那么如果一个 app 的逻辑由这三个 reducer 组成，我们就需要把这三种 reducer 对应的 action 也组合在一块，不难想到：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">enum</span> <span class="kt">AppAction</span> <span class="p">{</span>
  <span class="k">case</span> <span class="nf">counter</span><span class="p">(</span><span class="kt">CounterAction</span><span class="p">)</span>
  <span class="k">case</span> <span class="nf">primeModal</span><span class="p">(</span><span class="kt">PrimeModalAction</span><span class="p">)</span>
  <span class="k">case</span> <span class="nf">favoritePrimes</span><span class="p">(</span><span class="kt">FavoritePrimesAction</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>三个 reducer 的函数签名也得一并调整：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">counterReducer</span><span class="p">(</span><span class="nv">state</span><span class="p">:</span> <span class="k">inout</span> <span class="kt">AppState</span><span class="p">,</span> <span class="nv">action</span><span class="p">:</span> <span class="kt">AppAction</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">switch</span> <span class="n">action</span> <span class="p">{</span>
  <span class="k">case</span> <span class="o">.</span><span class="nf">counter</span><span class="p">(</span><span class="o">.</span><span class="n">decrTapped</span><span class="p">):</span>
    <span class="n">state</span><span class="o">.</span><span class="n">count</span> <span class="o">-=</span> <span class="mi">1</span>

  <span class="k">case</span> <span class="o">.</span><span class="nf">counter</span><span class="p">(</span><span class="o">.</span><span class="n">incrTapped</span><span class="p">):</span>
    <span class="n">state</span><span class="o">.</span><span class="n">count</span> <span class="o">+=</span> <span class="mi">1</span>

  <span class="k">default</span><span class="p">:</span>
    <span class="k">break</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="kd">func</span> <span class="nf">primeModalReducer</span><span class="p">(</span><span class="nv">state</span><span class="p">:</span> <span class="k">inout</span> <span class="kt">AppState</span><span class="p">,</span> <span class="nv">action</span><span class="p">:</span> <span class="kt">AppAction</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">switch</span> <span class="n">action</span> <span class="p">{</span>
  <span class="k">case</span> <span class="o">.</span><span class="nf">primeModal</span><span class="p">(</span><span class="o">.</span><span class="n">removeFavoritePrimeTapped</span><span class="p">):</span>
    <span class="n">state</span><span class="o">.</span><span class="n">favoritePrimes</span><span class="o">.</span><span class="nf">removeAll</span><span class="p">(</span><span class="nv">where</span><span class="p">:</span> <span class="p">{</span> <span class="nv">$0</span> <span class="o">==</span> <span class="n">state</span><span class="o">.</span><span class="n">count</span> <span class="p">})</span>
    <span class="n">state</span><span class="o">.</span><span class="n">activityFeed</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="nv">timestamp</span><span class="p">:</span> <span class="kt">Date</span><span class="p">(),</span> <span class="nv">type</span><span class="p">:</span> <span class="o">.</span><span class="nf">removedFavoritePrime</span><span class="p">(</span><span class="n">state</span><span class="o">.</span><span class="n">count</span><span class="p">)))</span>

  <span class="k">case</span> <span class="o">.</span><span class="nf">primeModal</span><span class="p">(</span><span class="o">.</span><span class="n">saveFavoritePrimeTapped</span><span class="p">):</span>
    <span class="n">state</span><span class="o">.</span><span class="n">favoritePrimes</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="n">state</span><span class="o">.</span><span class="n">count</span><span class="p">)</span>
    <span class="n">state</span><span class="o">.</span><span class="n">activityFeed</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="nv">timestamp</span><span class="p">:</span> <span class="kt">Date</span><span class="p">(),</span> <span class="nv">type</span><span class="p">:</span> <span class="o">.</span><span class="nf">addedFavoritePrime</span><span class="p">(</span><span class="n">state</span><span class="o">.</span><span class="n">count</span><span class="p">)))</span>

  <span class="k">default</span><span class="p">:</span>
    <span class="k">break</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="kd">func</span> <span class="nf">favoritePrimesReducer</span><span class="p">(</span><span class="nv">state</span><span class="p">:</span> <span class="k">inout</span> <span class="kt">AppState</span><span class="p">,</span> <span class="nv">action</span><span class="p">:</span> <span class="kt">AppAction</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">switch</span> <span class="n">action</span> <span class="p">{</span>
  <span class="k">case</span> <span class="kd">let</span> <span class="o">.</span><span class="nf">favoritePrimes</span><span class="p">(</span><span class="o">.</span><span class="nf">deleteFavoritePrimes</span><span class="p">(</span><span class="n">indexSet</span><span class="p">)):</span>
    <span class="k">let</span> <span class="nv">removedPrimes</span> <span class="o">=</span> <span class="n">indexSet</span><span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">state</span><span class="o">.</span><span class="n">favoritePrimes</span><span class="p">[</span><span class="nv">$0</span><span class="p">]</span> <span class="p">}</span>
    <span class="n">state</span><span class="o">.</span><span class="n">favoritePrimes</span><span class="o">.</span><span class="nf">remove</span><span class="p">(</span><span class="nv">atOffsets</span><span class="p">:</span> <span class="n">indexSet</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">prime</span> <span class="k">in</span> <span class="n">removedPrimes</span> <span class="p">{</span>
      <span class="n">state</span><span class="o">.</span><span class="n">activityFeed</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="nv">timestamp</span><span class="p">:</span> <span class="kt">Date</span><span class="p">(),</span> <span class="nv">type</span><span class="p">:</span> <span class="o">.</span><span class="nf">removedFavoritePrime</span><span class="p">(</span><span class="n">prime</span><span class="p">)))</span>
    <span class="p">}</span>

  <span class="k">default</span><span class="p">:</span>
    <span class="k">break</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>得出整个 app 最大的 reducer：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">appReducer</span><span class="p">(</span><span class="nv">value</span><span class="p">:</span> <span class="k">inout</span> <span class="kt">AppState</span><span class="p">,</span> <span class="nv">action</span><span class="p">:</span> <span class="kt">AppAction</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span> <span class="p">{</span>
  <span class="k">switch</span> <span class="n">action</span> <span class="p">{</span>
  <span class="k">case</span> <span class="o">.</span><span class="nf">counter</span><span class="p">(</span><span class="o">.</span><span class="n">decrTapped</span><span class="p">):</span>
    <span class="n">value</span><span class="o">.</span><span class="n">count</span> <span class="o">-=</span> <span class="mi">1</span>

  <span class="k">case</span> <span class="o">.</span><span class="nf">counter</span><span class="p">(</span><span class="o">.</span><span class="n">incrTapped</span><span class="p">):</span>
    <span class="n">value</span><span class="o">.</span><span class="n">count</span> <span class="o">+=</span> <span class="mi">1</span>

  <span class="k">case</span> <span class="o">.</span><span class="nf">primeModal</span><span class="p">(</span><span class="o">.</span><span class="n">saveFavoritePrimeTapped</span><span class="p">):</span>
    <span class="n">value</span><span class="o">.</span><span class="n">favoritePrimes</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="n">value</span><span class="o">.</span><span class="n">count</span><span class="p">)</span>
    <span class="n">value</span><span class="o">.</span><span class="n">activityFeed</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="nv">timestamp</span><span class="p">:</span> <span class="kt">Date</span><span class="p">(),</span> <span class="nv">type</span><span class="p">:</span> <span class="o">.</span><span class="nf">addedFavoritePrime</span><span class="p">(</span><span class="n">value</span><span class="o">.</span><span class="n">count</span><span class="p">)))</span>

  <span class="k">case</span> <span class="o">.</span><span class="nf">primeModal</span><span class="p">(</span><span class="o">.</span><span class="n">removeFavoritePrimeTapped</span><span class="p">):</span>
    <span class="n">value</span><span class="o">.</span><span class="n">favoritePrimes</span><span class="o">.</span><span class="nf">removeAll</span><span class="p">(</span><span class="nv">where</span><span class="p">:</span> <span class="p">{</span> <span class="nv">$0</span> <span class="o">==</span> <span class="n">value</span><span class="o">.</span><span class="n">count</span> <span class="p">})</span>
    <span class="n">value</span><span class="o">.</span><span class="n">activityFeed</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="nv">timestamp</span><span class="p">:</span> <span class="kt">Date</span><span class="p">(),</span> <span class="nv">type</span><span class="p">:</span> <span class="o">.</span><span class="nf">removedFavoritePrime</span><span class="p">(</span><span class="n">value</span><span class="o">.</span><span class="n">count</span><span class="p">)))</span>

  <span class="k">case</span> <span class="kd">let</span> <span class="o">.</span><span class="nf">favoritePrimes</span><span class="p">(</span><span class="o">.</span><span class="nf">deleteFavoritePrimes</span><span class="p">(</span><span class="n">indexSet</span><span class="p">)):</span>
    <span class="k">let</span> <span class="nv">removedPrimes</span> <span class="o">=</span> <span class="n">indexSet</span><span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">value</span><span class="o">.</span><span class="n">favoritePrimes</span><span class="p">[</span><span class="nv">$0</span><span class="p">]</span> <span class="p">}</span>
    <span class="n">value</span><span class="o">.</span><span class="n">favoritePrimes</span><span class="o">.</span><span class="nf">remove</span><span class="p">(</span><span class="nv">atOffsets</span><span class="p">:</span> <span class="n">indexSet</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">prime</span> <span class="k">in</span> <span class="n">removedPrimes</span> <span class="p">{</span>
      <span class="n">value</span><span class="o">.</span><span class="n">activityFeed</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="nv">timestamp</span><span class="p">:</span> <span class="kt">Date</span><span class="p">(),</span> <span class="nv">type</span><span class="p">:</span> <span class="o">.</span><span class="nf">removedFavoritePrime</span><span class="p">(</span><span class="n">prime</span><span class="p">)))</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>整个 app reducer 只是把三种局部 reducer 里的逻辑拷贝过来。可以想象，随着业务的迭代，整个 app reducer 会迅速膨胀。如果三个 reducer 能像乐高积木一样，组成一个最大的 app reducer 就好了！</p>

<h2 id="how-to-be-composable">How to be composable?</h2>

<p>当 app reducer 收到某个 action 时，它如果可以直接把 action 分发到每个 reducer，让每个 reducer 独立决定是否处理这个 action 就好了！顺着这个思路，我们可以写出这样的 <code class="language-plaintext highlighter-rouge">combine</code> 函数：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="n">combine</span><span class="o">&lt;</span><span class="kt">Value</span><span class="p">,</span> <span class="kt">Action</span><span class="o">&gt;</span><span class="p">(</span>
  <span class="n">_</span> <span class="nv">reducers</span><span class="p">:</span> <span class="p">(</span><span class="k">inout</span> <span class="kt">Value</span><span class="p">,</span> <span class="kt">Action</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span><span class="o">...</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="p">(</span><span class="k">inout</span> <span class="kt">Value</span><span class="p">,</span> <span class="kt">Action</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span> <span class="p">{</span>

  <span class="k">return</span> <span class="p">{</span> <span class="n">value</span><span class="p">,</span> <span class="n">action</span> <span class="k">in</span>
    <span class="k">for</span> <span class="n">reducer</span> <span class="k">in</span> <span class="n">reducers</span> <span class="p">{</span>
      <span class="nf">reducer</span><span class="p">(</span><span class="o">&amp;</span><span class="n">value</span><span class="p">,</span> <span class="n">action</span><span class="p">)</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>那么，app reducer 可简化为：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">appReducer</span> <span class="o">=</span> <span class="nf">combine</span><span class="p">(</span>
  <span class="n">counterReducer</span><span class="p">,</span>
  <span class="n">primeModalReducer</span><span class="p">,</span>
  <span class="n">favoritePrimesReducer</span>
<span class="p">)</span>
</code></pre></div></div>

<p>回看现在的 <code class="language-plaintext highlighter-rouge">counterReducer</code>，总感觉哪里怪怪的：它知道得太多了！它只需要知道它负责把一个 <code class="language-plaintext highlighter-rouge">inout</code> Int 加加减减就够了！它只需要在意 <code class="language-plaintext highlighter-rouge">.incrTapped</code> 和 <code class="language-plaintext highlighter-rouge">.decrTapped</code> 这两个 action 就行了！最终目标是 <code class="language-plaintext highlighter-rouge">func counterReducer(state: inout Int, action: CounterAction) { ... }</code>，但我们先只简化 State，Action 暂时保留 <code class="language-plaintext highlighter-rouge">AppAction</code>。</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 最终目标：func counterReducer(state: inout Int, action: CounterAction) { ... }</span>
<span class="kd">func</span> <span class="nf">counterReducer</span><span class="p">(</span><span class="nv">state</span><span class="p">:</span> <span class="k">inout</span> <span class="kt">AppState</span><span class="p">,</span> <span class="nv">action</span><span class="p">:</span> <span class="kt">AppAction</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">switch</span> <span class="n">action</span> <span class="p">{</span>
  <span class="k">case</span> <span class="o">.</span><span class="nf">counter</span><span class="p">(</span><span class="o">.</span><span class="n">decrTapped</span><span class="p">):</span>
    <span class="n">state</span><span class="o">.</span><span class="n">count</span> <span class="o">-=</span> <span class="mi">1</span>

  <span class="k">case</span> <span class="o">.</span><span class="nf">counter</span><span class="p">(</span><span class="o">.</span><span class="n">incrTapped</span><span class="p">):</span>
    <span class="n">state</span><span class="o">.</span><span class="n">count</span> <span class="o">+=</span> <span class="mi">1</span>

  <span class="k">default</span><span class="p">:</span>
    <span class="k">break</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="简化-state">简化 State</h3>

<p>这一步我们想要这样的 <code class="language-plaintext highlighter-rouge">counterReducer</code>：<code class="language-plaintext highlighter-rouge">(inout Int, AppAction) -&gt; Void</code>，整个 app reducer 是 <code class="language-plaintext highlighter-rouge">(inout AppState, AppAction) -&gt; Void</code>。我们需要新定义个 <code class="language-plaintext highlighter-rouge">transform</code> 函数来帮助完成这样的类型转换：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="n">transform</span><span class="o">&lt;</span><span class="kt">LocalValue</span><span class="p">,</span> <span class="kt">GlobalValue</span><span class="p">,</span> <span class="kt">Action</span><span class="o">&gt;</span><span class="p">(</span>
  <span class="n">_</span> <span class="nv">reducer</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="k">inout</span> <span class="kt">LocalValue</span><span class="p">,</span> <span class="kt">Action</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="p">(</span><span class="k">inout</span> <span class="kt">GlobalValue</span><span class="p">,</span> <span class="kt">Action</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span> <span class="p">{</span>
  <span class="k">return</span> <span class="p">{</span> <span class="n">globalValue</span><span class="p">,</span> <span class="n">action</span> <span class="k">in</span>
    <span class="c1">// Call the reducer</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>这里先用 <code class="language-plaintext highlighter-rouge">transform</code> 占位，实际我们会用 <code class="language-plaintext highlighter-rouge">pullback</code> 来完成这个映射。所有的局部 reducer 都得通过这种变换，变为 <code class="language-plaintext highlighter-rouge">(inout AppState, AppAction) -&gt; Void</code>，才能不破坏上面多个小 reducer combine 为一个大 reducer 的逻辑。</p>

<p>我们需要 <code class="language-plaintext highlighter-rouge">f: (GlobalValue) -&gt; LocalValue</code> 这样的变化，然后再调用 reducer：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="n">pullback</span><span class="o">&lt;</span><span class="kt">LocalValue</span><span class="p">,</span> <span class="kt">GlobalValue</span><span class="p">,</span> <span class="kt">Action</span><span class="o">&gt;</span><span class="p">(</span>
  <span class="n">_</span> <span class="nv">reducer</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="k">inout</span> <span class="kt">LocalValue</span><span class="p">,</span> <span class="kt">Action</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span><span class="p">,</span>
  <span class="n">_</span> <span class="nv">f</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="kt">GlobalValue</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">LocalValue</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="p">(</span><span class="k">inout</span> <span class="kt">GlobalValue</span><span class="p">,</span> <span class="kt">Action</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span> <span class="p">{</span>

  <span class="k">return</span> <span class="p">{</span> <span class="n">globalValue</span><span class="p">,</span> <span class="n">action</span> <span class="k">in</span>
    <span class="k">var</span> <span class="nv">localValue</span> <span class="o">=</span> <span class="nf">f</span><span class="p">(</span><span class="n">globalValue</span><span class="p">)</span>
    <span class="nf">reducer</span><span class="p">(</span><span class="o">&amp;</span><span class="n">localValue</span><span class="p">,</span> <span class="n">action</span><span class="p">)</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>这样的变化，可取名叫 <code class="language-plaintext highlighter-rouge">pullback</code>，意味从局部 reducer 拉回至全局的 reducer。</p>

<p>上面的实现仅改变了 <code class="language-plaintext highlighter-rouge">localValue</code>，并没有把 <code class="language-plaintext highlighter-rouge">localValue</code> 的改变反射回 <code class="language-plaintext highlighter-rouge">globalValue</code> 上，还需新增 <code class="language-plaintext highlighter-rouge">s: (inout GlobalValue, LocalValue) -&gt; Void</code>。<code class="language-plaintext highlighter-rouge">f</code> 更名为 <code class="language-plaintext highlighter-rouge">get</code>，<code class="language-plaintext highlighter-rouge">s</code> 更名为 <code class="language-plaintext highlighter-rouge">set</code>。</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="n">pullback</span><span class="o">&lt;</span><span class="kt">LocalValue</span><span class="p">,</span> <span class="kt">GlobalValue</span><span class="p">,</span> <span class="kt">Action</span><span class="o">&gt;</span><span class="p">(</span>
  <span class="n">_</span> <span class="nv">reducer</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="k">inout</span> <span class="kt">LocalValue</span><span class="p">,</span> <span class="kt">Action</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span><span class="p">,</span>
  <span class="nv">get</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="kt">GlobalValue</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">LocalValue</span><span class="p">,</span>
  <span class="nv">set</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="k">inout</span> <span class="kt">GlobalValue</span><span class="p">,</span> <span class="kt">LocalValue</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="p">(</span><span class="k">inout</span> <span class="kt">GlobalValue</span><span class="p">,</span> <span class="kt">Action</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span> <span class="p">{</span>

  <span class="k">return</span> <span class="p">{</span> <span class="n">globalValue</span><span class="p">,</span> <span class="n">action</span> <span class="k">in</span>
    <span class="k">var</span> <span class="nv">localValue</span> <span class="o">=</span> <span class="nf">get</span><span class="p">(</span><span class="n">globalValue</span><span class="p">)</span>
    <span class="nf">reducer</span><span class="p">(</span><span class="o">&amp;</span><span class="n">localValue</span><span class="p">,</span> <span class="n">action</span><span class="p">)</span>
    <span class="nf">set</span><span class="p">(</span><span class="o">&amp;</span><span class="n">globalValue</span><span class="p">,</span> <span class="n">localValue</span><span class="p">)</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>在 Swift 中，一对 getter 和 setter 可以用 <a href="https://developer.apple.com/documentation/swift/writablekeypath">WritableKeyPath</a> 来简化：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="n">pullback</span><span class="o">&lt;</span><span class="kt">LocalValue</span><span class="p">,</span> <span class="kt">GlobalValue</span><span class="p">,</span> <span class="kt">Action</span><span class="o">&gt;</span><span class="p">(</span>
  <span class="n">_</span> <span class="nv">reducer</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="k">inout</span> <span class="kt">LocalValue</span><span class="p">,</span> <span class="kt">Action</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span><span class="p">,</span>
  <span class="nv">value</span><span class="p">:</span> <span class="kt">WritableKeyPath</span><span class="o">&lt;</span><span class="kt">GlobalValue</span><span class="p">,</span> <span class="kt">LocalValue</span><span class="o">&gt;</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="p">(</span><span class="k">inout</span> <span class="kt">GlobalValue</span><span class="p">,</span> <span class="kt">Action</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span> <span class="p">{</span>
  <span class="k">return</span> <span class="p">{</span> <span class="n">globalValue</span><span class="p">,</span> <span class="n">action</span> <span class="k">in</span>
    <span class="nf">reducer</span><span class="p">(</span><span class="o">&amp;</span><span class="n">globalValue</span><span class="p">[</span><span class="nv">keyPath</span><span class="p">:</span> <span class="n">value</span><span class="p">],</span> <span class="n">action</span><span class="p">)</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">FavoritePrimesState</span> <span class="p">{</span>
  <span class="k">var</span> <span class="nv">favoritePrimes</span><span class="p">:</span> <span class="p">[</span><span class="kt">Int</span><span class="p">]</span>
  <span class="k">var</span> <span class="nv">activityFeed</span><span class="p">:</span> <span class="p">[</span><span class="kt">AppState</span><span class="o">.</span><span class="kt">Activity</span><span class="p">]</span>
<span class="p">}</span>

<span class="kd">func</span> <span class="nf">favoritePrimesReducer</span><span class="p">(</span><span class="nv">state</span><span class="p">:</span> <span class="k">inout</span> <span class="kt">FavoritePrimesState</span><span class="p">,</span> <span class="nv">action</span><span class="p">:</span> <span class="kt">AppAction</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">switch</span> <span class="n">action</span> <span class="p">{</span>
  <span class="k">case</span> <span class="kd">let</span> <span class="o">.</span><span class="nf">favoritePrimes</span><span class="p">(</span><span class="o">.</span><span class="nf">deleteFavoritePrimes</span><span class="p">(</span><span class="n">indexSet</span><span class="p">)):</span>
    <span class="k">let</span> <span class="nv">removedPrimes</span> <span class="o">=</span> <span class="n">indexSet</span><span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">state</span><span class="o">.</span><span class="n">favoritePrimes</span><span class="p">[</span><span class="nv">$0</span><span class="p">]</span> <span class="p">}</span>
    <span class="n">state</span><span class="o">.</span><span class="n">favoritePrimes</span><span class="o">.</span><span class="nf">remove</span><span class="p">(</span><span class="nv">atOffsets</span><span class="p">:</span> <span class="n">indexSet</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">prime</span> <span class="k">in</span> <span class="n">removedPrimes</span> <span class="p">{</span>
      <span class="n">state</span><span class="o">.</span><span class="n">activityFeed</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="nv">timestamp</span><span class="p">:</span> <span class="kt">Date</span><span class="p">(),</span> <span class="nv">type</span><span class="p">:</span> <span class="o">.</span><span class="nf">removedFavoritePrime</span><span class="p">(</span><span class="n">prime</span><span class="p">)))</span>
    <span class="p">}</span>

  <span class="k">default</span><span class="p">:</span>
    <span class="k">break</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="kd">extension</span> <span class="kt">AppState</span> <span class="p">{</span>
  <span class="k">var</span> <span class="nv">favoritePrimesState</span><span class="p">:</span> <span class="kt">FavoritePrimesState</span> <span class="p">{</span>
    <span class="k">get</span> <span class="p">{</span>
      <span class="kt">FavoritePrimesState</span><span class="p">(</span>
        <span class="nv">favoritePrimes</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">favoritePrimes</span><span class="p">,</span>
        <span class="nv">activityFeed</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">activityFeed</span>
      <span class="p">)</span>
    <span class="p">}</span>
    <span class="k">set</span> <span class="p">{</span>
      <span class="k">self</span><span class="o">.</span><span class="n">favoritePrimes</span> <span class="o">=</span> <span class="n">newValue</span><span class="o">.</span><span class="n">favoritePrimes</span>
      <span class="k">self</span><span class="o">.</span><span class="n">activityFeed</span> <span class="o">=</span> <span class="n">newValue</span><span class="o">.</span><span class="n">activityFeed</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="kd">func</span> <span class="nf">primeModalReducer</span><span class="p">(</span><span class="nv">state</span><span class="p">:</span> <span class="k">inout</span> <span class="kt">AppState</span><span class="p">,</span> <span class="nv">action</span><span class="p">:</span> <span class="kt">AppAction</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">switch</span> <span class="n">action</span> <span class="p">{</span>
  <span class="k">case</span> <span class="o">.</span><span class="nf">primeModal</span><span class="p">(</span><span class="o">.</span><span class="n">removeFavoritePrimeTapped</span><span class="p">):</span>
    <span class="n">state</span><span class="o">.</span><span class="n">favoritePrimes</span><span class="o">.</span><span class="nf">removeAll</span><span class="p">(</span><span class="nv">where</span><span class="p">:</span> <span class="p">{</span> <span class="nv">$0</span> <span class="o">==</span> <span class="n">state</span><span class="o">.</span><span class="n">count</span> <span class="p">})</span>
    <span class="n">state</span><span class="o">.</span><span class="n">activityFeed</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="nv">timestamp</span><span class="p">:</span> <span class="kt">Date</span><span class="p">(),</span> <span class="nv">type</span><span class="p">:</span> <span class="o">.</span><span class="nf">removedFavoritePrime</span><span class="p">(</span><span class="n">state</span><span class="o">.</span><span class="n">count</span><span class="p">)))</span>

  <span class="k">case</span> <span class="o">.</span><span class="nf">primeModal</span><span class="p">(</span><span class="o">.</span><span class="n">saveFavoritePrimeTapped</span><span class="p">):</span>
    <span class="n">state</span><span class="o">.</span><span class="n">favoritePrimes</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="n">state</span><span class="o">.</span><span class="n">count</span><span class="p">)</span>
    <span class="n">state</span><span class="o">.</span><span class="n">activityFeed</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="nv">timestamp</span><span class="p">:</span> <span class="kt">Date</span><span class="p">(),</span> <span class="nv">type</span><span class="p">:</span> <span class="o">.</span><span class="nf">addedFavoritePrime</span><span class="p">(</span><span class="n">state</span><span class="o">.</span><span class="n">count</span><span class="p">)))</span>

  <span class="k">default</span><span class="p">:</span>
    <span class="k">break</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// 直接操作必须要用到的状态，剔除多余的状态</span>
<span class="kd">func</span> <span class="nf">counterReducer</span><span class="p">(</span><span class="nv">state</span><span class="p">:</span> <span class="k">inout</span> <span class="kt">Int</span><span class="p">,</span> <span class="nv">action</span><span class="p">:</span> <span class="kt">AppAction</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">switch</span> <span class="n">action</span> <span class="p">{</span>
  <span class="k">case</span> <span class="o">.</span><span class="nf">counter</span><span class="p">(</span><span class="o">.</span><span class="n">decrTapped</span><span class="p">):</span>
    <span class="n">state</span> <span class="o">-=</span> <span class="mi">1</span>

  <span class="k">case</span> <span class="o">.</span><span class="nf">counter</span><span class="p">(</span><span class="o">.</span><span class="n">incrTapped</span><span class="p">):</span>
    <span class="n">state</span> <span class="o">+=</span> <span class="mi">1</span>

  <span class="k">default</span><span class="p">:</span>
    <span class="k">break</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">let</span> <span class="nv">appReducer</span> <span class="o">=</span> <span class="nf">combine</span><span class="p">(</span>
  <span class="nf">pullback</span><span class="p">(</span><span class="n">counterReducer</span><span class="p">,</span> <span class="nv">value</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="n">count</span><span class="p">),</span>
  <span class="n">primeModalReducer</span><span class="p">,</span>
  <span class="nf">pullback</span><span class="p">(</span><span class="n">favoritePrimesReducer</span><span class="p">,</span> <span class="nv">value</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="n">favoritePrimesState</span><span class="p">)</span>
<span class="p">)</span>
</code></pre></div></div>

<h3 id="简化-action">简化 Action</h3>

<p><code class="language-plaintext highlighter-rouge">counterReducer</code> 中仅使用 <code class="language-plaintext highlighter-rouge">AppAction</code> 中 <code class="language-plaintext highlighter-rouge">counter</code> 下的 actions，其他 enum case 默认不处理。这样也是易错的，app reducer 收到所有的 action 通通都会发送给这三个局部 reducer，如果局部 reducer 处理了不属于自己职责范围内的 action，就会出现 BUG！</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">counterReducer</span><span class="p">(</span><span class="nv">state</span><span class="p">:</span> <span class="k">inout</span> <span class="kt">Int</span><span class="p">,</span> <span class="nv">action</span><span class="p">:</span> <span class="kt">CounterAction</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">switch</span> <span class="n">action</span> <span class="p">{</span>
  <span class="k">case</span> <span class="o">.</span><span class="nv">decrTapped</span><span class="p">:</span>
    <span class="n">state</span> <span class="o">-=</span> <span class="mi">1</span>

  <span class="k">case</span> <span class="o">.</span><span class="nv">incrTapped</span><span class="p">:</span>
    <span class="n">state</span> <span class="o">+=</span> <span class="mi">1</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>同样地，我们也要把 <code class="language-plaintext highlighter-rouge">counterReducer</code> 能感知到的 action 简化至它仅需的 <code class="language-plaintext highlighter-rouge">CounterAction</code>！</p>

<p>我们需要这样的变换：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="n">pullback</span><span class="o">&lt;</span><span class="kt">Value</span><span class="p">,</span> <span class="kt">GlobalAction</span><span class="p">,</span> <span class="kt">LocalAction</span><span class="o">&gt;</span><span class="p">(</span>
  <span class="n">_</span> <span class="nv">reducer</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="k">inout</span> <span class="kt">Value</span><span class="p">,</span> <span class="kt">LocalAction</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span><span class="p">,</span>
  <span class="nv">action</span><span class="p">:</span> <span class="kt">WritableKeyPath</span><span class="o">&lt;</span><span class="kt">GlobalAction</span><span class="p">,</span> <span class="kt">LocalAction</span><span class="p">?</span><span class="o">&gt;</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="p">(</span><span class="k">inout</span> <span class="kt">Value</span><span class="p">,</span> <span class="kt">GlobalAction</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span> <span class="p">{</span>

  <span class="k">return</span> <span class="p">{</span> <span class="n">value</span><span class="p">,</span> <span class="n">globalAction</span> <span class="k">in</span>
    <span class="k">guard</span> <span class="k">let</span> <span class="nv">localAction</span> <span class="o">=</span> <span class="n">globalAction</span><span class="p">[</span><span class="nv">keyPath</span><span class="p">:</span> <span class="n">action</span><span class="p">]</span>
    <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>

    <span class="nf">reducer</span><span class="p">(</span><span class="o">&amp;</span><span class="n">value</span><span class="p">,</span> <span class="n">localAction</span><span class="p">)</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>为什么 <code class="language-plaintext highlighter-rouge">action: WritableKeyPath&lt;GlobalAction, LocalAction?&gt;</code> 中 <code class="language-plaintext highlighter-rouge">LocalAction</code> 是可选呢？</p>

<p>Swift 中没有类似的 enum KeyPath 概念：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">EnumKeyPath</span><span class="o">&lt;</span><span class="kt">Root</span><span class="p">,</span> <span class="kt">Value</span><span class="o">&gt;</span> <span class="p">{</span>
  <span class="k">let</span> <span class="nv">embed</span><span class="p">:</span> <span class="p">(</span><span class="kt">Value</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Root</span>
  <span class="k">let</span> <span class="nv">extract</span><span class="p">:</span> <span class="p">(</span><span class="kt">Root</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Value</span><span class="p">?</span>
<span class="p">}</span>
</code></pre></div></div>

<p>通过手动创建计算属性后，我们可以看到 getter 始终返回的是可选值。</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">enum</span> <span class="kt">AppAction</span> <span class="p">{</span>
  <span class="k">case</span> <span class="nf">counter</span><span class="p">(</span><span class="kt">CounterAction</span><span class="p">)</span>
  <span class="o">...</span>

  <span class="k">var</span> <span class="nv">counter</span><span class="p">:</span> <span class="kt">CounterAction</span><span class="p">?</span> <span class="p">{</span>
    <span class="k">get</span> <span class="p">{</span>
      <span class="k">guard</span> <span class="k">case</span> <span class="kd">let</span> <span class="o">.</span><span class="nf">counter</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="o">=</span> <span class="k">self</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">nil</span> <span class="p">}</span>
      <span class="k">return</span> <span class="n">value</span>
    <span class="p">}</span>
    <span class="k">set</span> <span class="p">{</span>
      <span class="k">guard</span> <span class="k">case</span> <span class="o">.</span><span class="n">counter</span> <span class="o">=</span> <span class="k">self</span><span class="p">,</span> <span class="k">let</span> <span class="nv">newValue</span> <span class="o">=</span> <span class="n">newValue</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
      <span class="k">self</span> <span class="o">=</span> <span class="o">.</span><span class="nf">counter</span><span class="p">(</span><span class="n">newValue</span><span class="p">)</span>
    <span class="p">}</span>
  <span class="p">}</span>
  <span class="o">...</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="合并两种-pullback">合并两种 pullback</h3>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="n">pullback</span><span class="o">&lt;</span><span class="kt">GlobalValue</span><span class="p">,</span> <span class="kt">LocalValue</span><span class="p">,</span> <span class="kt">GlobalAction</span><span class="p">,</span> <span class="kt">LocalAction</span><span class="o">&gt;</span><span class="p">(</span>
  <span class="n">_</span> <span class="nv">reducer</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="k">inout</span> <span class="kt">LocalValue</span><span class="p">,</span> <span class="kt">LocalAction</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span><span class="p">,</span>
  <span class="nv">value</span><span class="p">:</span> <span class="kt">WritableKeyPath</span><span class="o">&lt;</span><span class="kt">GlobalValue</span><span class="p">,</span> <span class="kt">LocalValue</span><span class="o">&gt;</span><span class="p">,</span>
  <span class="nv">action</span><span class="p">:</span> <span class="kt">WritableKeyPath</span><span class="o">&lt;</span><span class="kt">GlobalAction</span><span class="p">,</span> <span class="kt">LocalAction</span><span class="p">?</span><span class="o">&gt;</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="p">(</span><span class="k">inout</span> <span class="kt">GlobalValue</span><span class="p">,</span> <span class="kt">GlobalAction</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span> <span class="p">{</span>

  <span class="k">return</span> <span class="p">{</span> <span class="n">globalValue</span><span class="p">,</span> <span class="n">globalAction</span> <span class="k">in</span>
    <span class="k">guard</span> <span class="k">let</span> <span class="nv">localAction</span> <span class="o">=</span> <span class="n">globalAction</span><span class="p">[</span><span class="nv">keyPath</span><span class="p">:</span> <span class="n">action</span><span class="p">]</span>
    <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>

    <span class="nf">reducer</span><span class="p">(</span><span class="o">&amp;</span><span class="n">globalValue</span><span class="p">[</span><span class="nv">keyPath</span><span class="p">:</span> <span class="n">value</span><span class="p">],</span> <span class="n">localAction</span><span class="p">)</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>（我当时第一眼看到这样的函数签名，我也是惊呆的！）</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">enum</span> <span class="kt">CounterAction</span> <span class="p">{</span>
  <span class="k">case</span> <span class="n">decrTapped</span>
  <span class="k">case</span> <span class="n">incrTapped</span>
<span class="p">}</span>

<span class="kd">func</span> <span class="nf">counterReducer</span><span class="p">(</span><span class="nv">state</span><span class="p">:</span> <span class="k">inout</span> <span class="kt">Int</span><span class="p">,</span> <span class="nv">action</span><span class="p">:</span> <span class="kt">CounterAction</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">switch</span> <span class="n">action</span> <span class="p">{</span>
  <span class="k">case</span> <span class="o">.</span><span class="nv">decrTapped</span><span class="p">:</span>
    <span class="n">state</span> <span class="o">-=</span> <span class="mi">1</span>

  <span class="k">case</span> <span class="o">.</span><span class="nv">incrTapped</span><span class="p">:</span>
    <span class="n">state</span> <span class="o">+=</span> <span class="mi">1</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="kd">enum</span> <span class="kt">PrimeModalAction</span> <span class="p">{</span>
  <span class="k">case</span> <span class="n">saveFavoritePrimeTapped</span>
  <span class="k">case</span> <span class="n">removeFavoritePrimeTapped</span>
<span class="p">}</span>

<span class="kd">func</span> <span class="nf">primeModalReducer</span><span class="p">(</span><span class="nv">state</span><span class="p">:</span> <span class="k">inout</span> <span class="kt">AppState</span><span class="p">,</span> <span class="nv">action</span><span class="p">:</span> <span class="kt">PrimeModalAction</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span> <span class="p">{</span>
  <span class="k">switch</span> <span class="n">action</span> <span class="p">{</span>
  <span class="k">case</span> <span class="o">.</span><span class="nv">removeFavoritePrimeTapped</span><span class="p">:</span>
    <span class="n">state</span><span class="o">.</span><span class="n">favoritePrimes</span><span class="o">.</span><span class="nf">removeAll</span><span class="p">(</span><span class="nv">where</span><span class="p">:</span> <span class="p">{</span> <span class="nv">$0</span> <span class="o">==</span> <span class="n">state</span><span class="o">.</span><span class="n">count</span> <span class="p">})</span>
    <span class="n">state</span><span class="o">.</span><span class="n">activityFeed</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="nv">timestamp</span><span class="p">:</span> <span class="kt">Date</span><span class="p">(),</span> <span class="nv">type</span><span class="p">:</span> <span class="o">.</span><span class="nf">removedFavoritePrime</span><span class="p">(</span><span class="n">state</span><span class="o">.</span><span class="n">count</span><span class="p">)))</span>

  <span class="k">case</span> <span class="o">.</span><span class="nv">saveFavoritePrimeTapped</span><span class="p">:</span>
    <span class="n">state</span><span class="o">.</span><span class="n">favoritePrimes</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="n">state</span><span class="o">.</span><span class="n">count</span><span class="p">)</span>
    <span class="n">state</span><span class="o">.</span><span class="n">activityFeed</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="nv">timestamp</span><span class="p">:</span> <span class="kt">Date</span><span class="p">(),</span> <span class="nv">type</span><span class="p">:</span> <span class="o">.</span><span class="nf">addedFavoritePrime</span><span class="p">(</span><span class="n">state</span><span class="o">.</span><span class="n">count</span><span class="p">)))</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="kd">enum</span> <span class="kt">FavoritePrimesAction</span> <span class="p">{</span>
  <span class="k">case</span> <span class="nf">deleteFavoritePrimes</span><span class="p">(</span><span class="kt">IndexSet</span><span class="p">)</span>
<span class="p">}</span>

<span class="kd">func</span> <span class="nf">favoritePrimesReducer</span><span class="p">(</span><span class="nv">state</span><span class="p">:</span> <span class="k">inout</span> <span class="kt">FavoritePrimesState</span><span class="p">,</span> <span class="nv">action</span><span class="p">:</span> <span class="kt">FavoritePrimesAction</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span> <span class="p">{</span>
  <span class="k">switch</span> <span class="n">action</span> <span class="p">{</span>
  <span class="k">case</span> <span class="kd">let</span> <span class="o">.</span><span class="nf">deleteFavoritePrimes</span><span class="p">(</span><span class="n">indexSet</span><span class="p">):</span>
    <span class="k">let</span> <span class="nv">removedPrimes</span> <span class="o">=</span> <span class="n">indexSet</span><span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">state</span><span class="o">.</span><span class="n">favoritePrimes</span><span class="p">[</span><span class="nv">$0</span><span class="p">]</span> <span class="p">}</span>
    <span class="n">state</span><span class="o">.</span><span class="n">favoritePrimes</span><span class="o">.</span><span class="nf">remove</span><span class="p">(</span><span class="nv">atOffsets</span><span class="p">:</span> <span class="n">indexSet</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">prime</span> <span class="k">in</span> <span class="n">removedPrimes</span> <span class="p">{</span>
      <span class="n">state</span><span class="o">.</span><span class="n">activityFeed</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="nv">timestamp</span><span class="p">:</span> <span class="kt">Date</span><span class="p">(),</span> <span class="nv">type</span><span class="p">:</span> <span class="o">.</span><span class="nf">removedFavoritePrime</span><span class="p">(</span><span class="n">prime</span><span class="p">)))</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">let</span> <span class="nv">appReducer</span><span class="p">:</span> <span class="p">(</span><span class="k">inout</span> <span class="kt">AppState</span><span class="p">,</span> <span class="kt">AppAction</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span> <span class="o">=</span> <span class="nf">combine</span><span class="p">(</span>
  <span class="nf">pullback</span><span class="p">(</span><span class="n">counterReducer</span><span class="p">,</span> <span class="nv">value</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="n">count</span><span class="p">,</span> <span class="nv">action</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="n">counter</span><span class="p">),</span>
  <span class="nf">pullback</span><span class="p">(</span><span class="n">primeModalReducer</span><span class="p">,</span> <span class="nv">value</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">action</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="n">primeModal</span><span class="p">),</span>
  <span class="nf">pullback</span><span class="p">(</span><span class="n">favoritePrimesReducer</span><span class="p">,</span> <span class="nv">value</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="n">favoritePrimesState</span><span class="p">,</span> <span class="nv">action</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="n">favoritePrimes</span><span class="p">)</span>
<span class="p">)</span>
</code></pre></div></div>

<p>到这一步，<code class="language-plaintext highlighter-rouge">counterReducer</code> 和 <code class="language-plaintext highlighter-rouge">favoritePrimesReducer</code> 完完全全地专注于自己所需的 State 和 Action，这就为后续的模块化提供了便利！<code class="language-plaintext highlighter-rouge">primeModalReducer</code> 仍需直接操作 <code class="language-plaintext highlighter-rouge">AppState</code>，所以这里使用 <code class="language-plaintext highlighter-rouge">\.self</code>。</p>

<h2 id="higher-order-reducer">Higher order reducer</h2>

<p>高阶函数（higher‑order function）的定义：接受函数/闭包作为参数，或返回函数/闭包。那么「高阶 reducer」到底长什么样？函数既把 reducer 当输入，又把 reducer 当输出，这意味着什么？事实上，我们已经遇到过了：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">combine</code> 是一个高阶 reducer：它接收多个 reducer 作为输入（只要它们处理的 state 和 action 类型一致），然后把每个 reducer 都跑一遍，返回一个全新的 reducer。</li>
  <li><code class="language-plaintext highlighter-rouge">pullback</code> 也是一个高阶 reducer：它接收一个 reducer 作为输入，并通过对局部 state 和 action 的 KeyPath 映射，返回一个作用在更全局 state 和 action 上的新 reducer。</li>
</ul>

<p>在下面这两个 reducer 中，你会发现他们在自己的 action 中，还额外多做了更新 activityFeed 的事情。要么在各自 action 枚举中新增更新 activityFeed 的 action，要么把这部分逻辑挪出去，交给另一个 reducer 处理。</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">favoritePrimesReducer</span><span class="p">(</span><span class="nv">state</span><span class="p">:</span> <span class="k">inout</span> <span class="kt">FavoritePrimesState</span><span class="p">,</span> <span class="nv">action</span><span class="p">:</span> <span class="kt">FavoritePrimesAction</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span> <span class="p">{</span>
  <span class="k">switch</span> <span class="n">action</span> <span class="p">{</span>
  <span class="k">case</span> <span class="kd">let</span> <span class="o">.</span><span class="nf">deleteFavoritePrimes</span><span class="p">(</span><span class="n">indexSet</span><span class="p">):</span>
    <span class="k">let</span> <span class="nv">removedPrimes</span> <span class="o">=</span> <span class="n">indexSet</span><span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">state</span><span class="o">.</span><span class="n">favoritePrimes</span><span class="p">[</span><span class="nv">$0</span><span class="p">]</span> <span class="p">}</span>
    <span class="n">state</span><span class="o">.</span><span class="n">favoritePrimes</span><span class="o">.</span><span class="nf">remove</span><span class="p">(</span><span class="nv">atOffsets</span><span class="p">:</span> <span class="n">indexSet</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">prime</span> <span class="k">in</span> <span class="n">removedPrimes</span> <span class="p">{</span>
      <span class="n">state</span><span class="o">.</span><span class="n">activityFeed</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span> <span class="c1">// 更新 activity feed</span>
        <span class="o">.</span><span class="nf">init</span><span class="p">(</span>
          <span class="nv">timestamp</span><span class="p">:</span> <span class="kt">Date</span><span class="p">(),</span>
          <span class="nv">type</span><span class="p">:</span> <span class="o">.</span><span class="nf">removedFavoritePrime</span><span class="p">(</span><span class="n">prime</span><span class="p">)</span>
        <span class="p">)</span>
      <span class="p">)</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="kd">func</span> <span class="nf">primeModalReducer</span><span class="p">(</span><span class="nv">state</span><span class="p">:</span> <span class="k">inout</span> <span class="kt">AppState</span><span class="p">,</span> <span class="nv">action</span><span class="p">:</span> <span class="kt">PrimeModalAction</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span> <span class="p">{</span>
  <span class="k">switch</span> <span class="n">action</span> <span class="p">{</span>
  <span class="k">case</span> <span class="o">.</span><span class="nv">removeFavoritePrimeTapped</span><span class="p">:</span>
    <span class="n">state</span><span class="o">.</span><span class="n">favoritePrimes</span><span class="o">.</span><span class="nf">removeAll</span><span class="p">(</span><span class="nv">where</span><span class="p">:</span> <span class="p">{</span> <span class="nv">$0</span> <span class="o">==</span> <span class="n">state</span><span class="o">.</span><span class="n">count</span> <span class="p">})</span>
    <span class="n">state</span><span class="o">.</span><span class="n">activityFeed</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span> <span class="c1">// 更新 activity feed</span>
      <span class="o">.</span><span class="nf">init</span><span class="p">(</span>
        <span class="nv">timestamp</span><span class="p">:</span> <span class="kt">Date</span><span class="p">(),</span>
        <span class="nv">type</span><span class="p">:</span> <span class="o">.</span><span class="nf">removedFavoritePrime</span><span class="p">(</span><span class="n">state</span><span class="o">.</span><span class="n">count</span><span class="p">)</span>
      <span class="p">)</span>
    <span class="p">)</span>

  <span class="k">case</span> <span class="o">.</span><span class="nv">saveFavoritePrimeTapped</span><span class="p">:</span>
    <span class="n">state</span><span class="o">.</span><span class="n">favoritePrimes</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="n">state</span><span class="o">.</span><span class="n">count</span><span class="p">)</span>
    <span class="n">state</span><span class="o">.</span><span class="n">activityFeed</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span> <span class="c1">// 更新 activity feed</span>
      <span class="o">.</span><span class="nf">init</span><span class="p">(</span>
        <span class="nv">timestamp</span><span class="p">:</span> <span class="kt">Date</span><span class="p">(),</span>
        <span class="nv">type</span><span class="p">:</span> <span class="o">.</span><span class="nf">addedFavoritePrime</span><span class="p">(</span><span class="n">state</span><span class="o">.</span><span class="n">count</span><span class="p">)</span>
      <span class="p">)</span>
    <span class="p">)</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>下面演示把这段更新逻辑挪到高阶 reducer 中，所以请理解为局部 reducer 不再写 <code class="language-plaintext highlighter-rouge">activityFeed</code>（否则会重复记录）。我们可以自定义一个高阶 reducer，名叫 <code class="language-plaintext highlighter-rouge">activityFeed</code>，放入更新 <code class="language-plaintext highlighter-rouge">activityFeed</code> 的逻辑。</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">activityFeed</span><span class="p">(</span>
  <span class="n">_</span> <span class="nv">reducer</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="k">inout</span> <span class="kt">AppState</span><span class="p">,</span> <span class="kt">AppAction</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="p">(</span><span class="k">inout</span> <span class="kt">AppState</span><span class="p">,</span> <span class="kt">AppAction</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span> <span class="p">{</span>

  <span class="k">return</span> <span class="p">{</span> <span class="n">state</span><span class="p">,</span> <span class="n">action</span> <span class="k">in</span>
    <span class="k">switch</span> <span class="n">action</span> <span class="p">{</span>
    <span class="k">case</span> <span class="o">.</span><span class="nv">counter</span><span class="p">:</span>
      <span class="k">break</span>

    <span class="k">case</span> <span class="o">.</span><span class="nf">primeModal</span><span class="p">(</span><span class="o">.</span><span class="n">removeFavoritePrimeTapped</span><span class="p">):</span>
      <span class="n">state</span><span class="o">.</span><span class="n">activityFeed</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span>
        <span class="o">.</span><span class="nf">init</span><span class="p">(</span>
          <span class="nv">timestamp</span><span class="p">:</span> <span class="kt">Date</span><span class="p">(),</span>
          <span class="nv">type</span><span class="p">:</span> <span class="o">.</span><span class="nf">removedFavoritePrime</span><span class="p">(</span><span class="n">state</span><span class="o">.</span><span class="n">count</span><span class="p">)</span>
        <span class="p">)</span>
      <span class="p">)</span>

    <span class="k">case</span> <span class="o">.</span><span class="nf">primeModal</span><span class="p">(</span><span class="o">.</span><span class="n">saveFavoritePrimeTapped</span><span class="p">):</span>
      <span class="n">state</span><span class="o">.</span><span class="n">activityFeed</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span>
        <span class="o">.</span><span class="nf">init</span><span class="p">(</span>
          <span class="nv">timestamp</span><span class="p">:</span> <span class="kt">Date</span><span class="p">(),</span>
          <span class="nv">type</span><span class="p">:</span> <span class="o">.</span><span class="nf">addedFavoritePrime</span><span class="p">(</span><span class="n">state</span><span class="o">.</span><span class="n">count</span><span class="p">)</span>
        <span class="p">)</span>
      <span class="p">)</span>

    <span class="k">case</span> <span class="kd">let</span> <span class="o">.</span><span class="nf">favoritePrimes</span><span class="p">(</span><span class="o">.</span><span class="nf">deleteFavoritePrimes</span><span class="p">(</span><span class="n">indexSet</span><span class="p">)):</span>
      <span class="k">let</span> <span class="nv">removedPrimes</span> <span class="o">=</span> <span class="n">indexSet</span><span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="n">state</span><span class="o">.</span><span class="n">favoritePrimes</span><span class="p">[</span><span class="nv">$0</span><span class="p">]</span> <span class="p">}</span>
      <span class="k">for</span> <span class="n">prime</span> <span class="k">in</span> <span class="n">removedPrimes</span> <span class="p">{</span>
        <span class="n">state</span><span class="o">.</span><span class="n">activityFeed</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span>
          <span class="o">.</span><span class="nf">init</span><span class="p">(</span>
            <span class="nv">timestamp</span><span class="p">:</span> <span class="kt">Date</span><span class="p">(),</span>
            <span class="nv">type</span><span class="p">:</span> <span class="o">.</span><span class="nf">removedFavoritePrime</span><span class="p">(</span><span class="n">prime</span><span class="p">)</span>
          <span class="p">)</span>
        <span class="p">)</span>
      <span class="p">}</span>
    <span class="p">}</span>

    <span class="nf">reducer</span><span class="p">(</span><span class="o">&amp;</span><span class="n">state</span><span class="p">,</span> <span class="n">action</span><span class="p">)</span>
  <span class="p">}</span>
<span class="p">}</span>


<span class="k">let</span> <span class="nv">appReducer</span><span class="p">:</span> <span class="p">(</span><span class="k">inout</span> <span class="kt">AppState</span><span class="p">,</span> <span class="kt">AppAction</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span> <span class="o">=</span> <span class="nf">combine</span><span class="p">(</span>
  <span class="nf">pullback</span><span class="p">(</span><span class="n">counterReducer</span><span class="p">,</span> <span class="nv">value</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="n">count</span><span class="p">,</span> <span class="nv">action</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="n">counter</span><span class="p">),</span>
  <span class="nf">pullback</span><span class="p">(</span><span class="n">primeModalReducer</span><span class="p">,</span> <span class="nv">value</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">action</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="n">primeModal</span><span class="p">),</span>
  <span class="nf">pullback</span><span class="p">(</span><span class="n">favoritePrimesReducer</span><span class="p">,</span> <span class="nv">value</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="n">favoritePrimesState</span><span class="p">,</span> <span class="nv">action</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="n">favoritePrimes</span><span class="p">)</span>
<span class="p">)</span>

<span class="k">let</span> <span class="nv">withActivityFeed</span> <span class="o">=</span> <span class="nf">activityFeed</span><span class="p">(</span><span class="n">appReducer</span><span class="p">)</span>

</code></pre></div></div>

<p>不难看出：高阶 reducer 的作用像一种 state 和 action 过滤器！在这样的架构下，打日志自然是方便许多的！</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="n">logging</span><span class="o">&lt;</span><span class="kt">Value</span><span class="p">,</span> <span class="kt">Action</span><span class="o">&gt;</span><span class="p">(</span>
  <span class="n">_</span> <span class="nv">reducer</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="k">inout</span> <span class="kt">Value</span><span class="p">,</span> <span class="kt">Action</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="p">(</span><span class="k">inout</span> <span class="kt">Value</span><span class="p">,</span> <span class="kt">Action</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span> <span class="p">{</span>
  <span class="k">return</span> <span class="p">{</span> <span class="n">value</span><span class="p">,</span> <span class="n">action</span> <span class="k">in</span>
    <span class="nf">reducer</span><span class="p">(</span><span class="o">&amp;</span><span class="n">value</span><span class="p">,</span> <span class="n">action</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="s">"Action: </span><span class="se">\(</span><span class="n">action</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="s">"State:"</span><span class="p">)</span>
    <span class="nf">dump</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="s">"---"</span><span class="p">)</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">let</span> <span class="nv">withLogging</span> <span class="o">=</span> <span class="nf">logging</span><span class="p">(</span><span class="nf">activityFeed</span><span class="p">(</span><span class="n">appReducer</span><span class="p">))</span>
</code></pre></div></div>

<h2 id="store">Store</h2>

<p>Reducer 定义了状态变化的核心逻辑，它本质上只是一个函数，那谁负责持有状态并分发出去呢？是 Store！Store 是运行时核心对象，持有当前状态，接收 Action，然后调用 reducer 处理 action、更新状态，并分发至 View。</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">final</span> <span class="kd">class</span> <span class="kt">Store</span><span class="o">&lt;</span><span class="kt">Value</span><span class="p">,</span> <span class="kt">Action</span><span class="o">&gt;</span><span class="p">:</span> <span class="kt">ObservableObject</span> <span class="p">{</span>
    <span class="kd">private</span> <span class="k">let</span> <span class="nv">reducer</span><span class="p">:</span> <span class="p">(</span><span class="k">inout</span> <span class="kt">Value</span><span class="p">,</span> <span class="kt">Action</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span>
    <span class="kd">@Published</span> <span class="kd">public</span> <span class="kd">private(set)</span> <span class="k">var</span> <span class="nv">value</span><span class="p">:</span> <span class="kt">Value</span>

    <span class="kd">public</span> <span class="nf">init</span><span class="p">(</span><span class="nv">initialValue</span><span class="p">:</span> <span class="kt">Value</span><span class="p">,</span> <span class="nv">reducer</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="k">inout</span> <span class="kt">Value</span><span class="p">,</span> <span class="kt">Action</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">self</span><span class="o">.</span><span class="n">reducer</span> <span class="o">=</span> <span class="n">reducer</span>
        <span class="k">self</span><span class="o">.</span><span class="n">value</span> <span class="o">=</span> <span class="n">initialValue</span>
    <span class="p">}</span>

    <span class="kd">public</span> <span class="kd">func</span> <span class="nf">send</span><span class="p">(</span><span class="n">_</span> <span class="nv">action</span><span class="p">:</span> <span class="kt">Action</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">self</span><span class="o">.</span><span class="nf">reducer</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="o">.</span><span class="n">value</span><span class="p">,</span> <span class="n">action</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="viewstate">ViewState</h2>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">IsPrimeModalView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
  <span class="kd">@ObservedObject</span> <span class="k">var</span> <span class="nv">store</span><span class="p">:</span> <span class="kt">Store</span><span class="o">&lt;</span><span class="kt">AppState</span><span class="p">,</span> <span class="kt">AppAction</span><span class="o">&gt;</span>

  <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kt">VStack</span> <span class="p">{</span>
      <span class="k">if</span> <span class="nf">isPrime</span><span class="p">(</span><span class="n">store</span><span class="o">.</span><span class="n">value</span><span class="o">.</span><span class="n">count</span><span class="p">)</span> <span class="p">{</span>
        <span class="kt">Text</span><span class="p">(</span><span class="s">"</span><span class="se">\(</span><span class="n">store</span><span class="o">.</span><span class="n">value</span><span class="o">.</span><span class="n">count</span><span class="se">)</span><span class="s"> is prime 🎉"</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">store</span><span class="o">.</span><span class="n">value</span><span class="o">.</span><span class="n">favoritePrimes</span><span class="o">.</span><span class="nf">contains</span><span class="p">(</span><span class="n">store</span><span class="o">.</span><span class="n">value</span><span class="o">.</span><span class="n">count</span><span class="p">)</span> <span class="p">{</span>
          <span class="kt">Button</span><span class="p">(</span><span class="s">"Remove from favorite primes"</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">store</span><span class="o">.</span><span class="nf">send</span><span class="p">(</span><span class="o">.</span><span class="nf">primeModal</span><span class="p">(</span><span class="o">.</span><span class="n">removeFavoritePrimeTapped</span><span class="p">))</span>
          <span class="p">}</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
          <span class="kt">Button</span><span class="p">(</span><span class="s">"Save to favorite primes"</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">store</span><span class="o">.</span><span class="nf">send</span><span class="p">(</span><span class="o">.</span><span class="nf">primeModal</span><span class="p">(</span><span class="o">.</span><span class="n">saveFavoritePrimeTapped</span><span class="p">))</span>
            <span class="n">store</span><span class="o">.</span><span class="nf">send</span><span class="p">(</span><span class="o">.</span><span class="nf">counter</span><span class="p">(</span><span class="o">.</span><span class="n">decrTapped</span><span class="p">))</span> <span class="c1">// ? 易错！发送不属于自己应该关注的 action</span>
          <span class="p">}</span>
        <span class="p">}</span>
      <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="kt">Text</span><span class="p">(</span><span class="s">"</span><span class="se">\(</span><span class="n">store</span><span class="o">.</span><span class="n">value</span><span class="o">.</span><span class="n">count</span><span class="se">)</span><span class="s"> is not prime :("</span><span class="p">)</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="o">...</span>
    <span class="o">.</span><span class="nf">sheet</span><span class="p">(</span><span class="nv">isPresented</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">$isPrimeModalShown</span><span class="p">)</span> <span class="p">{</span>
      <span class="kt">IsPrimeModalView</span><span class="p">(</span><span class="nv">store</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">store</span><span class="p">)</span>
    <span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">IsPrimeModalView</code> 中持有了全局的 store，很容易发送不属于自己应该关注的 action。如果能把 action 限定为 <code class="language-plaintext highlighter-rouge">PrimeModalAction</code>，这样在编译时就能发现问题所在了！再者，向 <code class="language-plaintext highlighter-rouge">IsPrimeModalView</code> 暴露了全局状态，View 上压根用不上这么多状态，同时也阻碍了模块的拆分。</p>

<p>与 reducer 全局 state、全局 action 的拆分类似，对于 Store 而言，也需要将其拆分为聚焦于局部 state 和局部 action 的局部 store！</p>

<h3 id="简化-state-1">简化 State</h3>

<p>我们可以这样做 <code class="language-plaintext highlighter-rouge">(Value) -&gt; LocalValue</code> 的变换：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">func</span> <span class="n">map</span><span class="o">&lt;</span><span class="kt">LocalValue</span><span class="o">&gt;</span><span class="p">(</span><span class="n">_</span> <span class="nv">f</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="kt">Value</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">LocalValue</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Store</span><span class="o">&lt;</span><span class="kt">LocalValue</span><span class="p">,</span> <span class="kt">Action</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="kt">Store</span><span class="o">&lt;</span><span class="kt">LocalValue</span><span class="p">,</span> <span class="kt">Action</span><span class="o">&gt;</span><span class="p">(</span>
      <span class="nv">initialValue</span><span class="p">:</span> <span class="nf">f</span><span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="n">value</span><span class="p">),</span>
      <span class="nv">reducer</span><span class="p">:</span> <span class="p">{</span> <span class="n">localValue</span><span class="p">,</span> <span class="n">action</span> <span class="k">in</span>
        <span class="k">self</span><span class="o">.</span><span class="nf">send</span><span class="p">(</span><span class="n">action</span><span class="p">)</span>
        <span class="k">let</span> <span class="nv">updatedLocalValue</span> <span class="o">=</span> <span class="nf">f</span><span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="n">value</span><span class="p">)</span>
        <span class="n">localValue</span> <span class="o">=</span> <span class="n">updatedLocalValue</span>
      <span class="p">}</span>
    <span class="p">)</span>
  <span class="p">}</span>
</code></pre></div></div>

<p>那我们新建一个 playground 来看下运行效果：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">store</span> <span class="o">=</span> <span class="kt">Store</span><span class="o">&lt;</span><span class="kt">Int</span><span class="p">,</span> <span class="kt">Void</span><span class="o">&gt;</span><span class="p">(</span><span class="nv">initialValue</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="nv">reducer</span><span class="p">:</span> <span class="p">{</span> <span class="n">count</span><span class="p">,</span> <span class="n">_</span> <span class="k">in</span>
  <span class="n">count</span> <span class="o">+=</span> <span class="mi">1</span>
<span class="p">})</span>

<span class="n">store</span><span class="o">.</span><span class="nf">send</span><span class="p">(())</span> <span class="c1">// value: 1</span>
<span class="n">store</span><span class="o">.</span><span class="nf">send</span><span class="p">(())</span> <span class="c1">// value: 2</span>
<span class="n">store</span><span class="o">.</span><span class="nf">send</span><span class="p">(())</span> <span class="c1">// value: 3</span>
<span class="n">store</span><span class="o">.</span><span class="nf">send</span><span class="p">(())</span> <span class="c1">// value: 4</span>
<span class="n">store</span><span class="o">.</span><span class="nf">send</span><span class="p">(())</span> <span class="c1">// value: 5</span>

<span class="n">store</span><span class="o">.</span><span class="n">value</span> <span class="c1">// value: 5</span>

<span class="k">let</span> <span class="nv">newStore</span> <span class="o">=</span> <span class="n">store</span><span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="nv">$0</span> <span class="p">}</span>

<span class="n">newStore</span><span class="o">.</span><span class="n">value</span> <span class="c1">// new value: 5</span>
<span class="n">newStore</span><span class="o">.</span><span class="nf">send</span><span class="p">(())</span> <span class="c1">// new value: 6</span>
<span class="n">newStore</span><span class="o">.</span><span class="nf">send</span><span class="p">(())</span> <span class="c1">// new value: 7</span>
<span class="n">newStore</span><span class="o">.</span><span class="nf">send</span><span class="p">(())</span> <span class="c1">// new value: 8</span>
<span class="n">newStore</span><span class="o">.</span><span class="n">value</span> <span class="c1">// new value: 8</span>

<span class="n">store</span><span class="o">.</span><span class="n">value</span> <span class="c1">// value: 8</span>
</code></pre></div></div>

<p>通过运行结果可以看出：<code class="language-plaintext highlighter-rouge">newStore</code> 收到的 action 不光会更新自己的 value，还会同步更新至 store。因为 map 创建的 <code class="language-plaintext highlighter-rouge">newStore</code> 不是独立的「副本」，而是转发到原始 store 的「映射」：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">newStore.send(())</code> 会执行它的 reducer。</li>
  <li>这个 reducer 里第一句是 <code class="language-plaintext highlighter-rouge">self.send(action)</code>，也就是把 action 发送给原始 store。</li>
  <li>原始 store 的 reducer 会把 value 加 1。</li>
  <li>然后 <code class="language-plaintext highlighter-rouge">newStore</code> 再用 <code class="language-plaintext highlighter-rouge">f(self.value)</code> 把原始 store 的最新值同步到自己的 localValue。</li>
</ul>

<p>所以 <code class="language-plaintext highlighter-rouge">newStore</code> 的每次 <code class="language-plaintext highlighter-rouge">send</code> 都会更新原始 store.value，因此最后原始 value 也变成 8。</p>

<p>这其实正是我们想要的效果：局部 store 收到 action 后产生的状态变化能正确投射到全局 store 上！</p>

<p>那反过来呢？全局 store 的状态没法与局部 store 的状态同步！</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">store</span><span class="o">.</span><span class="n">value</span> <span class="c1">// value: 8</span>

<span class="n">store</span><span class="o">.</span><span class="nf">send</span><span class="p">(())</span> <span class="c1">// value: 9</span>
<span class="n">store</span><span class="o">.</span><span class="nf">send</span><span class="p">(())</span> <span class="c1">// value: 10</span>
<span class="n">store</span><span class="o">.</span><span class="nf">send</span><span class="p">(())</span> <span class="c1">// value: 11</span>

<span class="n">newStore</span><span class="o">.</span><span class="n">value</span> <span class="c1">// new value: 8</span>
<span class="n">store</span><span class="o">.</span><span class="n">value</span> <span class="c1">// value: 11</span>
</code></pre></div></div>

<p>现在的局部 store 只能把自己的动作转发给全局 store（因为它直接调用了全局 store 的 <code class="language-plaintext highlighter-rouge">send</code>），所以能把变化传上去；但它没有办法从全局 store 接收后续的状态更新。如果局部 store 能监听全局 store 的状态变化，就可以把全局的新状态同步到局部状态，从而保持一致。</p>

<p>在 Swift 中，<code class="language-plaintext highlighter-rouge">@Published</code> 是 Combine 的属性包装器，它会生成一个投影值 <code class="language-plaintext highlighter-rouge">projectedValue</code>，用 <code class="language-plaintext highlighter-rouge">$</code> 访问。这个投影值的类型是 <code class="language-plaintext highlighter-rouge">Published&lt;Value&gt;.Publisher</code>，它遵循 <code class="language-plaintext highlighter-rouge">Publisher</code> 协议，所以 <code class="language-plaintext highlighter-rouge">$value</code> 本质上就是一个 <code class="language-plaintext highlighter-rouge">Publisher</code>。局部 store 用 <code class="language-plaintext highlighter-rouge">self.$value.sink</code> 订阅全局 value 的发布流。每当全局 value 变化，就通过 <code class="language-plaintext highlighter-rouge">f</code> 映射成 <code class="language-plaintext highlighter-rouge">LocalValue</code>，并更新局部 <code class="language-plaintext highlighter-rouge">localStore.value</code>，最后把订阅存到 <code class="language-plaintext highlighter-rouge">localStore.cancellable</code>，保证监听持续存在。</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">func</span> <span class="n">map</span><span class="o">&lt;</span><span class="kt">LocalValue</span><span class="o">&gt;</span><span class="p">(</span>
    <span class="n">_</span> <span class="nv">f</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="kt">Value</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">LocalValue</span>
  <span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Store</span><span class="o">&lt;</span><span class="kt">LocalValue</span><span class="p">,</span> <span class="kt">Action</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">localStore</span> <span class="o">=</span> <span class="kt">Store</span><span class="o">&lt;</span><span class="kt">LocalValue</span><span class="p">,</span> <span class="kt">Action</span><span class="o">&gt;</span><span class="p">(</span>
      <span class="nv">initialValue</span><span class="p">:</span> <span class="nf">f</span><span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="n">value</span><span class="p">),</span>
      <span class="nv">reducer</span><span class="p">:</span> <span class="p">{</span> <span class="n">localValue</span><span class="p">,</span> <span class="n">action</span> <span class="k">in</span>
        <span class="k">self</span><span class="o">.</span><span class="nf">send</span><span class="p">(</span><span class="n">action</span><span class="p">)</span>
        <span class="n">localValue</span> <span class="o">=</span> <span class="nf">f</span><span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="n">value</span><span class="p">)</span>
      <span class="p">}</span>
    <span class="p">)</span>
    <span class="n">localStore</span><span class="o">.</span><span class="n">cancellable</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="n">$value</span><span class="o">.</span><span class="n">sink</span> <span class="p">{</span> <span class="p">[</span><span class="k">weak</span> <span class="n">localStore</span><span class="p">]</span> <span class="n">newValue</span> <span class="k">in</span>
      <span class="n">localStore</span><span class="p">?</span><span class="o">.</span><span class="n">value</span> <span class="o">=</span> <span class="nf">f</span><span class="p">(</span><span class="n">newValue</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">localStore</span>
  <span class="p">}</span>
</code></pre></div></div>

<h3 id="简化-action-1">简化 Action</h3>

<p>类似地，如果我们知道 <code class="language-plaintext highlighter-rouge">f: @escaping (LocalAction) -&gt; Action</code> 这样的变换，就可以把 <code class="language-plaintext highlighter-rouge">Store&lt;Value, Action&gt;</code> 变为 <code class="language-plaintext highlighter-rouge">Store&lt;Value, LocalAction&gt;</code>。</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kd">public</span> <span class="kd">func</span> <span class="n">map</span><span class="o">&lt;</span><span class="kt">LocalAction</span><span class="o">&gt;</span><span class="p">(</span>
    <span class="nv">f</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="kt">LocalAction</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Action</span>
  <span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Store</span><span class="o">&lt;</span><span class="kt">Value</span><span class="p">,</span> <span class="kt">LocalAction</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="kt">Store</span><span class="o">&lt;</span><span class="kt">Value</span><span class="p">,</span> <span class="kt">LocalAction</span><span class="o">&gt;</span><span class="p">(</span>
      <span class="nv">initialValue</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">value</span><span class="p">,</span>
      <span class="nv">reducer</span><span class="p">:</span> <span class="p">{</span> <span class="n">value</span><span class="p">,</span> <span class="n">localAction</span> <span class="k">in</span>
        <span class="k">self</span><span class="o">.</span><span class="nf">send</span><span class="p">(</span><span class="nf">f</span><span class="p">(</span><span class="n">localAction</span><span class="p">))</span>
        <span class="n">value</span> <span class="o">=</span> <span class="k">self</span><span class="o">.</span><span class="n">value</span>
      <span class="p">}</span>
    <span class="p">)</span>
  <span class="p">}</span>
</code></pre></div></div>

<h3 id="合并两种-map">合并两种 map</h3>

<p>并改名为 <code class="language-plaintext highlighter-rouge">view</code>，因为该方法返回的不是一个「独立的新 store」，而是原 store 的一个投影！通过 <code class="language-plaintext highlighter-rouge">toLocalValue</code> 把全局 Value 映射成局部 <code class="language-plaintext highlighter-rouge">LocalValue</code>，局部 store 只看到「局部视角」的状态。局部 store 只能发送 <code class="language-plaintext highlighter-rouge">LocalAction</code>，再被 <code class="language-plaintext highlighter-rouge">toGlobalAction</code> 转成全局 Action。也就是说，局部 store 只能发一个局部动作集合。数据源仍是全局 value。局部 store 订阅 <code class="language-plaintext highlighter-rouge">$value</code>，始终跟随全局 state 更新；它不拥有自己的状态源。</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="kd">public</span> <span class="kd">func</span> <span class="n">view</span><span class="o">&lt;</span><span class="kt">LocalValue</span><span class="p">,</span> <span class="kt">LocalAction</span><span class="o">&gt;</span><span class="p">(</span>
        <span class="n">value</span> <span class="nv">toLocalValue</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="kt">Value</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">LocalValue</span><span class="p">,</span>
        <span class="n">action</span> <span class="nv">toGlobalAction</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="kt">LocalAction</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Action</span>
    <span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Store</span><span class="o">&lt;</span><span class="kt">LocalValue</span><span class="p">,</span> <span class="kt">LocalAction</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">localStore</span> <span class="o">=</span> <span class="kt">Store</span><span class="o">&lt;</span><span class="kt">LocalValue</span><span class="p">,</span> <span class="kt">LocalAction</span><span class="o">&gt;</span><span class="p">(</span>
            <span class="nv">initialValue</span><span class="p">:</span> <span class="nf">toLocalValue</span><span class="p">(</span><span class="n">value</span><span class="p">),</span>
            <span class="nv">reducer</span><span class="p">:</span> <span class="p">{</span> <span class="n">localValue</span><span class="p">,</span> <span class="n">localAction</span> <span class="k">in</span>
                <span class="k">self</span><span class="o">.</span><span class="nf">send</span><span class="p">(</span><span class="nf">toGlobalAction</span><span class="p">(</span><span class="n">localAction</span><span class="p">))</span>
                <span class="n">localValue</span> <span class="o">=</span> <span class="nf">toLocalValue</span><span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="n">value</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">)</span>
        <span class="n">localStore</span><span class="o">.</span><span class="n">cancellable</span> <span class="o">=</span> <span class="n">$value</span><span class="o">.</span><span class="n">sink</span> <span class="p">{</span> <span class="p">[</span><span class="k">weak</span> <span class="n">localStore</span><span class="p">]</span> <span class="n">newValue</span> <span class="k">in</span>
            <span class="n">localStore</span><span class="p">?</span><span class="o">.</span><span class="n">value</span> <span class="o">=</span> <span class="nf">toLocalValue</span><span class="p">(</span><span class="n">newValue</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="n">localStore</span>
    <span class="p">}</span>
</code></pre></div></div>

<p>回顾一下现在 Store 的代码：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">final</span> <span class="kd">class</span> <span class="kt">Store</span><span class="o">&lt;</span><span class="kt">Value</span><span class="p">,</span> <span class="kt">Action</span><span class="o">&gt;</span><span class="p">:</span> <span class="kt">ObservableObject</span> <span class="p">{</span>
    <span class="kd">private</span> <span class="k">let</span> <span class="nv">reducer</span><span class="p">:</span> <span class="p">(</span><span class="k">inout</span> <span class="kt">Value</span><span class="p">,</span> <span class="kt">Action</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span>
    <span class="kd">@Published</span> <span class="kd">public</span> <span class="kd">private(set)</span> <span class="k">var</span> <span class="nv">value</span><span class="p">:</span> <span class="kt">Value</span>
    <span class="kd">private</span> <span class="k">var</span> <span class="nv">cancellable</span><span class="p">:</span> <span class="kt">Cancellable</span><span class="p">?</span>

    <span class="kd">public</span> <span class="nf">init</span><span class="p">(</span><span class="nv">initialValue</span><span class="p">:</span> <span class="kt">Value</span><span class="p">,</span> <span class="nv">reducer</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="k">inout</span> <span class="kt">Value</span><span class="p">,</span> <span class="kt">Action</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">self</span><span class="o">.</span><span class="n">reducer</span> <span class="o">=</span> <span class="n">reducer</span>
        <span class="k">self</span><span class="o">.</span><span class="n">value</span> <span class="o">=</span> <span class="n">initialValue</span>
    <span class="p">}</span>

    <span class="kd">public</span> <span class="kd">func</span> <span class="nf">send</span><span class="p">(</span><span class="n">_</span> <span class="nv">action</span><span class="p">:</span> <span class="kt">Action</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">self</span><span class="o">.</span><span class="nf">reducer</span><span class="p">(</span><span class="o">&amp;</span><span class="k">self</span><span class="o">.</span><span class="n">value</span><span class="p">,</span> <span class="n">action</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="kd">public</span> <span class="kd">func</span> <span class="n">view</span><span class="o">&lt;</span><span class="kt">LocalValue</span><span class="p">,</span> <span class="kt">LocalAction</span><span class="o">&gt;</span><span class="p">(</span>
        <span class="n">value</span> <span class="nv">toLocalValue</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="kt">Value</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">LocalValue</span><span class="p">,</span>
        <span class="n">action</span> <span class="nv">toGlobalAction</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="kt">LocalAction</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Action</span>
    <span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Store</span><span class="o">&lt;</span><span class="kt">LocalValue</span><span class="p">,</span> <span class="kt">LocalAction</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">localStore</span> <span class="o">=</span> <span class="kt">Store</span><span class="o">&lt;</span><span class="kt">LocalValue</span><span class="p">,</span> <span class="kt">LocalAction</span><span class="o">&gt;</span><span class="p">(</span>
            <span class="nv">initialValue</span><span class="p">:</span> <span class="nf">toLocalValue</span><span class="p">(</span><span class="n">value</span><span class="p">),</span>
            <span class="nv">reducer</span><span class="p">:</span> <span class="p">{</span> <span class="n">localValue</span><span class="p">,</span> <span class="n">localAction</span> <span class="k">in</span>
                <span class="k">self</span><span class="o">.</span><span class="nf">send</span><span class="p">(</span><span class="nf">toGlobalAction</span><span class="p">(</span><span class="n">localAction</span><span class="p">))</span>
                <span class="n">localValue</span> <span class="o">=</span> <span class="nf">toLocalValue</span><span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="n">value</span><span class="p">)</span>
            <span class="p">}</span>
        <span class="p">)</span>
        <span class="n">localStore</span><span class="o">.</span><span class="n">cancellable</span> <span class="o">=</span> <span class="n">$value</span><span class="o">.</span><span class="n">sink</span> <span class="p">{</span> <span class="p">[</span><span class="k">weak</span> <span class="n">localStore</span><span class="p">]</span> <span class="n">newValue</span> <span class="k">in</span>
            <span class="n">localStore</span><span class="p">?</span><span class="o">.</span><span class="n">value</span> <span class="o">=</span> <span class="nf">toLocalValue</span><span class="p">(</span><span class="n">newValue</span><span class="p">)</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="n">localStore</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>以本节开头提到的 <code class="language-plaintext highlighter-rouge">IsPrimeModalView</code> 为例，让我们看看如何修改：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">typealias</span> <span class="kt">PrimeModalState</span> <span class="o">=</span> <span class="p">(</span><span class="nv">count</span><span class="p">:</span> <span class="kt">Int</span><span class="p">,</span> <span class="nv">favoritePrimes</span><span class="p">:</span> <span class="p">[</span><span class="kt">Int</span><span class="p">])</span>

<span class="kd">public</span> <span class="kd">struct</span> <span class="kt">IsPrimeModalView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
  <span class="kd">@ObservedObject</span> <span class="k">var</span> <span class="nv">store</span><span class="p">:</span> <span class="kt">Store</span><span class="o">&lt;</span><span class="kt">PrimeModalState</span><span class="p">,</span> <span class="kt">PrimeModalAction</span><span class="o">&gt;</span>

  <span class="kd">public</span> <span class="nf">init</span><span class="p">(</span><span class="nv">store</span><span class="p">:</span> <span class="kt">Store</span><span class="o">&lt;</span><span class="kt">PrimeModalState</span><span class="p">,</span> <span class="kt">PrimeModalAction</span><span class="o">&gt;</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">self</span><span class="o">.</span><span class="n">store</span> <span class="o">=</span> <span class="n">store</span>
  <span class="p">}</span>

  <span class="kd">public</span> <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kt">VStack</span> <span class="p">{</span>
      <span class="k">if</span> <span class="nf">isPrime</span><span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="n">store</span><span class="o">.</span><span class="n">value</span><span class="o">.</span><span class="n">count</span><span class="p">)</span> <span class="p">{</span>
        <span class="kt">Text</span><span class="p">(</span><span class="s">"</span><span class="se">\(</span><span class="k">self</span><span class="o">.</span><span class="n">store</span><span class="o">.</span><span class="n">value</span><span class="o">.</span><span class="n">count</span><span class="se">)</span><span class="s"> is prime 🎉"</span><span class="p">)</span>
        <span class="k">if</span> <span class="k">self</span><span class="o">.</span><span class="n">store</span><span class="o">.</span><span class="n">value</span><span class="o">.</span><span class="n">favoritePrimes</span><span class="o">.</span><span class="nf">contains</span><span class="p">(</span><span class="k">self</span><span class="o">.</span><span class="n">store</span><span class="o">.</span><span class="n">value</span><span class="o">.</span><span class="n">count</span><span class="p">)</span> <span class="p">{</span>
          <span class="kt">Button</span><span class="p">(</span><span class="s">"Remove from favorite primes"</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">self</span><span class="o">.</span><span class="n">store</span><span class="o">.</span><span class="nf">send</span><span class="p">(</span><span class="o">.</span><span class="n">removeFavoritePrimeTapped</span><span class="p">)</span>
          <span class="p">}</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
          <span class="kt">Button</span><span class="p">(</span><span class="s">"Save to favorite primes"</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">self</span><span class="o">.</span><span class="n">store</span><span class="o">.</span><span class="nf">send</span><span class="p">(</span><span class="o">.</span><span class="n">saveFavoritePrimeTapped</span><span class="p">)</span>
          <span class="p">}</span>
        <span class="p">}</span>
      <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="kt">Text</span><span class="p">(</span><span class="s">"</span><span class="se">\(</span><span class="k">self</span><span class="o">.</span><span class="n">store</span><span class="o">.</span><span class="n">value</span><span class="o">.</span><span class="n">count</span><span class="se">)</span><span class="s"> is not prime :("</span><span class="p">)</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>


<span class="o">...</span>
    <span class="o">.</span><span class="nf">sheet</span><span class="p">(</span><span class="nv">isPresented</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">$isPrimeModalShown</span><span class="p">)</span> <span class="p">{</span>
        <span class="kt">IsPrimeModalView</span><span class="p">(</span>
            <span class="nv">store</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">store</span>
                <span class="o">.</span><span class="nf">view</span><span class="p">(</span> <span class="c1">// 投射出一个局部 Store</span>
                    <span class="nv">value</span><span class="p">:</span> <span class="p">{</span> <span class="p">(</span><span class="nv">$0</span><span class="o">.</span><span class="n">count</span><span class="p">,</span> <span class="nv">$0</span><span class="o">.</span><span class="n">favoritePrimes</span><span class="p">)</span> <span class="p">},</span>
                    <span class="nv">action</span><span class="p">:</span> <span class="p">{</span> <span class="o">.</span><span class="nf">primeModal</span><span class="p">(</span><span class="nv">$0</span><span class="p">)</span> <span class="p">}</span>
                <span class="p">)</span>
        <span class="p">)</span>
    <span class="p">}</span>
</code></pre></div></div>

<h2 id="结语">结语</h2>

<p>叽里咕噜说了一大堆，第一部分就写到这里吧，目前为止展示的示例代码可在<a href="https://github.com/pointfreeco/episode-code-samples/blob/main/0074-modular-state-management-view-actions/PrimeTime/ComposableArchitecture/ComposableArchitecture.swift">这里看</a>。</p>

<p>下次介绍 reducer 在收到 action 后还可以执行一些副作用（Side Effects）。比如说，我收到一个 action，需要发送一个网络请求，等网络请求结束后，根据结果再执行不同的 action。</p>]]></content><author><name>Sunset Wan</name></author><summary type="html"><![CDATA[Reducer public enum CounterAction { case decrTapped case incrTapped } func counterReducer( state: AppState, action: CounterAction ) -&gt; AppState { var copy = state switch action { case .decrTapped: copy.count -= 1 case .incrTapped: copy.count += 1 } return copy } 上面的代码是个标准的 reducer 结构：当收到 incrTapped 时，修改全局状态（计数加一），再返回新的全局状态。为什么称这样的结构为 reducer？]]></summary></entry><entry><title type="html">WebSocket 协议学习笔记</title><link href="https://sunsetwan.github.io/2026/01/25/WebSocket-note/" rel="alternate" type="text/html" title="WebSocket 协议学习笔记" /><published>2026-01-25T10:48:23+08:00</published><updated>2026-01-25T10:48:23+08:00</updated><id>https://sunsetwan.github.io/2026/01/25/WebSocket-note</id><content type="html" xml:base="https://sunsetwan.github.io/2026/01/25/WebSocket-note/"><![CDATA[<p>工作项目用了 Socket.IO 来做客户端与服务端的实时通信，我看了 Socket.IO 是基于 WebSocket 协议进一步封装的。于是，我希望通过实现 WebSocket <strong>客户端</strong> 的 <strong>一小部分</strong> 功能，以达到了解 WebSocket 协议的目标！</p>

<ol>
  <li>教科书：<a href="https://www.rfc-editor.org/rfc/rfc6455.txt">RFC 6455</a></li>
  <li>老师：Codex</li>
  <li>代码仓库地址：<a href="https://github.com/SunsetWan/WebSocketClient-Demo/tree/main/Sources/LearnWebSocket">WebSocketClient-Demo</a><br />
<!-- more --></li>
</ol>

<h2 id="websocket-协议简介">WebSocket 协议简介</h2>

<p>WebSocket 协议是一种能让客户端（浏览器）与服务端进行双向通信的协议。该协议由基于传输控制协议（Transmission Control Protocol, TCP）的打开握手与后续的基本消息成帧组成。</p>

<h3 id="建立连接时的握手阶段">建立连接时的握手阶段</h3>

<h4 id="客户端发起握手">客户端发起握手</h4>

<p>WebSocket 在握手阶段是利用 HTTP GET 请求来与服务端建立连接的，但对于 HTTP 有版本要求：至少大于等于 1.1。</p>

<p>常见的客户端发起握手请求头如下：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>        GET /chat HTTP/1.1
        Host: server.example.com
        Upgrade: websocket
        Connection: Upgrade
        Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
        Sec-WebSocket-Version: 13
</code></pre></div></div>

<p>以下是握手中必须包含的关键头字段：<code class="language-plaintext highlighter-rouge">Upgrade</code>，<code class="language-plaintext highlighter-rouge">Connection</code>，<code class="language-plaintext highlighter-rouge">Sec-WebSocket-Key</code>，<code class="language-plaintext highlighter-rouge">Sec-WebSocket-Version</code>。且根据协议的定义：</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">Upgrade: websocket</code></li>
  <li><code class="language-plaintext highlighter-rouge">Connection: Upgrade</code></li>
  <li><code class="language-plaintext highlighter-rouge">Sec-WebSocket-Version: 13</code></li>
</ol>

<p>这三组键值对是固定的（大小写不敏感）。</p>

<h5 id="如何计算出-sec-websocket-key">如何计算出 Sec-WebSocket-Key？</h5>

<p>首先，设客户端随机生成 16 个字节长度的随机数为 A，再对 A 做 Base64 编码，得出的结果就是该 Key 对应的 value。客户端每次开始握手都得重新生成一次，不可重复。</p>

<h5 id="为什么需要-sec-websocket-key">为什么需要 Sec-WebSocket-Key？</h5>

<p>它用于让服务端根据该随机值生成 <code class="language-plaintext highlighter-rouge">Sec-WebSocket-Accept</code>，客户端再进行校验，从而确认对端确实理解并接受 WebSocket 握手。</p>

<h5 id="实现客户端握手中产生的思考">实现客户端握手中产生的思考</h5>

<p>因为是基于 <code class="language-plaintext highlighter-rouge">NWConnection</code> <a href="https://github.com/SunsetWan/WebSocketClient-Demo/blob/main/Sources/LearnWebSocket/TransportConnection.swift#L41">利用 TCP 协议直接发送</a> GET 请求头，所以我们得自己 <a href="https://github.com/SunsetWan/WebSocketClient-Demo/blob/main/Sources/LearnWebSocket/WebSocketClient.swift#L277">手动调整字符串</a> 构造 Data，以遵循 HTTP 请求头的格式。这一开始把我难到了，意识到还需多复习 HTTP 协议。</p>

<h4 id="服务端响应握手">服务端响应握手</h4>

<p>服务端会阅读客户端请求头，并检查是否符合以下几点：</p>

<ol>
  <li>GET 请求且 HTTP 版本至少为 1.1</li>
  <li>得有键值对 Upgrade: websocket（大小写不敏感）</li>
  <li>得有键值对 Connection: Upgrade（大小写不敏感）</li>
  <li>得有键值对 Sec-WebSocket-Version: 13（大小写不敏感）</li>
  <li>对 <code class="language-plaintext highlighter-rouge">Sec-WebSocket-Key</code> 作 Base64 decode，检查长度得是 16 个字节</li>
</ol>

<p>如果以上的检查通过且可以创建连接，服务端会发出 HTTP Response：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>        HTTP/1.1 101 Switching Protocols
        Upgrade: websocket
        Connection: Upgrade
        Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
</code></pre></div></div>

<h5 id="如何计算-sec-websocket-accept">如何计算 Sec-WebSocket-Accept？</h5>

<p>具体实现可见 <a href="https://github.com/SunsetWan/WebSocketClient-Demo/blob/main/Sources/LearnWebSocket/Handshake.swift">Handshake.swift</a>。</p>

<h6 id="拼接字符串">拼接字符串</h6>

<p>首先获取到客户端发来的 Sec-WebSocket-Key（dGhlIHNhbXBsZSBub25jZQ==），拼接上这段协议中定义好的 GUID（全局唯一标识符）：258EAFA5-E914-47DA-95CA-C5AB0DC85B11。</p>

<p>结果为 dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11</p>

<h6 id="对拼接后的字符串做-sha1">对拼接后的字符串做 SHA‑1</h6>

<p>结果是 20 字节（160 位）的散列值：b3 7a 4f 2c c0 62 4f 16 90 f6 46 06 cf 38 59 45 b2 be c4 ea</p>

<h6 id="把-sha1-结果做-base64-编码">把 SHA‑1 结果做 Base64 编码</h6>

<p>结果为 s3pPLMBiTxaQ9kYGzzhZRbK+xOo=。客户端收到这个 accept key 后，还会自行校验一遍。如果与自己算出来的不一致，会导致握手失败。</p>

<h3 id="解析数据帧data-frame">解析数据帧（Data Frame）</h3>

<p>具体实现可见 <a href="https://github.com/SunsetWan/WebSocketClient-Demo/blob/main/Sources/LearnWebSocket/DataFrameParser.swift">DataFrameParser.swift</a>。</p>

<p>在握手成功后，双端严格以数据帧格式通信，格式如下图：</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2026/websocket_note/websocket_note_1.png" alt="WebSocket 数据帧中 FIN、RSV、opcode、MASK、长度和载荷字段的位布局" /></p>

<h4 id="头部结构2-个字节">头部结构（2 个字节）</h4>

<p>第 1 个字节：FIN（Final，是否是消息最后一个分片）、RSV1/RSV2/RSV3（Reserved，保留位）、opcode（操作码，4 位）。</p>

<p>FIN 为 1 表示这是消息的最后一个分片；0 表示后续还有分片。</p>

<p>RSV1/RSV2/RSV3：除非协商了扩展，否则必须为 0；否则接收方必须关闭连接。</p>

<p>opcode：</p>

<ul>
  <li>0x0 续帧（Continuation）</li>
  <li>0x1 文本帧（Text）</li>
  <li>0x2 二进制帧（Binary）</li>
  <li>0x3–0x7 预留给未来非控制帧</li>
  <li>0x8 关闭（Close）</li>
  <li>0x9 Ping</li>
  <li>0xA Pong</li>
  <li>0xB–0xF 预留给未来控制帧</li>
</ul>

<p>第 2 个字节：MASK（是否掩码）+ Payload len（负载长度，7 比特）。客户端向服务端发送的数据帧必须进行 masking，故 MASK 为 1，服务端向客户端发送的数据帧不进行 masking，故 MASK 为 0。</p>

<h4 id="payload-data">Payload Data</h4>

<p>若 Payload len 为 126：后面跟 2 字节扩展长度（16 位无符号整数）。若 Payload len 为 127：后面跟 8 字节扩展长度（64 位无符号整数，最高位必须为 0）。扩展长度使用网络字节序（network byte order，大端序）。若 MASK 为 1：后面跟 4 字节 Masking-key（掩码键）。最后是 Payload Data（负载数据）：由 Extension data（扩展数据）+ Application data（应用数据）拼接组成。Payload Data 的总长度等于负载长度；其中扩展数据长度由扩展协议定义，未协商扩展时为 0。</p>

<h4 id="特殊的数据帧控制帧control-frame">特殊的数据帧——控制帧（Control Frame）</h4>

<ul>
  <li>定义与类型：操作码（opcode）最高位为 1 的帧即控制帧，已定义的有 0x8 关闭、0x9 Ping、0xA Pong；0xB–0xF 预留给未来控制帧。</li>
  <li>不能分片：控制帧必须 FIN = 1，且不得分片；如果接收方收到分片的控制帧，必须按协议失败处理（Fail the WebSocket Connection）。</li>
  <li>负载长度限制：控制帧负载长度必须 ≤ 125 字节；超过上限必须按协议失败处理。</li>
  <li>可穿插发送：控制帧可以在分片消息的中间插入发送，用于及时传达连接状态；接收方必须能在分片过程中处理控制帧。</li>
  <li>掩码规则：客户端发送的所有帧（包括控制帧）都必须带掩码；服务器发送的帧不得掩码。</li>
</ul>

<h5 id="close0x8">Close（0x8）</h5>

<ul>
  <li>作用：发起关闭握手，告知对端准备关闭连接。</li>
  <li>负载结构：可选负载由状态码（2 字节）+ 关闭原因（可选 UTF‑8 文本）组成；若带原因文本，必须是有效 UTF‑8。</li>
  <li>行为要求：
    <ul>
      <li>一端发送 Close 后，不得再发送数据帧。</li>
      <li>一端收到 Close 且自己尚未发送 Close，必须回送 Close 作为响应。</li>
      <li>完成双方 Close 交换后，连接关闭。</li>
    </ul>
  </li>
</ul>

<h5 id="ping0x9与-pong0xa">Ping（0x9）与 Pong（0xA）</h5>

<ul>
  <li>Ping 可携带应用数据；收到 Ping 必须尽快回送 Pong。</li>
  <li>Pong 的负载必须与对应 Ping 完全一致（逐字节相同）。</li>
  <li>Pong 可以主动发送（非响应），作为单向心跳；对主动 Pong 不要求再回 Pong。</li>
</ul>

<h5 id="实现数据帧解析中产生的思考">实现数据帧解析中产生的思考</h5>

<p>复习了位运算，原来位运算发光发热的地方是这里！（例如获取 FIN 位）</p>

<p>复习了大端序和小端序：</p>

<ul>
  <li>大端序（Big Endian）：最高有效字节放在最低内存地址；符合人类的阅读习惯</li>
  <li>小端序（Little Endian）：最低有效字节放在最低内存地址</li>
</ul>

<p>强调「有效」是为了排除前导零或无效位造成的歧义。</p>

<p>当 payloadLength 等于 126 或 127 时，真正的 payloadLength 是要从紧接的 2 个字节或 8 个字节去读。这时候协议规定这段字节序是 network byte order（即大端序）。</p>

<p>在研究如何操作 Data 时，我仔细看了 Data 目前提供的 API，发现有 <a href="https://developer.apple.com/documentation/foundation/data/span">Span types</a>！WWDC 25 <a href="https://developer.apple.com/videos/play/wwdc2025/312/">Improve memory usage and performance with Swift</a> 中有对它的介绍！Apple 也推出了 <a href="https://github.com/apple/swift-binary-parsing">Swift Binary Parsing</a>，后面计划抽空看看，应该能用来优化目前 <a href="https://github.com/SunsetWan/WebSocketClient-Demo/blob/main/Sources/LearnWebSocket/DataFrameParser.swift">DataFrameParser.swift</a> 的实现。</p>

<h4 id="分片fragmentation">分片（Fragmentation）</h4>

<p>WebSocket 允许把一个消息拆成多个帧发送，降低内存压力与延迟，支持流式处理。数据帧也可以是不分片的。一个不分片的数据帧，FIN 位总是 1，opcode 肯定不为 0。</p>

<p>规则：</p>

<ul>
  <li>第一个分片帧的 opcode 必须是文本帧（0x1）或二进制帧（0x2），FIN 位为 0。</li>
  <li>后续分片帧必须是续帧（0x0）。</li>
  <li>最后一个分片帧 FIN 为 1，opcode 仍为续帧（0x0）。</li>
  <li>消息边界：接收方用 FIN 判断一条消息结束；所有分片负载拼接成完整消息。</li>
  <li>控制帧：控制帧不能分片，但可以插入在分片序列中；接收方必须能在分片过程中处理控制帧。</li>
  <li>一致性约束：在一个消息分片未结束前，不能开始新的数据消息分片序列；否则属于协议错误。</li>
</ul>

<h3 id="关闭连接">关闭连接</h3>

<h4 id="正常关闭连接">正常关闭连接</h4>

<p>任一端可发起：发送 Close 控制帧（可带状态码与原因），连接进入 CLOSING；双方都发送并接收 Close 后，应关闭底层传输控制协议（Transmission Control Protocol，TCP）连接，进入 CLOSED 状态。如果完成关闭握手后再关闭 TCP 连接，可称之为「干净关闭」，否则视为「非干净关闭」。</p>

<h4 id="异常断开连接">异常断开连接</h4>

<p>出现错误即需关闭连接（握手阶段失败、协议错误、传输层连接意外中断等）。服务器在握手阶段可直接「中止连接（Abort）」并关闭 TCP 连接，不必发送 Close 帧。</p>

<h2 id="用-wireshark-分析">用 Wireshark 分析</h2>

<p>因为 WebSocket 的数据帧直接跑在 TCP 之上，很多只关注 HTTP 的抓包工具无法直接解析，所以我使用 Wireshark 来分析 <a href="https://github.com/SunsetWan/WebSocketClient-Demo/tree/main/Sources/LearnWebSocket">WebSocketClient-Demo</a> 发送的数据帧。我使用的是 <a href="https://github.com/vi/websocat">websocat</a>，用它来运行一个本地的 WebSocket 回声（echo）服务器。你给它发「hello」，它也会默认回你「hello」。</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// 回声服务器在本地 8080 端口监听
websocat -t ws-listen:127.0.0.1:8080 mirror:
</code></pre></div></div>

<p>在另一个终端里连接回声服务器：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>websocat -t ws://127.0.0.1:8080
</code></pre></div></div>

<h3 id="建立连接时的握手阶段-1">建立连接时的握手阶段</h3>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2026/websocket_note/websocket_note_2.jpg" alt="Wireshark 抓包显示客户端 GET 升级请求和服务端 101 Switching Protocols 响应" /></p>

<p>点击 Connect，客户端主动发送 HTTP 升级请求，并收到服务端的同意协议升级响应。</p>

<h3 id="解析数据帧data-frame-1">解析数据帧（Data Frame）</h3>

<p>建立连接成功后，发送 “hello” 字符串</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2026/websocket_note/websocket_note_3.jpg" alt="Wireshark 显示客户端发送带 MASK 的 hello 文本帧" /></p>

<p>我们可以看到数据帧为：0x81 0x85 0x88 0x56 0xa3 0xf5 0xe0 0x33 0xcf 0x99 0xe7</p>

<p>数据帧头部为两个字节：</p>

<ol>
  <li>0x81 -&gt; 0b1000_0001；可得 FIN 为 1，后面保留的三位均为 0，最后四位是 opcode，为 1，即代表该数据帧为文本。</li>
  <li>0x85 -&gt; 0b1000_0101；可得 MASK 为 1，因为这是客户端发送的数据，必须 masking。后面七位是 payload length，为 5，等于 “hello” 字符串的长度。</li>
</ol>

<p>因为 MASK 为 1，所以接下来的四个字节是客户端随机生成的 4 字节 masking key：0x88 0x56 0xa3 0xf5；</p>

<p>因为从头部分析出 payload length 是 5，所以后续 5 个字节就是 masked payload。</p>

<p><a href="https://github.com/SunsetWan/WebSocketClient-Demo/blob/main/Sources/LearnWebSocket/DataFrameBuilder.swift#L92">客户端 masking</a>：遍历 unmasked payload 中的每个字节，与 masking key 中每个字节（取余 4）作异或操作。</p>

<p>服务端从数据帧中取出 masking key，用其与 masked payload 再做一遍异或操作，就能还原出 unmasked payload。这是利用了异或的可逆性。设任意 bit 为 b，masking key 为 k，那么可得 b XOR k XOR k = b XOR (k XOR k) = b XOR 0 = b。</p>

<p>“hello” 对应的 16 进制的表达为：0x68 0x65 0x6c 0x6c 0x6f，与 masking key（0x88 0x56 0xa3 0xf5）异或后为：0xe0 0x33 0xcf 0x99 0xe7；与 Wireshark 上捕捉的结果相符！</p>

<h4 id="服务端发来的数据帧">服务端发来的数据帧</h4>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2026/websocket_note/websocket_note_4.jpg" alt="Wireshark 显示服务端返回不带 MASK 的 hello 文本帧" /></p>

<p>因为是回声服务器，所以服务端也是发来 “hello”，没有做 masking，能直接看到 payload 是 “hello”。</p>

<h4 id="客户端主动与服务端断开连接">客户端主动与服务端断开连接</h4>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2026/websocket_note/websocket_note_5.jpg" alt="Wireshark 显示客户端发送带 MASK、状态码 1000 的关闭帧" /></p>

<p>上图中：客户端发出关闭控制帧，FIN 位为 1，opcode 为 8。</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2026/websocket_note/websocket_note_6.jpg" alt="Wireshark 显示服务端返回不带 MASK 和载荷的关闭帧" /></p>

<p>上图中：客户端收到服务端响应的关闭控制帧。</p>

<h2 id="心得体会">心得体会</h2>

<p>首先得：</p>

<ol>
  <li>把各个类的类名取好，能把类名取得比较恰当，说明心里已经把它的职责划分好。</li>
  <li>定好各自公开/私有的接口。</li>
  <li>开始使用之前「空实现」的类，把逻辑写对。</li>
  <li>在第三步中，会带来很多思考，会使你退回第二步，重新思考接口的定义。重复几轮下来，大概率能完善接口的设计！</li>
  <li>补上一些单元测试</li>
  <li>补上具体方法的实现</li>
</ol>

<p>我感觉通过前四步的操作，能对全局的实现以及方向有个更清晰的认识，俗称把握大局观。第一步和第二步只要和 Codex 老师进入 Plan mode 猛聊一小时（实际取决于实现的功能大小）。第三第四步自己开始尝试，遇到新的思路，再回头与 Codex 继续聊。第五第六步已经是来到 Codex 老师的舒适区了，要么是 review 它的代码，要么就是虚心学习它的代码。</p>

<h2 id="q--a">Q &amp; A</h2>

<h3 id="websocket-的出现是为了解决什么问题">WebSocket 的出现是为了解决什么问题？</h3>

<p>在 WebSocket 出现前，大家想在浏览器里做「服务端能主动推、客户端也能随时发」的双向通信，常见是这些方案：</p>

<ol>
  <li>Long Polling（长轮询）：客户端发起一个超文本传输协议（Hypertext Transfer Protocol，HTTP）请求，服务器在新消息前「挂起」响应；一旦返回，客户端立即发起下一次请求，以此近似持续连接与服务端推送。</li>
  <li>流式传输：服务器保持单个 HTTP 连接很久，持续写入分块数据，客户端边接收边处理，达到持续推送效果。</li>
</ol>

<p>它们能用，但都有明显缺点：开销大、延迟不稳定、实现复杂。WebSocket 设计出来就是要<strong>用一个标准协议把这事正规化</strong>：一次升级握手后，真正实现<strong>全双工</strong>，不再靠「把 HTTP 请求拖得很长」来模拟。</p>

<h3 id="websocket-握手为什么要借用-http">WebSocket 握手为什么要借用 HTTP？</h3>

<p>它通过 HTTP/1.1 Upgrade 发起请求，升级成功之后，同一条 TCP 连接上跑的是 WebSocket 帧协议，已经不是 HTTP 报文了。它「借用了 HTTP 这张门票」进入现有网络环境。WebSocket 想把那些「伪装成 HTTP 的双向通信方案」统一替换掉；它通过 HTTP Upgrade 来建立连接，从而<strong>继续利用现有的代理、过滤和鉴权等基础设施</strong>。</p>

<h3 id="tcp-不是已经有-finack-关闭流程了吗为什么-websocket-还要多此一举">TCP 不是已经有 FIN/ACK 关闭流程了吗？为什么 WebSocket 还要多此一举？</h3>

<p>现实网络中的中间设备有很多，TCP 关闭原因/时机，不一定能可靠地表达「对端按应用层语义关了」。</p>

<p>TCP 的 FIN/ACK 只表达「字节流结束」，不表达「消息语义结束」。但 WebSocket 有「消息/帧语义」和「关闭码/原因」。Close 帧可以带：关闭状态码（比如正常关闭、协议错误、消息太大、服务端异常等）和可选原因字符串。</p>

<h2 id="参考">参考</h2>

<p><a href="https://github.com/SunsetWan/WebSocketClient-Demo/tree/main/Sources/LearnWebSocket">WebSocketClient-Demo</a> 中仅实现了：</p>

<ol>
  <li>握手过程</li>
  <li>仅支持解析/构造控制帧和 text 数据帧</li>
</ol>

<p>所以，还是很有必要去看看其他 WebSocket 客户端/服务端的实现，我搜集了如下几个：</p>

<ul>
  <li><a href="https://github.com/daltoniam/Starscream">Starscream</a></li>
  <li>Vapor 的 <a href="https://github.com/vapor/websocket-kit">websocket-kit</a></li>
  <li>Swift-NIO 里的 <a href="https://github.com/apple/swift-nio/tree/main/Sources/NIOWebSocket">NIOWebSocket</a></li>
  <li>WebSocket <a href="https://github.com/vi/websocat">命令行客户端 websocat</a></li>
</ul>

<h2 id="starscream-源码分析">Starscream 源码分析</h2>

<p>看来看去还是 Starscream 的星星数比较多多，同时也是 Socket.IO iOS 客户端的依赖之一，所有首先来看看它的架构图：</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2026/websocket_note/WebSocket%20Engine%20Protocol-2026-02-21-144603.png" alt="Starscream 从 API、Engine 到 Compression、Security、Transport 和 Framer 的分层架构图" /></p>

<h4 id="设计特点">设计特点</h4>

<ol>
  <li>
    <p><strong>面向协议设计（POP）</strong>：每一层都通过协议（<code class="language-plaintext highlighter-rouge">Engine</code>, <code class="language-plaintext highlighter-rouge">Transport</code>, <code class="language-plaintext highlighter-rouge">Framer</code>, <code class="language-plaintext highlighter-rouge">CertificatePinning</code>, <code class="language-plaintext highlighter-rouge">CompressionHandler</code>）定义接口，实现可替换。</p>
  </li>
  <li>
    <p><strong>双引擎策略</strong>：<code class="language-plaintext highlighter-rouge">WSEngine</code> 提供完整的自定义 RFC 6455 实现；<code class="language-plaintext highlighter-rouge">NativeEngine</code> 封装系统的 <code class="language-plaintext highlighter-rouge">URLSessionWebSocketTask</code>。在 <code class="language-plaintext highlighter-rouge">WebSocket.init</code> 中根据系统版本和 <code class="language-plaintext highlighter-rouge">useCustomEngine</code> 参数自动选择。</p>
  </li>
  <li>
    <p><strong>WSEngine 是组合中心</strong>：它实现了 <code class="language-plaintext highlighter-rouge">TransportEventClient</code>、<code class="language-plaintext highlighter-rouge">FramerEventClient</code>、<code class="language-plaintext highlighter-rouge">FrameCollectorDelegate</code>、<code class="language-plaintext highlighter-rouge">HTTPHandlerDelegate</code> 四个回调协议，将 Transport → HTTP 握手 → 帧解析 → 分片重组 → 压缩/解压 的完整流程串联起来。</p>
  </li>
  <li>
    <p><strong>传输层适配</strong>：<code class="language-plaintext highlighter-rouge">TCPTransport</code>（基于 Network.framework）用于 iOS 12+；<code class="language-plaintext highlighter-rouge">FoundationTransport</code>（基于 Stream）兼容更早的系统。</p>
  </li>
</ol>

<h4 id="starscreamwebsocket-的初始化过程">Starscream.WebSocket 的初始化过程</h4>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="kd">public</span> <span class="kd">convenience</span> <span class="nf">init</span><span class="p">(</span>
        <span class="nv">request</span><span class="p">:</span> <span class="kt">URLRequest</span><span class="p">,</span>
        <span class="nv">certPinner</span><span class="p">:</span> <span class="kt">CertificatePinning</span><span class="p">?</span> <span class="o">=</span> <span class="kt">FoundationSecurity</span><span class="p">(),</span>
        <span class="nv">compressionHandler</span><span class="p">:</span> <span class="kt">CompressionHandler</span><span class="p">?</span> <span class="o">=</span> <span class="kc">nil</span><span class="p">,</span>
        <span class="nv">useCustomEngine</span><span class="p">:</span> <span class="kt">Bool</span> <span class="o">=</span> <span class="kc">true</span>
    <span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="k">#available</span><span class="p">(</span><span class="n">macOS</span> <span class="mf">10.15</span><span class="p">,</span> <span class="n">iOS</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="n">tvOS</span> <span class="mf">13.0</span><span class="p">,</span> <span class="o">*</span><span class="p">),</span> <span class="o">!</span><span class="n">useCustomEngine</span> <span class="p">{</span>
            <span class="k">self</span><span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="nv">request</span><span class="p">:</span> <span class="n">request</span><span class="p">,</span> <span class="nv">engine</span><span class="p">:</span> <span class="p">())</span>
        <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="k">#available</span><span class="p">(</span><span class="n">macOS</span> <span class="mf">10.14</span><span class="p">,</span> <span class="n">iOS</span> <span class="mf">12.0</span><span class="p">,</span> <span class="n">watchOS</span> <span class="mf">5.0</span><span class="p">,</span> <span class="n">tvOS</span> <span class="mf">12.0</span><span class="p">,</span> <span class="o">*</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">self</span><span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="nv">request</span><span class="p">:</span> <span class="n">request</span><span class="p">,</span> <span class="nv">engine</span><span class="p">:</span> <span class="kt">WSEngine</span><span class="p">(</span><span class="nv">transport</span><span class="p">:</span> <span class="kt">TCPTransport</span><span class="p">(),</span> <span class="nv">certPinner</span><span class="p">:</span> <span class="n">certPinner</span><span class="p">,</span> <span class="nv">compressionHandler</span><span class="p">:</span> <span class="n">compressionHandler</span><span class="p">))</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="k">self</span><span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="nv">request</span><span class="p">:</span> <span class="n">request</span><span class="p">,</span> <span class="nv">engine</span><span class="p">:</span> <span class="kt">WSEngine</span><span class="p">(</span><span class="nv">transport</span><span class="p">:</span> <span class="kt">FoundationTransport</span><span class="p">(),</span> <span class="nv">certPinner</span><span class="p">:</span> <span class="n">certPinner</span><span class="p">,</span> <span class="nv">compressionHandler</span><span class="p">:</span> <span class="n">compressionHandler</span><span class="p">))</span>
        <span class="p">}</span>
    <span class="p">}</span>

</code></pre></div></div>

<p>当最低支持 iOS 13 时，Starscream.WebSocket 背后的核心实现依赖于 <a href="https://github.com/daltoniam/Starscream/blob/master/Sources/Engine/NativeEngine.swift">NativeEngine</a>，而 NativeEngine 则是构建于 <a href="https://developer.apple.com/documentation/foundation/urlsessionwebsockettask">URLSessionWebSocketTask</a> 之上。现在大部分的 iOS app 早就最低支持 iOS 13 了吧，我怀疑可能就是这个原因让这个项目基本上不活跃了，最近的一次 release 已经是 2024 年三月份的了！</p>

<p>本着学习协议的目标，我们可以阅读 <a href="https://github.com/daltoniam/Starscream/blob/master/Sources/Engine/WSEngine.swift">WSEngine</a> 的代码。</p>

<h4 id="握手阶段">握手阶段</h4>

<p><a href="https://github.com/daltoniam/Starscream/blob/master/Sources/Engine/WSEngine.swift#L143">代码</a></p>

<ol>
  <li>创建一个 HTTP Upgrade 的 URLRequest</li>
  <li>利用 CFHTTPMessageCopySerializedMessage 方法把 URLRequest 转为 Data （<a href="https://github.com/daltoniam/Starscream/blob/master/Sources/Framer/FoundationHTTPHandler.swift#L47">代码</a>）</li>
  <li>使用 NWConnection 的 TCP 连接发送该 Data （<a href="https://github.com/daltoniam/Starscream/blob/master/Sources/Transport/TCPTransport.swift#L99">代码</a>）</li>
  <li>等待 NWConnection 收齐服务端发来的 HTTP Response 头时，检验其是否合法 （<a href="https://github.com/daltoniam/Starscream/blob/master/Sources/Framer/FoundationHTTPHandler.swift#L94">代码</a>）</li>
  <li>若合法，在 <a href="https://github.com/daltoniam/Starscream/blob/master/Sources/Engine/WSEngine.swift#L177">didReceiveHTTP</a> 回调中更新标志位 isConnecting = false, didUpgrade = true, canSend = true。使用 Value 为 1 的 DispatchSemaphore 来保护这些共享状态，以防出现数据竞争。</li>
</ol>

<p>对比我写的 demo 里，是 <a href="https://github.com/SunsetWan/WebSocketClient-Demo/blob/main/Sources/LearnWebSocket/WebSocketClient.swift#L266">手动构造一个多行 String</a> 来实现 HTTP 请求头，而 Starscream 的做法在工程角度上说更好，先创建的是 URLRequest 对象，再用 CFNetwork 中的方法把它序列化为 Data，交给 TCP 传输层发送。</p>

<h4 id="接受数据">接受数据</h4>

<p><a href="https://github.com/daltoniam/Starscream/blob/master/Sources/Engine/WSEngine.swift#L154">代码</a></p>

<ol>
  <li><a href="https://github.com/daltoniam/Starscream/blob/master/Sources/Transport/TCPTransport.swift">TCPTransport</a> 把 TCP 连接中的字节流组装为 Data，喂给 <a href="https://github.com/daltoniam/Starscream/blob/master/Sources/Framer/Framer.swift#L86">WSFramer</a></li>
  <li>WSFramer 把 Data 解析为 WebSocket 帧数据（是续帧，还是文本帧，还是二进制帧，还是控制帧）</li>
  <li>WSFrame 把解析出来的帧数据喂给 <a href="https://github.com/daltoniam/Starscream/blob/master/Sources/Framer/FrameCollector.swift">FrameCollector</a></li>
  <li>FrameCollector 把续帧拼接起来，组装为完整的“消息”（.text(String) / .binary(Data)）</li>
</ol>

<h4 id="代理链条">代理链条</h4>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌──────────────┐    TransportEventClient     ┌──────────┐
│ TCPTransport │ ──────────────────────────▶  │ WSEngine  │
└──────────────┘  "TCP 有数据来了/连上了"       └────┬─────┘
                                                   │
                    ┌──────────────────────────────┤
                    │ WSEngine 把数据分发给          │
                    │ 两个下游模块                   │
                    ▼                              ▼
          ┌─────────────────────┐        ┌──────────────┐
          │ FoundationHTTPHandler│        │   WSFramer    │
          └────────┬────────────┘        └──────┬───────┘
                   │ HTTPHandlerDelegate         │ FramerEventClient
                   │ "HTTP 握手成功了"             │ "解析出一个帧了"
                   ▼                              ▼
             ┌──────────┐                  ┌──────────┐
             │ WSEngine  │                 │ WSEngine  │
             └──────────┘                  └────┬─────┘
                                                │
                                                │ WSEngine 把帧交给
                                                ▼
                                       ┌────────────────┐
                                       │ FrameCollector  │
                                       └───────┬────────┘
                                               │ FrameCollectorDelegate
                                               │ "完整消息拼好了"
                                               ▼
                                         ┌──────────┐
                                         │ WSEngine  │
                                         └────┬─────┘
                                               │ EngineDelegate
                                               │ "这是最终事件"
                                               ▼
                                         ┌──────────┐
                                         │ WebSocket │
                                         └────┬─────┘
                                               │
                                               ▼
                                       用户 delegate / onEvent
</code></pre></div></div>

<p>WSEngine 在图中反复出现——它<strong>同时是 4 个模块的 delegate</strong>：</p>

<table>
  <thead>
    <tr>
      <th>谁通知 WSEngine</th>
      <th>通过什么协议</th>
      <th>WSEngine 的回调方法</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>TCPTransport</td>
      <td><code class="language-plaintext highlighter-rouge">TransportEventClient</code></td>
      <td><code class="language-plaintext highlighter-rouge">connectionChanged(state:)</code></td>
    </tr>
    <tr>
      <td>FoundationHTTPHandler</td>
      <td><code class="language-plaintext highlighter-rouge">HTTPHandlerDelegate</code></td>
      <td><code class="language-plaintext highlighter-rouge">didReceiveHTTP(event:)</code></td>
    </tr>
    <tr>
      <td>WSFramer</td>
      <td><code class="language-plaintext highlighter-rouge">FramerEventClient</code></td>
      <td><code class="language-plaintext highlighter-rouge">frameProcessed(event:)</code></td>
    </tr>
    <tr>
      <td>FrameCollector</td>
      <td><code class="language-plaintext highlighter-rouge">FrameCollectorDelegate</code></td>
      <td><code class="language-plaintext highlighter-rouge">didForm(event:)</code></td>
    </tr>
  </tbody>
</table>

<p>WSEngine 作为”中枢调度器”：所有事件都汇聚到 WSEngine，由它决定下一步交给谁处理，最终通过 <code class="language-plaintext highlighter-rouge">EngineDelegate</code> 把结果传给 WebSocket。</p>]]></content><author><name>Sunset Wan</name></author><category term="WebSocket" /><category term="Networking" /><summary type="html"><![CDATA[工作项目用了 Socket.IO 来做客户端与服务端的实时通信，我看了 Socket.IO 是基于 WebSocket 协议进一步封装的。于是，我希望通过实现 WebSocket 客户端 的 一小部分 功能，以达到了解 WebSocket 协议的目标！ 教科书：RFC 6455 老师：Codex 代码仓库地址：WebSocketClient-Demo]]></summary></entry><entry><title type="html">SwiftUI View 的 Identity 与 Lifetime</title><link href="https://sunsetwan.github.io/2025/12/01/SwiftUI_Identity_lifetime/" rel="alternate" type="text/html" title="SwiftUI View 的 Identity 与 Lifetime" /><published>2025-12-01T23:33:33+08:00</published><updated>2025-12-01T23:33:33+08:00</updated><id>https://sunsetwan.github.io/2025/12/01/SwiftUI_Identity_lifetime</id><content type="html" xml:base="https://sunsetwan.github.io/2025/12/01/SwiftUI_Identity_lifetime/"><![CDATA[<p>前阵子遇到一个很奇怪的 BUG：用 <a href="https://developer.apple.com/documentation/SwiftUI/NavigationStack">NavigationStack</a> push 一个 View，CPU 使用率竟然冲上 100%！当 Xcode 运行一段 SwiftUI 代码时，我们应该关注哪些方面？特此回顾 <a href="https://developer.apple.com/videos/play/wwdc2021/10022/">WWDC 21 Demystify SwiftUI</a>。<!-- more --></p>

<p>当 Xcode 运行一段 SwiftUI 代码时，我们应该关注哪些方面？</p>

<ol>
  <li>Identity — 是 SwiftUI 用来区分视图元素的「身份证」</li>
  <li>Lifetime — 是 SwiftUI 用来管理视图与其相关状态的生命周期</li>
  <li>Dependency — 是 SwiftUI 用来决定视图为何以及何时需要更新</li>
</ol>

<h2 id="identity">Identity</h2>

<p>SwiftUI 中 Identity 分为两种：Explicit Identity 和 Structural Identity。</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2025/swiftui_identity_lifetime/assets/01.jpeg" alt="外表相似的双胞胎并肩站在走廊中" /></p>

<p>当遇到两个外表很相似的双胞胎，我们该如何区分？对，可以通过姓名来区分！这就是 Explicit Identity。</p>

<p>我查了点资料，我们仅知道这对著名的双胞胎姐妹是格雷迪双胞胎姐妹，并不知道各自的姓名。那我问你：你觉得哪个更恐怖一点？我是觉得左边那位更恐怖一些。<br />
通过她俩的相对位置来区分：一位是站在左边，另一位是站在右边。这就是 Structural Identity。</p>

<p>接下来，我们来看一个更实际的例子：</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2025/swiftui_identity_lifetime/assets/02.jpg" alt="SwiftUI 条件分支映射为 ConditionalContent 的 True 和 False 两种结构身份" /></p>

<p>左边的代码在 SwiftUI 「眼里」当条件为真时显示 <code class="language-plaintext highlighter-rouge">AdoptionDirectory</code> 视图，条件为假时显示 <code class="language-plaintext highlighter-rouge">DogList</code> 视图。只要条件恒定，SwiftUI 能知道哪个是「真分支」哪个是「假分支」。</p>

<p>View body 中的 if 语句在 SwiftUI 里会被编译成 _ConditionalContent&lt;TrueView, FalseView&gt; 这样的泛型类型，TrueView 和 FalseView 是两个分支的具体类型。这个转换由 <a href="https://developer.apple.com/documentation/swiftui/viewbuilder">ViewBuilder</a> 完成。<br />
View 协议的 body 会隐式包裹在 ViewBuilder 中，ViewBuilder 会把 body 里的条件逻辑拼成一个统一的泛型视图类型。</p>

<p>Structural Identity 是 Swift 通过视图元素在视图层级中的类型与位置来识别视图身份的方法。</p>

<h3 id="pawview-示例">PawView 示例</h3>

<p>在这个 <a href="https://github.com/SunsetWan/WWDC-21-Demystify-SwiftUI-code-example/blob/main/SharingDemo-PawView/SharingDemo/ContentView.swift#L115">代码示例</a> 里，我们可以通过点击事件来更新 <code class="language-plaintext highlighter-rouge">dog.isGood</code> 状态，从而触发 <code class="language-plaintext highlighter-rouge">pawView</code> 的刷新。</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="kd">@ViewBuilder</span>
    <span class="k">var</span> <span class="nv">pawView</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">if</span> <span class="n">dog</span><span class="o">.</span><span class="n">isGood</span> <span class="p">{</span>
            <span class="kt">PawView</span><span class="p">(</span><span class="nv">tint</span><span class="p">:</span> <span class="o">.</span><span class="n">green</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">padding</span><span class="p">(</span><span class="o">.</span><span class="n">top</span><span class="p">,</span> <span class="mi">32</span><span class="p">)</span>
            <span class="kt">Spacer</span><span class="p">()</span>
                <span class="o">.</span><span class="nf">padding</span><span class="p">(</span><span class="o">.</span><span class="n">bottom</span><span class="p">,</span> <span class="mi">32</span><span class="p">)</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="kt">Spacer</span><span class="p">()</span>
            <span class="kt">PawView</span><span class="p">(</span><span class="nv">tint</span><span class="p">:</span> <span class="o">.</span><span class="n">red</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">padding</span><span class="p">(</span><span class="o">.</span><span class="n">bottom</span><span class="p">,</span> <span class="mi">32</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
</code></pre></div></div>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2025/swiftui_identity_lifetime/assets/03.gif" alt="条件分支切换时，绿色爪印消失后红色爪印重新出现的动画" /></p>

<h4 id="问题一为什么是绿色爪印消失后再出现红色爪印呢">问题一：为什么是绿色爪印消失后再出现红色爪印呢？</h4>

<p>因为 SwiftUI 是通过 Structural Identity 来区分绿色爪印和红色爪印，当 <code class="language-plaintext highlighter-rouge">dog.isGood</code> 变化时，SwiftUI 认为它需要销毁绿色爪印视图和创建红色爪印视图。</p>

<p>不难得出：视图的 Identity 发生变化后，SwiftUI 会认为它需要销毁旧视图并创建新视图。</p>

<h4 id="问题二单个爪印直接从绿色过渡到红色应该会更丝滑我要怎么做">问题二：单个爪印直接从绿色过渡到红色应该会更丝滑，我要怎么做？</h4>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">var</span> <span class="nv">pawView</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">PawView</span><span class="p">(</span><span class="nv">tint</span><span class="p">:</span> <span class="n">dog</span><span class="o">.</span><span class="n">isGood</span> <span class="p">?</span> <span class="o">.</span><span class="nv">green</span> <span class="p">:</span> <span class="o">.</span><span class="n">red</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">padding</span><span class="p">(</span><span class="o">.</span><span class="n">top</span><span class="p">,</span> <span class="mi">32</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">padding</span><span class="p">(</span><span class="o">.</span><span class="n">bottom</span><span class="p">,</span> <span class="mi">32</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">maxHeight</span><span class="p">:</span> <span class="o">.</span><span class="n">infinity</span><span class="p">,</span> <span class="nv">alignment</span><span class="p">:</span> <span class="n">dog</span><span class="o">.</span><span class="n">isGood</span> <span class="p">?</span> <span class="o">.</span><span class="nv">top</span> <span class="p">:</span> <span class="o">.</span><span class="n">bottom</span><span class="p">)</span>
    <span class="p">}</span>
</code></pre></div></div>

<p>我们可以把 <code class="language-plaintext highlighter-rouge">pawView</code> 里的 if 分支拿掉，单个 <code class="language-plaintext highlighter-rouge">PawView</code> 的 Structural Identity 就保持不变；<code class="language-plaintext highlighter-rouge">dog.isGood</code> 变化时，只是同一个视图的 tintColor 和位置在变，状态与动画都能连续。</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2025/swiftui_identity_lifetime/assets/04.gif" alt="保持同一 PawView 身份后，爪印颜色和位置连续过渡的动画" /></p>

<h2 id="lifetime">Lifetime</h2>

<p>我们给 <code class="language-plaintext highlighter-rouge">PawView</code> 加上 <code class="language-plaintext highlighter-rouge">PawViewState</code> 状态，来看看 Structural Identity 的变化对视图状态的影响</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="kt">PawViewState</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">id</span><span class="p">:</span> <span class="kt">UUID</span>

    <span class="nf">init</span><span class="p">(</span><span class="nv">id</span><span class="p">:</span> <span class="kt">UUID</span> <span class="o">=</span> <span class="kt">UUID</span><span class="p">())</span> <span class="p">{</span>
        <span class="k">self</span><span class="o">.</span><span class="n">id</span> <span class="o">=</span> <span class="n">id</span>
        <span class="nf">print</span><span class="p">(</span><span class="s">"PawViewState(</span><span class="se">\(</span><span class="n">id</span><span class="se">)</span><span class="s">) init at </span><span class="se">\(</span><span class="kt">Date</span><span class="p">()</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="kd">deinit</span> <span class="p">{</span>
        <span class="nf">print</span><span class="p">(</span><span class="s">"PawViewState(</span><span class="se">\(</span><span class="n">id</span><span class="se">)</span><span class="s">) deinit at </span><span class="se">\(</span><span class="kt">Date</span><span class="p">()</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">PawView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">tint</span><span class="p">:</span> <span class="kt">Color</span>
    <span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">state</span><span class="p">:</span> <span class="kt">PawViewState</span> <span class="o">=</span> <span class="o">.</span><span class="nf">init</span><span class="p">()</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">Circle</span><span class="p">()</span>
            <span class="o">.</span><span class="nf">fill</span><span class="p">(</span><span class="n">tint</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">frame</span><span class="p">(</span><span class="nv">width</span><span class="p">:</span> <span class="mi">180</span><span class="p">,</span> <span class="nv">height</span><span class="p">:</span> <span class="mi">180</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">overlay</span><span class="p">(</span>
                <span class="kt">Image</span><span class="p">(</span><span class="nv">systemName</span><span class="p">:</span> <span class="s">"pawprint.fill"</span><span class="p">)</span>
                    <span class="o">.</span><span class="nf">resizable</span><span class="p">()</span>
                    <span class="o">.</span><span class="nf">scaledToFit</span><span class="p">()</span>
                    <span class="o">.</span><span class="nf">foregroundColor</span><span class="p">(</span><span class="o">.</span><span class="n">white</span><span class="p">)</span>
                    <span class="o">.</span><span class="nf">padding</span><span class="p">(</span><span class="mi">44</span><span class="p">)</span>
            <span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>在我触发两次点击事件（Good dog → Bad dog, Bad dog → Good dog）后，Xcode console 输出如下：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">PawViewState</span><span class="p">(</span><span class="mi">39</span><span class="kt">BFC159</span><span class="o">-</span><span class="kt">AE06</span><span class="o">-</span><span class="mi">45</span><span class="kt">A5</span><span class="o">-</span><span class="mi">9083</span><span class="o">-</span><span class="kt">B1C5BDF053C3</span> <span class="kd">init</span> <span class="n">at</span> <span class="mi">2025</span><span class="o">-</span><span class="mi">11</span><span class="o">-</span><span class="mi">30</span> <span class="mo">05</span><span class="p">:</span><span class="mi">42</span><span class="p">:</span><span class="mo">02</span> <span class="o">+</span><span class="mo">0000</span> <span class="c1">// Good dog 1 init</span>
<span class="kt">PawViewState</span><span class="p">(</span><span class="mi">5</span><span class="kt">FB6A6D4</span><span class="o">-</span><span class="mf">1E81</span><span class="o">-</span><span class="mi">43</span><span class="kt">A1</span><span class="o">-</span><span class="mi">8</span><span class="kt">AB1</span><span class="o">-</span><span class="mo">04</span><span class="kt">C2DEE8455A</span> <span class="kd">init</span> <span class="n">at</span> <span class="mi">2025</span><span class="o">-</span><span class="mi">11</span><span class="o">-</span><span class="mi">30</span> <span class="mo">05</span><span class="p">:</span><span class="mi">42</span><span class="p">:</span><span class="mo">05</span> <span class="o">+</span><span class="mo">0000</span> <span class="c1">// Bad dog 1 init</span>
<span class="kt">PawViewState</span><span class="p">(</span><span class="mi">39</span><span class="kt">BFC159</span><span class="o">-</span><span class="kt">AE06</span><span class="o">-</span><span class="mi">45</span><span class="kt">A5</span><span class="o">-</span><span class="mi">9083</span><span class="o">-</span><span class="kt">B1C5BDF053C3</span> <span class="kd">deinit</span> <span class="n">at</span> <span class="mi">2025</span><span class="o">-</span><span class="mi">11</span><span class="o">-</span><span class="mi">30</span> <span class="mo">05</span><span class="p">:</span><span class="mi">42</span><span class="p">:</span><span class="mo">06</span> <span class="o">+</span><span class="mo">0000</span> <span class="c1">// Good dog 1 deinit</span>
<span class="kt">PawViewState</span><span class="p">(</span><span class="mf">33E05</span><span class="kt">E98</span><span class="o">-</span><span class="mi">2553</span><span class="o">-</span><span class="mi">4</span><span class="kt">C8D</span><span class="o">-</span><span class="mi">81</span><span class="kt">B0</span><span class="o">-</span><span class="kt">A56E2E353962</span> <span class="kd">init</span> <span class="n">at</span> <span class="mi">2025</span><span class="o">-</span><span class="mi">11</span><span class="o">-</span><span class="mi">30</span> <span class="mo">05</span><span class="p">:</span><span class="mi">42</span><span class="p">:</span><span class="mo">07</span> <span class="o">+</span><span class="mo">0000</span> <span class="c1">// Good dog 2 init</span>
<span class="kt">PawViewState</span><span class="p">(</span><span class="mi">5</span><span class="kt">FB6A6D4</span><span class="o">-</span><span class="mf">1E81</span><span class="o">-</span><span class="mi">43</span><span class="kt">A1</span><span class="o">-</span><span class="mi">8</span><span class="kt">AB1</span><span class="o">-</span><span class="mo">04</span><span class="kt">C2DEE8455A</span> <span class="kd">deinit</span> <span class="n">at</span> <span class="mi">2025</span><span class="o">-</span><span class="mi">11</span><span class="o">-</span><span class="mi">30</span> <span class="mo">05</span><span class="p">:</span><span class="mi">42</span><span class="p">:</span><span class="mi">08</span> <span class="o">+</span><span class="mo">0000</span> <span class="c1">// Bad dog 1 deinit</span>
</code></pre></div></div>

<p>从上面的例子中总结：</p>

<ol>
  <li>视图的 Identity 发生变化后，SwiftUI 会认为它需要销毁旧视图并创建新视图。也就是说，视图的生命周期是否延续取决于 Identity 是否保持不变。</li>
  <li>SwiftUI 里状态的生命周期和视图的生命周期是绑定在一起的。也就是说，一个视图被创建时状态才会被创建，视图被销毁时状态就随之消失。</li>
</ol>

<p>因此，可得：</p>

<p>SwiftUI 把视图的「本质」归结为它持有的状态，并把这份状态和视图的身份绑定在一起。<br />
身份不变，状态就被保留下来；身份一变，SwiftUI 就当成全新的视图，状态也重建。这样能清晰地区分：哪些状态应该延续（身份稳定的视图），哪些状态应该重置（身份变化的视图）。</p>

<h3 id="favoritepetsview-示例">FavoritePetsView 示例</h3>

<p>再看这个 <a href="https://github.com/SunsetWan/WWDC-21-Demystify-SwiftUI-code-example/blob/main/SharingDemo-TreatJar/TreatJar/TreatJar/FavoritePetsView.swift#L37">例子</a>，来辅助理解上面的总结。</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2025/swiftui_identity_lifetime/assets/05.png" alt="Favorite Pets 列表显示五只宠物及右上角 Add 按钮" /></p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">Pet</span><span class="p">:</span> <span class="kt">Identifiable</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">name</span><span class="p">:</span> <span class="kt">String</span>
    <span class="k">var</span> <span class="nv">kind</span><span class="p">:</span> <span class="kt">Animal</span>
    <span class="k">var</span> <span class="nv">id</span><span class="p">:</span> <span class="kt">UUID</span> <span class="p">{</span> <span class="c1">// 不稳定的 identity</span>
        <span class="kt">UUID</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">struct</span> <span class="kt">FavoritePetsView</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">@State</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">pets</span><span class="p">:</span> <span class="p">[</span><span class="kt">Pet</span><span class="p">]</span>

    <span class="nf">init</span><span class="p">(</span><span class="nv">pets</span><span class="p">:</span> <span class="p">[</span><span class="kt">Pet</span><span class="p">])</span> <span class="p">{</span>
        <span class="k">self</span><span class="o">.</span><span class="n">pets</span> <span class="o">=</span> <span class="n">pets</span>
    <span class="p">}</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">NavigationStack</span> <span class="p">{</span>
            <span class="kt">List</span> <span class="p">{</span>
                <span class="kt">ForEach</span><span class="p">(</span><span class="n">pets</span><span class="p">)</span> <span class="p">{</span> <span class="n">pet</span> <span class="k">in</span>
                    <span class="kt">PetRow</span><span class="p">(</span><span class="nv">pet</span><span class="p">:</span> <span class="n">pet</span><span class="p">)</span>
                <span class="p">}</span>
            <span class="p">}</span>
            <span class="o">.</span><span class="nf">listStyle</span><span class="p">(</span><span class="o">.</span><span class="n">plain</span><span class="p">)</span>
            <span class="o">.</span><span class="nf">navigationTitle</span><span class="p">(</span><span class="s">"Favorite Pets"</span><span class="p">)</span>
            <span class="o">.</span><span class="n">toolbar</span> <span class="p">{</span>
                <span class="kt">ToolbarItem</span><span class="p">(</span><span class="nv">placement</span><span class="p">:</span> <span class="o">.</span><span class="n">topBarTrailing</span><span class="p">)</span> <span class="p">{</span>
                    <span class="kt">Button</span><span class="p">(</span><span class="s">"Add"</span><span class="p">)</span> <span class="p">{</span>
                        <span class="nf">addTomCat</span><span class="p">()</span>
                    <span class="p">}</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="kd">func</span> <span class="nf">addTomCat</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">newPet</span> <span class="o">=</span> <span class="kt">Pet</span><span class="p">(</span><span class="nv">name</span><span class="p">:</span> <span class="s">"汤姆"</span><span class="p">,</span> <span class="nv">kind</span><span class="p">:</span> <span class="o">.</span><span class="n">cat</span><span class="p">)</span>
        <span class="n">pets</span><span class="o">.</span><span class="nf">insert</span><span class="p">(</span><span class="n">newPet</span><span class="p">,</span> <span class="nv">at</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>当点击 Add 按钮时，「汤姆猫」出现在 List 的首位。让我们用 SwiftUI Instruments 来查看当前视图的更新次数。</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2025/swiftui_identity_lifetime/assets/06.jpg" alt="SwiftUI Instruments 显示身份不稳定时 PetRow body 执行 29 次" /></p>

<p>因为 PetRow Identity 的不稳定性，导致 <code class="language-plaintext highlighter-rouge">PetRow</code> 的 body 被执行多次。如果放在一个大型项目中，可能会导致 App 出现卡顿。</p>

<p>使用计算属性生成 UUID 的方式遵循 <code class="language-plaintext highlighter-rouge">Identifiable</code> 会导致视图 Identity 不稳定，所以作如下修改，以提供稳定的 Identity。</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">Pet</span><span class="p">:</span> <span class="kt">Identifiable</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">name</span><span class="p">:</span> <span class="kt">String</span>
    <span class="k">var</span> <span class="nv">kind</span><span class="p">:</span> <span class="kt">Animal</span>
    <span class="k">let</span> <span class="nv">id</span> <span class="o">=</span> <span class="kt">UUID</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

<p>可以再次运行 SwiftUI Instruments 来查看当前视图的更新次数：</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2025/swiftui_identity_lifetime/assets/07.jpg" alt="使用稳定 id 后 SwiftUI Instruments 显示 PetRow body 仅创建 6 次" /></p>

<p>PetRow 的 body 被执行的次数锐减至 6 次！原本已有 5 个宠物，再加上新增的「汤姆猫」，总计 6 次，与预期相符。</p>

<h3 id="treatjar-示例">TreatJar 示例</h3>

<p>再看这个稍微复杂一点的 <a href="https://github.com/SunsetWan/WWDC-21-Demystify-SwiftUI-code-example/blob/main/SharingDemo-TreatJar/TreatJar/TreatJar/TreatJarView.swift#L45">例子</a>。Treat Jar View 展示的是给宠物准备的零食。如果有零食过期，那就置灰。</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2025/swiftui_identity_lifetime/assets/08.png" alt="Treat Jar 页面中已过期零食变灰，未过期零食保持高亮" /></p>

<p>我们借助于 <code class="language-plaintext highlighter-rouge">ExpirationModifier</code> 来实现置灰的效果。</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">NavigationStack</span> <span class="p">{</span>
            <span class="kt">ScrollView</span> <span class="p">{</span>
                <span class="kt">LazyVGrid</span><span class="p">(</span><span class="nv">columns</span><span class="p">:</span> <span class="n">columns</span><span class="p">,</span> <span class="nv">spacing</span><span class="p">:</span> <span class="mi">16</span><span class="p">)</span> <span class="p">{</span>
                    <span class="kt">ForEach</span><span class="p">(</span><span class="n">treats</span><span class="p">,</span> <span class="nv">id</span><span class="p">:</span> <span class="p">\</span><span class="o">.</span><span class="n">serialNumber</span><span class="p">)</span> <span class="p">{</span> <span class="n">treat</span> <span class="k">in</span>
                        <span class="kt">TreatCell</span><span class="p">(</span><span class="nv">treat</span><span class="p">:</span> <span class="n">treat</span><span class="p">)</span>
                            <span class="o">.</span><span class="nf">modifier</span><span class="p">(</span><span class="kt">ExpirationModifier</span><span class="p">(</span><span class="nv">date</span><span class="p">:</span> <span class="n">treat</span><span class="o">.</span><span class="n">expiryDate</span><span class="p">,</span> <span class="nv">referenceDate</span><span class="p">:</span> <span class="n">referenceDate</span><span class="p">))</span>
                    <span class="p">}</span>
                <span class="p">}</span>
                <span class="o">.</span><span class="nf">padding</span><span class="p">(</span><span class="mi">16</span><span class="p">)</span>
            <span class="p">}</span>
            <span class="o">.</span><span class="nf">background</span><span class="p">(</span><span class="kt">Color</span><span class="p">(</span><span class="o">.</span><span class="n">systemGroupedBackground</span><span class="p">))</span>
            <span class="o">.</span><span class="nf">navigationTitle</span><span class="p">(</span><span class="s">"Treat Jar"</span><span class="p">)</span>
            <span class="o">.</span><span class="n">toolbar</span> <span class="p">{</span>
                <span class="kt">ToolbarItem</span><span class="p">(</span><span class="nv">placement</span><span class="p">:</span> <span class="o">.</span><span class="n">topBarLeading</span><span class="p">)</span> <span class="p">{</span>
                    <span class="kt">Button</span><span class="p">(</span><span class="n">referenceDateIsExpired</span> <span class="p">?</span> <span class="s">"Restore"</span> <span class="p">:</span> <span class="s">"Expire"</span><span class="p">)</span> <span class="p">{</span>
                        <span class="nf">toggleExpirationSimulation</span><span class="p">()</span>
                    <span class="p">}</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
</code></pre></div></div>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">ExpirationModifier</span><span class="p">:</span> <span class="kt">ViewModifier</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">date</span><span class="p">:</span> <span class="kt">Date</span>
    <span class="k">var</span> <span class="nv">referenceDate</span><span class="p">:</span> <span class="kt">Date</span> <span class="o">=</span> <span class="o">.</span><span class="n">now</span>

    <span class="kd">func</span> <span class="nf">body</span><span class="p">(</span><span class="nv">content</span><span class="p">:</span> <span class="kt">Content</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">if</span> <span class="n">date</span> <span class="o">&lt;</span> <span class="n">referenceDate</span> <span class="p">{</span>
            <span class="n">content</span><span class="o">.</span><span class="nf">opacity</span><span class="p">(</span><span class="mf">0.3</span><span class="p">)</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="n">content</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>此时执行点击左上角 Expire 按钮来模拟所有的零食过期。SwiftUI Instruments 给出的视图更新次数如下：</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2025/swiftui_identity_lifetime/assets/09.jpg" alt="条件分支切换过期状态后，SwiftUI Instruments 显示 TreatCell body 创建 2 次" /></p>

<p>代表两种零食的 TreatCell 创建了两次，但实际上零食过期后，只需置灰即可，不需要重新创建 TreatCell。</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">ExpirationModifier</span><span class="p">:</span> <span class="kt">ViewModifier</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">date</span><span class="p">:</span> <span class="kt">Date</span>
    <span class="k">var</span> <span class="nv">referenceDate</span><span class="p">:</span> <span class="kt">Date</span> <span class="o">=</span> <span class="o">.</span><span class="n">now</span>

    <span class="kd">func</span> <span class="nf">body</span><span class="p">(</span><span class="nv">content</span><span class="p">:</span> <span class="kt">Content</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="k">if</span> <span class="n">date</span> <span class="o">&lt;</span> <span class="n">referenceDate</span> <span class="p">{</span> <span class="c1">// Structural Identity</span>
            <span class="n">content</span><span class="o">.</span><span class="nf">opacity</span><span class="p">(</span><span class="mf">0.3</span><span class="p">)</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="n">content</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>使用了 <code class="language-plaintext highlighter-rouge">ExpirationModifier</code> 之后，<code class="language-plaintext highlighter-rouge">ForEach</code> 中的 <code class="language-plaintext highlighter-rouge">Content View</code> 的 Identity 属于 Structural Identity。</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ForEach 中的 Content View</span>
<span class="kt">TreatCell</span><span class="p">(</span><span class="nv">treat</span><span class="p">:</span> <span class="n">treat</span><span class="p">)</span>
 <span class="o">.</span><span class="nf">modifier</span><span class="p">(</span><span class="kt">ExpirationModifier</span><span class="p">(</span><span class="nv">date</span><span class="p">:</span> <span class="n">treat</span><span class="o">.</span><span class="n">expiryDate</span><span class="p">,</span> <span class="nv">referenceDate</span><span class="p">:</span> <span class="n">referenceDate</span><span class="p">))</span>
</code></pre></div></div>

<p>根据之前的经验，我们可以通过这种方式来优化，即去除 if 分支：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">ExpirationModifier</span><span class="p">:</span> <span class="kt">ViewModifier</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">date</span><span class="p">:</span> <span class="kt">Date</span>
    <span class="k">var</span> <span class="nv">referenceDate</span><span class="p">:</span> <span class="kt">Date</span> <span class="o">=</span> <span class="o">.</span><span class="n">now</span>

    <span class="kd">func</span> <span class="nf">body</span><span class="p">(</span><span class="nv">content</span><span class="p">:</span> <span class="kt">Content</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="n">content</span><span class="o">.</span><span class="nf">opacity</span><span class="p">(</span><span class="n">date</span> <span class="o">&lt;</span> <span class="n">referenceDate</span> <span class="p">?</span> <span class="mf">0.3</span> <span class="p">:</span> <span class="mi">1</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>运行 SwiftUI Instruments，再次点击左上角 Expire 按钮，可以看到：</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2025/swiftui_identity_lifetime/assets/10.jpg" alt="移除条件分支后，Instruments 只记录 ExpirationModifier body 的 7 次更新" /></p>

<p><code class="language-plaintext highlighter-rouge">TreatCell.body</code> 没有再被执行过了！所以在 View 中使用 if 分支时，可以停下来问自己：我是要多个视图还是某一视图的两种状态呢？引入不必要的分支，可能会导致性能下降，甚至视图的状态丢失。</p>

<h2 id="dependency">Dependency</h2>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2025/swiftui_identity_lifetime/assets/11.jpg" alt="DogView 代码中依赖、body 和 action 的对应区域" /></p>

<p>（本节不是本文讨论的重点，于是我放上 AI 对此的总结）</p>

<ul>
  <li>依赖的含义：视图的输入（属性、@State/@Binding/@Environment/ObservableObject 等）。依赖一旦变更，对应视图就必须重新计算 body。</li>
  <li>依赖关系的结构：虽然视图层级像树，但同一个依赖可被多处使用，整体更像一张依赖图。某个依赖变化时，SwiftUI 只让依赖它的视图重建，而不是全量刷新。</li>
  <li>身份在其中的作用：视图的身份（显式 id 或结构身份）决定依赖变化如何被路由到具体节点；身份稳定，就能更好地保持状态和避免多余重建。</li>
  <li>选择稳定且唯一的标识：数据驱动的视图（ForEach 等）需要稳定、唯一的 id。随机 id 或数组下标会让身份漂移，造成闪烁、错误动画或状态丢失。</li>
  <li>精简条件分支：多余的 if/switch 会制造额外的身份节点，导致重复重建。能用「惰性」修饰符（如条件放进 opacity，1 时无影响）折叠条件时，更新会更集中高效。</li>
  <li>更新流程心智模型：依赖变化 → 标记相关视图 → 重新求 body 生成新值；值本身是短暂对比用的，持久状态与视图身份绑定并随身份存续。</li>
</ul>

<h2 id="回顾">回顾</h2>

<p>还记得文章开头我提及的奇怪 BUG 吗？这是能复现的 <a href="https://github.com/SunsetWan/WWDC-21-Demystify-SwiftUI-code-example/blob/main/unstable-navigation/unstable-navigation/ContentView.swift#L15">demo</a>，而且在 iOS 17.1.2 上能稳定复现。</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">typealias</span> <span class="kt">DetailState</span> <span class="o">=</span> <span class="kt">DetailViewModel</span><span class="o">.</span><span class="kt">State</span>

<span class="kd">final</span> <span class="kd">class</span> <span class="kt">DetailViewModel</span><span class="p">:</span> <span class="kt">ObservableObject</span> <span class="p">{</span>
    <span class="kd">struct</span> <span class="kt">State</span><span class="p">:</span> <span class="kt">Identifiable</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">title</span><span class="p">:</span> <span class="kt">String</span>
        <span class="k">let</span> <span class="nv">subtitle</span><span class="p">:</span> <span class="kt">String</span>
        <span class="k">var</span> <span class="nv">text</span><span class="p">:</span> <span class="kt">String</span> <span class="o">=</span> <span class="s">"123"</span>
<span class="c1">//</span>
        <span class="k">var</span> <span class="nv">id</span><span class="p">:</span> <span class="kt">UUID</span> <span class="p">{</span>
            <span class="k">let</span> <span class="nv">newID</span> <span class="o">=</span> <span class="kt">UUID</span><span class="p">()</span>
            <span class="nf">print</span><span class="p">(</span><span class="s">"🆔🆔🆔 [DetailState] id accessed, generated: </span><span class="se">\(</span><span class="n">newID</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
            <span class="k">return</span> <span class="n">newID</span>
        <span class="p">}</span>

        <span class="c1">// Solution: Use stable explicit identity</span>
<span class="c1">//        let id = UUID()</span>
    <span class="p">}</span>

    <span class="kd">@Published</span> <span class="k">var</span> <span class="nv">state</span><span class="p">:</span> <span class="kt">State</span>

    <span class="c1">// MARK: - Textfield logic</span>

    <span class="nf">init</span><span class="p">(</span><span class="nv">state</span><span class="p">:</span> <span class="kt">State</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">self</span><span class="o">.</span><span class="n">state</span> <span class="o">=</span> <span class="n">state</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>核心就在于通过 Navigator push Detail 页面时，DetailState 的 explicit identity 不稳定。每次求值 <code class="language-plaintext highlighter-rouge">body</code> 都会重新访问 <code class="language-plaintext highlighter-rouge">State.id</code>，计算属性每次生成新的 UUID，导致 <code class="language-plaintext highlighter-rouge">Identifiable</code> 身份变化。SwiftUI 认为当前 NavigationStack 顶端元素变了，于是销毁旧的 Detail 页面并创建新的 Detail 页面。</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">Button</span><span class="p">(</span><span class="s">"Show Detail"</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">state</span><span class="p">:</span> <span class="kt">DetailState</span> <span class="o">=</span> <span class="o">.</span><span class="nf">init</span><span class="p">(</span><span class="nv">title</span><span class="p">:</span> <span class="s">"Detail Title"</span><span class="p">,</span> <span class="nv">subtitle</span><span class="p">:</span> <span class="s">"Detail subtitle"</span><span class="p">)</span>
    <span class="n">navigator</span><span class="o">.</span><span class="nf">push</span><span class="p">(</span><span class="nv">to</span><span class="p">:</span> <span class="o">.</span><span class="nf">detail</span><span class="p">(</span><span class="n">state</span><span class="p">))</span>
<span class="p">}</span>
</code></pre></div></div>

<p>运行 SwiftUI Instruments 来看看视图的变化次数：</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2025/swiftui_identity_lifetime/assets/12.jpg" alt="SwiftUI Instruments 显示 DetailView 更新约 4900 次并出现严重卡顿" /></p>

<p>从上图可以看出：在短短几秒之内，DetailView 更新了将近 5000 次，SwiftUI 内部的 NavigationStack 更新了将近一万五千次，整个 App 出现了严重的卡顿！</p>]]></content><author><name>Sunset Wan</name></author><category term="SwiftUI" /><summary type="html"><![CDATA[前阵子遇到一个很奇怪的 BUG：用 NavigationStack push 一个 View，CPU 使用率竟然冲上 100%！当 Xcode 运行一段 SwiftUI 代码时，我们应该关注哪些方面？特此回顾 WWDC 21 Demystify SwiftUI。]]></summary></entry><entry><title type="html">Protocol Witness in Swift</title><link href="https://sunsetwan.github.io/2024/06/23/Swift-Protocol-Witness/" rel="alternate" type="text/html" title="Protocol Witness in Swift" /><published>2024-06-23T21:23:49+08:00</published><updated>2024-06-23T21:23:49+08:00</updated><id>https://sunsetwan.github.io/2024/06/23/Swift-Protocol-Witness</id><content type="html" xml:base="https://sunsetwan.github.io/2024/06/23/Swift-Protocol-Witness/"><![CDATA[<p>在 Swift 中，协议是非常重要的部分之一。使用协议，可以让我们写出灵活的代码，而不必拘泥于接口和实现的耦合，但协议也有自己的问题。<!-- more --></p>

<h2 id="问题-1">问题 1</h2>

<pre><code class="language-Swift">protocol A {}

protocol B {}

extension B: A {} // 🛑 Extension of protocol 'B' cannot have an inheritance clause
</code></pre>

<p>我们无法在协议 B 的扩展中继承协议 A。</p>

<pre><code class="language-Swift">protocol A {}

protocol B: A {} // 🟢
</code></pre>

<p>只有在协议 B 声明时，才能建立继承关系。当协议 B 非我们自己声明时，那就无计可施了。</p>

<h2 id="问题-2">问题 2</h2>

<p>遵循协议的类型只能以一种方式来实现协议的要求。</p>

<pre><code class="language-Swift">indirect enum Tree&lt;A&gt; {
    case empty
    case node(left: Tree&lt;A&gt;, value: A, right: Tree&lt;A&gt;)
}

// ⚠️ Types are only allowed to conform to a protocol in one single way.
extension Tree: Sequence {
    // in-order?
    // pre-order?
    // post-order?
}
</code></pre>

<p>我们利用 Swift 中的 enum 来定义树的结构，并期望 <code class="language-plaintext highlighter-rouge">Tree</code> 遵循 <code class="language-plaintext highlighter-rouge">Sequence</code> 协议。那么遍历 <code class="language-plaintext highlighter-rouge">Tree</code> 时，用哪种方式去遍历是固定的，是根据 <code class="language-plaintext highlighter-rouge">IteratorProtocol</code> 协议的实现来定的。</p>

<h2 id="问题-3">问题 3</h2>

<p>有些类型无法遵循协议，如 <code class="language-plaintext highlighter-rouge">Tuple</code>。</p>

<pre><code class="language-Swift">let tuple1 = (1, 1)
let tuple2 = (2, 2)

@discardableResult
func isEqual&lt;T: Equatable&gt;(lhs: T, rhs: T) -&gt; Bool {
    lhs == rhs
}

isEqual(lhs: tuple1, rhs: tuple2) // 🛑 Type '(Int, Int)' cannot conform to 'Equatable'
</code></pre>

<h2 id="protocol-witnesses-协议目击者">Protocol Witnesses （协议目击者）</h2>
<blockquote>

  <p>Protocol witnesses are generated by the Swift compiler, when a protocol is used in a generic context, the compiler generates a separate implementation for each concrete type that conforms to the protocols. These implementations called protocol witnesses, are optimized for the specific type.</p>
</blockquote>

<p>简而言之，Protocol Witnesses 是 Swift 编译器生成的一种数据结构，这种数据结构里包含了遵循某个协议的实现细节。也就是说，如果 Swift 中没有设计协议，我们也能利用这种数据结构实现协议的特性。</p>

<pre><code class="language-Swift">protocol Combinable {
    func combine(_ other: Self) -&gt; Self
}

extension Int: Combinable {
    func combine(_ other: Int) -&gt; Int {
        self + other
    }
}
</code></pre>

<p><code class="language-plaintext highlighter-rouge">Combinable</code> 是个很简单的协议，我们让 <code class="language-plaintext highlighter-rouge">Int</code> 遵循 <code class="language-plaintext highlighter-rouge">Combinable</code> 协议，并用整型数的加法来满足协议要求。</p>

<p>如果 <code class="language-plaintext highlighter-rouge">Double</code> 和 <code class="language-plaintext highlighter-rouge">Float</code> 类型也想变成 <code class="language-plaintext highlighter-rouge">Combinable</code> 呢？</p>

<p>😐 嗯，继续添加对应的实现呗！</p>

<pre><code class="language-Swift">extension Double: Combinable {
    func combine(_ other: Double) -&gt; Double {
        self + other
    }
}

extension Float: Combinable {
    func combine(_ other: Float) -&gt; Float {
        self + other
    }
}
</code></pre>

<p>似乎闻到了坏代码的味道！😷</p>

<p>我们转头一想，<code class="language-plaintext highlighter-rouge">Int</code>，<code class="language-plaintext highlighter-rouge">Double</code>，<code class="language-plaintext highlighter-rouge">Float</code> 都是遵循 <code class="language-plaintext highlighter-rouge">Numeric</code> 协议的，那么直接让 <code class="language-plaintext highlighter-rouge">Numeric</code> 协议继承自 <code class="language-plaintext highlighter-rouge">Combinable</code>，似乎问题就解决了！</p>

<pre><code class="language-Swift">// 🛑 Extension of protocol 'Numeric' cannot have an inheritance clause
extension Numeric: Combinable {

}
</code></pre>

<p>😇 还是不行！🙅</p>

<h3 id="解决问题-1">解决问题 1</h3>

<p>让我们写出 <code class="language-plaintext highlighter-rouge">Combinable</code> 协议对应的 Witness 来解决这个问题。<code class="language-plaintext highlighter-rouge">Combining&lt;A&gt;</code> 是<code class="language-plaintext highlighter-rouge">Combinable</code> 协议的 Witness。</p>

<pre><code class="language-Swift">struct Combining&lt;A&gt; {
    let combine: (A, A) -&gt; A
}
</code></pre>

<p>再写出加法的实现：</p>

<pre><code class="language-Swift">extension Combining where A: Numeric {
    static var sum: Combining {
        Combining { $0 + $1 }
    }
}
</code></pre>

<p>最后，</p>

<pre><code class="language-Swift">Combining&lt;Int&gt;.sum.combine(1, 2) // Output: Int 3

Combining&lt;Double&gt;.sum.combine(1.0, 2.0) // Output: Double 3.0
</code></pre>

<h3 id="解决问题-2">解决问题 2</h3>

<p><code class="language-plaintext highlighter-rouge">Int</code> 变成 <code class="language-plaintext highlighter-rouge">Combinable</code>，不应该只有加法这一种实现方式，也可以是两个 <code class="language-plaintext highlighter-rouge">Int</code> 做乘法。</p>

<pre><code class="language-Swift">extension Combining where A: Numeric {
    static var sum: Combining {
        Combining { $0 + $1 }
    }

    static var prod: Combining {
        Combining { $0 * $1 }
    }
}
</code></pre>

<p>让我们看一个复杂点的例子：</p>

<pre><code class="language-Swift">extension Array where Element: Combinable {
    func reduce(_ initial: Element) -&gt; Element {
        self.reduce(initial) {
            $0.combine($1)
        }
    }
}

// 转为使用 Combining&lt;A&gt;
extension Array {
    func reduce(_ initial: Element, _ combining: Combining&lt;Element&gt;) -&gt; Element {
        return reduce(initial, combining.combine)
    }
}

[1, 3, 5].reduce(0, .sum) // Output: 9
[CGFloat(2), 3, 5].reduce(1, .prod) // Output: 30
</code></pre>

<p>我们只需传入不同实现的 Witness 即可实现同一协议方法的不同实现。</p>

<h3 id="解决问题-3">解决问题 3</h3>

<pre><code class="language-Swift">let tuple1 = (1, 1)
let tuple2 = (2, 2)

@discardableResult
func isEqual&lt;T: Equatable&gt;(lhs: T, rhs: T) -&gt; Bool {
    lhs == rhs
}

isEqual(lhs: tuple1, rhs: tuple2) // 🛑 Type '(Int, Int)' cannot conform to 'Equatable'
</code></pre>

<p>以 <code class="language-plaintext highlighter-rouge">Array</code> 为例，如果 Element 是 <code class="language-plaintext highlighter-rouge">Equatable</code> 的，那么整个 <code class="language-plaintext highlighter-rouge">Array</code> 也是 <code class="language-plaintext highlighter-rouge">Equatable</code> 的。那么同理 <code class="language-plaintext highlighter-rouge">(1, 1)</code> 这个 tuple 也应该是 <code class="language-plaintext highlighter-rouge">Equatable</code> 或者 <code class="language-plaintext highlighter-rouge">Combinable</code> 的，因为整型数 1 是 <code class="language-plaintext highlighter-rouge">Equatable</code> 的，也是 <code class="language-plaintext highlighter-rouge">Combinable</code> 的！</p>

<p>如果目标是证明： <code class="language-plaintext highlighter-rouge">(A, B)</code> 是 <code class="language-plaintext highlighter-rouge">Combinable</code> 的，那么我们需要拿出 <code class="language-plaintext highlighter-rouge">Combining&lt;(A, B)&gt;</code> 这个 witness 才能证明！但我们现在只有 <code class="language-plaintext highlighter-rouge">Combining&lt;A&gt;</code> 和 <code class="language-plaintext highlighter-rouge">Combining&lt;B&gt;</code>，能以此为基础推导出 <code class="language-plaintext highlighter-rouge">Combining&lt;(A, B)&gt;</code> 吗？</p>

<p>答案是可以：</p>

<pre><code class="language-Swift">func zip&lt;A, B&gt;(_ a: Combining&lt;A&gt;, _ b: Combining&lt;B&gt;) -&gt; Combining&lt;(A, B)&gt; {
    Combining { lhs, rhs in
        (a.combine(lhs.0, rhs.0), b.combine(lhs.1, rhs.1))
    }
}

[
    (1, 1.1),
    (2, 2),
    (3, 3),
].reduce((1, 0), zip(.prod, .sum)) // Output: (6, 6.1)
</code></pre>

<h2 id="收获">收获</h2>

<p>前面的内容讲了不少 Protocol Witness 的内容，但既然 Swift 编译器已经把这些细节隐藏起来了，那我们知道这些有什么用呢？请看下面一个例子：</p>

<pre><code class="language-Swift">protocol 动物 {
    func 走路()
}

extension 动物 {
    func 走路() {
        print("我用 4 条腿走路")
    }

    func 奔跑() {
        print("我用 4 条腿奔跑")
    }

    func 跑跑跳跳() {
        走路()
        奔跑()
    }
}

struct 蜘蛛: 动物 {
    func 走路() {
        print("我用 8 条腿走路")
    }

    func 奔跑() {
        print("我用 8 条腿奔跑")
    }
}

let 小蜘蛛 = 蜘蛛()
小蜘蛛.跑跑跳跳()
// Output:
// 我用 8 条腿走路
// 我用 4 条腿奔跑
</code></pre>

<p>我们可以用 Protocol Witness 的知识来说明这个输出的原因。在 Swift 协议中，只有被要求的函数才会进行动态派发，即只有在 Protocol Witness 中的方法才会进行动态派发。</p>

<p>所以，我们可以想象小蜘蛛的 <code class="language-plaintext highlighter-rouge">走路</code> 和 <code class="language-plaintext highlighter-rouge">奔跑</code> 方法中都有一个隐藏的参数（<code class="language-plaintext highlighter-rouge">动物</code> 的 witness）。</p>

<p>在小蜘蛛的 <code class="language-plaintext highlighter-rouge">走路</code> 方法执行时，发现 witness 中有 <code class="language-plaintext highlighter-rouge">走路</code> 方法的实现，即执行动态派发的方法 <code class="language-plaintext highlighter-rouge">走路</code>（输出：”我用 8 条腿走路”）。</p>

<p>在小蜘蛛的 <code class="language-plaintext highlighter-rouge">奔跑</code> 方法执行时，发现 witness 中并没有 <code class="language-plaintext highlighter-rouge">奔跑</code> 方法的实现，即执行协议扩展中静态派发的方法 <code class="language-plaintext highlighter-rouge">奔跑</code>（输出：”我用 4 条腿奔跑”）</p>

<p>很明显，蜘蛛不光用 8 条腿走路，它们还可能是用 8 条腿奔跑的。</p>

<p>那么我们只需把 <code class="language-plaintext highlighter-rouge">奔跑</code> 方法加入 <code class="language-plaintext highlighter-rouge">动物</code> 协议的 witness 中，即把 <code class="language-plaintext highlighter-rouge">奔跑</code> 方法加入 <code class="language-plaintext highlighter-rouge">动物</code> 协议中，使其成为协议约束的一部分！这样就可以让这只小蜘蛛也用上 8 条腿奔跑了！</p>

<pre><code class="language-Swift">protocol 动物 {
    func 走路()
    func 奔跑()
}

...
...

let 小蜘蛛 = 蜘蛛()
小蜘蛛.跑跑跳跳()
// Output:
// 我用 8 条腿走路
// 我用 8 条腿奔跑
</code></pre>

<h2 id="wwdc-24-中也提到了-protocol-witness">WWDC 24 中也提到了 Protocol Witness</h2>

<p>于 <a href="https://developer.apple.com/videos/play/wwdc2024/10217/">Explore Swift performance</a> 视频 [31: 04] 处：<br />
<img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2024/wwdc24_swift_performance.jpg" alt="DataModel 协议要求与 DataModelWitnessTable 中函数指针的对照" /></p>

<p>左边为 <code class="language-plaintext highlighter-rouge">DataModel</code> 协议，右边为 <code class="language-plaintext highlighter-rouge">DataModel</code> 协议的 Witness。</p>

<p>在每个协议要求的方法中，均有一个额外的隐藏参数——指向 witness 的指针。</p>]]></content><author><name>Sunset Wan</name></author><category term="Protocol" /><summary type="html"><![CDATA[在 Swift 中，协议是非常重要的部分之一。使用协议，可以让我们写出灵活的代码，而不必拘泥于接口和实现的耦合，但协议也有自己的问题。]]></summary></entry><entry><title type="html">Alamofire 源码阅读（一）</title><link href="https://sunsetwan.github.io/2024/06/15/Alamofire-%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%E7%AC%94%E8%AE%B0/" rel="alternate" type="text/html" title="Alamofire 源码阅读（一）" /><published>2024-06-15T00:01:13+08:00</published><updated>2024-06-15T00:01:13+08:00</updated><id>https://sunsetwan.github.io/2024/06/15/Alamofire-%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%E7%AC%94%E8%AE%B0</id><content type="html" xml:base="https://sunsetwan.github.io/2024/06/15/Alamofire-%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%E7%AC%94%E8%AE%B0/"><![CDATA[<p>阅读 Alamofire 5.9.1 版本的源代码，有些收获，记录于此。<!-- more --></p>

<h2 id="创建-urlsession">创建 URLSession</h2>

<p>AF 在初始化 <code class="language-plaintext highlighter-rouge">URLSession</code> 时，使用到三个队列，分别是：rootQueue、requestQueue 和 serializationQueue。rootQueue 默认为 DispatchQueue(label: “org.alamofire.session.rootQueue”)。</p>

<p>而 requestQueue 和 serializationQueue 默认均是以 rootQueue 为 target 的串行队列。也就是说，提交到 requestQueue 上的 task A 是会在 rootQueue 上执行的。</p>

<p>假设 iOS 会给 rootQueue 分配一个线程 T1，那么提交到 requestQueue 和 rootQueue 上的任务均会在线程 T1 上执行。</p>

<p>如果 requestQueue 自己是个单独的串行队列呢？iOS 会专门给 requestQueue 分配另一个线程 T2，这样 App 就可能使用了不必要的线程资源。</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2024/AF_01.jpg" alt="Alamofire 的 requestQueue 和 serializationQueue 都以 rootQueue 为目标队列，共用其执行线程" /></p>

<p>当存在过多线程时，不仅会增加内存压力，更重要的是，这些线程在有限的 CPU 核心上运行，导致频繁的线程上下文切换。有时候，与实际执行需要的指令相比，这些上下文切换所消耗的资源和时间反而占据了主要部分。这种情况下，由于线程被阻塞的同时，又不断有新的任务以 async 的方式提交到并行队列，导致过多的新线程被创建，我们将其称为<strong>线程爆炸</strong>。</p>

<p>Apple 也在 DispatchQueue <a href="https://developer.apple.com/documentation/dispatch/dispatchqueue#3136752">文档</a> 中提示我们避免创建过多线程，限制线程数量可以避免资源的过度占用和性能下降。</p>

<h2 id="af-创建-request-的流程">AF 创建 Request 的流程</h2>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2024/af_create_data_request.png" alt="Alamofire 从 Session 发起请求并创建 DataRequest 的调用流程" /></p>

<h2 id="af-处理-response-的流程">AF 处理 Response 的流程</h2>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2024/af_response.png" alt="Alamofire 接收 URLSession 回调并序列化 Response 的处理流程" /></p>

<h2 id="af-中是如何解决数据竞争的">AF 中是如何解决数据竞争的？</h2>

<p>AF 中是利用泛型类 <a href="https://github.com/Alamofire/Alamofire/blob/master/Source/Core/Protected.swift">Protected&lt;Value&gt;</a> 来实现线程安全的读和写。</p>

<p>泛型类 <code class="language-plaintext highlighter-rouge">Protected&lt;Value&gt;</code> 的核心思路是利用锁的机制来实现线程同步。除此以外，还利用到<code class="language-plaintext highlighter-rouge">@dynamicMemberLookup</code> 和 <code class="language-plaintext highlighter-rouge">KeyPath</code> 的特性，以方便地访问/修改 Value 的成员变量（属性）。</p>

<p>我也借鉴它的思路，解决了工作中的一个数据竞争导致的崩溃（EXC_BAD_ACCESS KERN_INVALID_ADDRESS）。在读取硬件设备的数据时，业务层会以并发的方式发送蓝牙指令，而蓝牙发送工具类中使用一个结构体 <code class="language-plaintext highlighter-rouge">FIFO</code> 队列来控制蓝牙指令的执行顺序。</p>

<pre><code class="language-Swift">public protocol Queue {
    associatedtype Element
    mutating func enqueue(_ element: Element)
    mutating func dequeue() -&gt; Element?
}

public struct FIFOQueue&lt;Element&gt;: Queue {
    ...
    ...
}
</code></pre>

<p>该结构体 <code class="language-plaintext highlighter-rouge">FIFO</code> 队列的入队出队方法均是 <code class="language-plaintext highlighter-rouge">mutating</code> 方法，结构体的 <code class="language-plaintext highlighter-rouge">mutating</code> 方法本质上是把当前的值拷贝一份，作出修改后，重新赋值给 <code class="language-plaintext highlighter-rouge">self</code>。</p>

<p>假设要发出的两个蓝牙指令分别在不同的线程中发送，那么这两个线程就有可能同时执行 <code class="language-plaintext highlighter-rouge">FIFO</code> 队列的入队方法，即出现数据竞争的情况。</p>

<p>在定位到这个崩溃的根本原因后，利用泛型类 <code class="language-plaintext highlighter-rouge">Protected&lt;Value&gt;</code> 就能很方便地解决问题。</p>

<pre><code class="language-Swift">extension FIFOQueue {
    /// Return a thread-safe reference of a `FIFOQueue`.
    public static func makeThreadSafeReference(_ elements: [Element] = []) -&gt; Protected&lt;Self&gt; {
        Protected(Self.init(elements))
    }
}

extension Protected: Queue where Value: Queue {
    public typealias Element = Value.Element

    public func enqueue(_ element: Value.Element) {
        write { queue in queue.enqueue(element) }
    }

    public func dequeue() -&gt; Value.Element? {
        write { queue in queue.dequeue() }
    }
}
</code></pre>

<p>在添加上述代码之后，只需改动蓝牙发送工具类中初始化 FIFO 队列 一行代码。</p>

<pre><code class="language-Swift">// Before
var queue = FIFOQueue&lt;Value&gt;()

// After
var queue = FIFOQueue&lt;Value&gt;.makeThreadSafeReference()
</code></pre>

<p>这一刻，我觉得 Swift 是真的优雅！</p>

<h2 id="ssl-pinning">SSL Pinning</h2>

<p>AF 也内置了常见的 SSL pinning 方式，比如通过固定证书的方式和使用公钥的方式等。</p>

<blockquote>
  <p>For projects deploying to iOS 14, tvOS 14, watchOS 7, or macOS 11 or later, <a href="https://developer.apple.com/news/?id=g9ejcf8y">Apple now provides built in pinning capabilities</a> configurable in your app’s Info.plist. Please use that capability before implementing your own using Alamofire.</p>
</blockquote>

<p>但如果 iOS 项目是最低支持 iOS 14 的，AF 优先推荐使用 iOS 系统内置的方案来实现 SSL Pinning。</p>

<h3 id="什么是-pinning">什么是 Pinning？</h3>
<p>把主机名（Host）与预设好的 X509 证书或者公钥关联起来的过程叫做 pinning。iOS App 中可内置关联好的证书或公钥来防止中间人攻击（Man-in-the-middle attack）。</p>

<h3 id="什么是中间人攻击man-in-the-middle-attack">什么是中间人攻击（Man-in-the-middle attack）</h3>
<p>在使用非对称加密中，Alice 若想与 Bob 进行安全的通信，Alice 需要拿到 Bob 的公钥，然后 Alice 使用 Bob 的公钥加密通信内容。而加密后的内容只能以 Bob 的私钥解密，这似乎听起来很安全。那么设想这样一个场景：</p>

<ol>
  <li>Alice 向 Bob 发送：嗨！Bob 请给我你的公钥。</li>
  <li>Jack 窃取到了这条信息，且转发给了 Bob。</li>
  <li>Bob 把自己的公钥发送给 Alice。</li>
  <li>Jack 收到了 Bob 的公钥，但却把他自己的公钥发给 Alice。</li>
  <li>Alice 认为自己收到了 Bob 的公钥，其实是 Jack 的公钥。</li>
  <li>Alice 向 Bob 发送：今晚 7 点去吃麦当劳吧，并用（她认为的） Bob 的公钥加密。</li>
  <li>Jack 收到 Alice 用自己的公钥加密的信息后，用自己的私钥解密，并把信息篡改为“今晚 6 点去吃食其家吧”，再用 Bob 的公钥加密，发给 Bob。</li>
  <li>Bob 收到信息，用自己的私钥解密后，就去准备晚上 6 点和 Alice 在食其家碰头了。</li>
</ol>

<p>在上述场景中，Alice 需要一种方式来确保自己收到的公钥是 Bob 的公钥，而不是攻击者 Jack 的公钥。Bob 可以向权威机构申请一个证书，这个证书会包含 Bob 的公钥，由权威机构来给 Bob 的公钥作背书。</p>

<p>所以 iOS app 中常见的防止中间人攻击的方法之一就是预先把与公司主机名关联好的公钥内置在 app 中。在建立 HTTPS 安全连接时，读取证书链中的叶子节点证书，先通过证书的签名和所使用的加密算法来验证证书的完整性。完整性验证通过后，再对比公钥是否一致。</p>

<h2 id="cache">Cache</h2>
<p>AF 使用 <code class="language-plaintext highlighter-rouge">URLSession</code> 的系统实现来缓存网络请求的响应的，即默认存入 <code class="language-plaintext highlighter-rouge">URLCache.shared</code> 中。</p>

<p><code class="language-plaintext highlighter-rouge">URLSession</code> 的缓存策略（<code class="language-plaintext highlighter-rouge">requestCachePolicy</code>）有 6 种：</p>
<ol>
  <li>useProtocolCachePolicy
    <ol>
      <li>解释：遵循 HTTP 中的缓存机制，详见 <a href="https://datatracker.ietf.org/doc/html/rfc2616#autoid-129">Caching in HTTP</a>。</li>
    </ol>
  </li>
  <li>reloadIgnoringLocalCacheData
    <ol>
      <li>解释：不使用任何缓存数据。</li>
    </ol>
  </li>
  <li>reloadIgnoringLocalAndRemoteCacheData</li>
  <li>returnCacheDataElseLoad
    <ol>
      <li>解释：优先使用缓存数据，若没有缓存数据，则开始网络请求。</li>
    </ol>
  </li>
  <li>returnCacheDataDontLoad
    <ol>
      <li>解释：使用缓存数据，若没有缓存数据，则直接网络请求失败，这种行为有点像<strong>离线模式</strong>。</li>
    </ol>
  </li>
  <li>reloadRevalidatingCacheData</li>
</ol>

<p>我们还可以通过实现这个代理方法，来实现更细节的控制，比如阻止对某个特定 URL 的响应进行缓存。</p>
<pre><code class="language-Swift">    open func urlSession(_ session: URLSession,
                         dataTask: URLSessionDataTask,
                         willCacheResponse proposedResponse: CachedURLResponse,
                         completionHandler: @escaping (CachedURLResponse?) -&gt; Void) {
    // Add your implementation
    }
</code></pre>

<p>但需要注意的是，这个代理方法被调用的条件有些苛刻，得同时满足以下 7 个条件才行！</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2024/AF_02.jpg" alt="URLSession 调用 willCacheResponse 代理方法所需满足的七个条件" /></p>]]></content><author><name>Sunset Wan</name></author><category term="Alamofire" /><summary type="html"><![CDATA[阅读 Alamofire 5.9.1 版本的源代码，有些收获，记录于此。]]></summary></entry><entry><title type="html">Codable 学习笔记</title><link href="https://sunsetwan.github.io/2022/04/14/Codable/" rel="alternate" type="text/html" title="Codable 学习笔记" /><published>2022-04-14T09:41:00+08:00</published><updated>2022-04-14T09:41:00+08:00</updated><id>https://sunsetwan.github.io/2022/04/14/Codable</id><content type="html" xml:base="https://sunsetwan.github.io/2022/04/14/Codable/"><![CDATA[<p>本文为 Raywenderlich 出版的《Expert Swift》第 8 章节《Codable》阅读笔记。<!-- more --></p>

<p>Swift 4.0 就引入了 <code class="language-plaintext highlighter-rouge">Codable</code> 协议。话不多说，进入正题：<br />
示例 JSON：</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"accessKey"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"S|_|p3rs3cr37"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"AddressInfo"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"atmCode"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"1132"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"city"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"Albuquerque"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"street"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"3828 Piermont Drive"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"zip"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="mi">87112</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"ContactInfo"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"addedOn"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"04-14-2022 13:58"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"cellularPhone"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"+972 542-288-482"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"email"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"freak4pc@gmail.com"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"homePhone"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"+1 212-741-4695"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"website"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"http://github.com/freak4pc"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"family"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="s2">"Tom"</span><span class="p">,</span><span class="w">
    </span><span class="s2">"Elia"</span><span class="w">
  </span><span class="p">],</span><span class="w">
  </span><span class="nl">"name"</span><span class="w"> </span><span class="p">:</span><span class="w"> </span><span class="s2">"Shai Mishali"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<h2 id="如何-decode">如何 Decode？</h2>
<p>即如何解析这段 JSON？</p>
<h3 id="维持原有层级结构">维持原有层级结构</h3>
<p>这个 JSON 对应的 Swift 模型为：</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">Foundation</span>

<span class="kd">struct</span> <span class="kt">Customer1</span><span class="p">:</span> <span class="kt">Codable</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">accessKey</span><span class="p">:</span> <span class="kt">String</span>
    <span class="k">let</span> <span class="nv">addressInfo</span><span class="p">:</span> <span class="kt">AddressInfo</span>
    <span class="k">let</span> <span class="nv">contactInfo</span><span class="p">:</span> <span class="kt">ContactInfo</span>
    <span class="k">let</span> <span class="nv">family</span><span class="p">:</span> <span class="p">[</span><span class="kt">String</span><span class="p">]</span>
    <span class="k">let</span> <span class="nv">name</span><span class="p">:</span> <span class="kt">String</span>

    <span class="kd">enum</span> <span class="kt">CodingKeys</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> <span class="kt">CodingKey</span> <span class="p">{</span>
        <span class="k">case</span> <span class="n">accessKey</span>
        <span class="k">case</span> <span class="n">addressInfo</span>
        <span class="k">case</span> <span class="n">contactInfo</span>
        <span class="k">case</span> <span class="n">family</span><span class="p">,</span> <span class="n">name</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// MARK: - AddressInfo</span>
<span class="kd">struct</span> <span class="kt">AddressInfo</span><span class="p">:</span> <span class="kt">Codable</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">atmCode</span><span class="p">,</span> <span class="n">city</span><span class="p">,</span> <span class="nv">street</span><span class="p">:</span> <span class="kt">String</span>
    <span class="k">let</span> <span class="nv">zip</span><span class="p">:</span> <span class="kt">Int</span>
<span class="p">}</span>

<span class="c1">// MARK: - ContactInfo</span>
<span class="kd">struct</span> <span class="kt">ContactInfo</span><span class="p">:</span> <span class="kt">Codable</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">addedOn</span><span class="p">,</span> <span class="n">cellularPhone</span><span class="p">,</span> <span class="n">email</span><span class="p">,</span> <span class="nv">homePhone</span><span class="p">:</span> <span class="kt">String</span>
    <span class="k">let</span> <span class="nv">website</span><span class="p">:</span> <span class="kt">String</span>
<span class="p">}</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">Customer1</code> 结构体模型的层级结构与 JSON 对象的层级结构保持一致。这样就可以利用 <code class="language-plaintext highlighter-rouge">JSONDecoder</code> 把示例 JSON 成功地解析为 <code class="language-plaintext highlighter-rouge">Customer1</code> 结构体模型。</p>

<h3 id="自定义-json-对应模型的层级结构">自定义 JSON 对应模型的层级结构</h3>
<p>以上述 <code class="language-plaintext highlighter-rouge">Customer1</code> 模型为例，如果想访问该顾客所在的城市：</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">jack</span> <span class="o">=</span> <span class="kt">Customer1</span><span class="p">()</span>
<span class="k">let</span> <span class="nv">city</span> <span class="o">=</span> <span class="n">jack</span><span class="o">.</span><span class="n">addressInfo</span><span class="o">.</span><span class="n">city</span>
</code></pre></div></div>

<p>如果想直接通过 <code class="language-plaintext highlighter-rouge">jack.city</code> 来访问 jack 所在的城市信息，该怎么处理？</p>
<h4 id="1-使用计算属性">1. 使用计算属性</h4>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">var</span> <span class="nv">city</span><span class="p">:</span> <span class="kt">String</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">addressInfo</span><span class="o">.</span><span class="n">city</span>
    <span class="p">}</span>
</code></pre></div></div>
<p>但这种方式的弊端是：万一如果想把 <code class="language-plaintext highlighter-rouge">AddressInfo</code> 这嵌套的一层直接去掉，得手写好多类似上面的代码。</p>

<h4 id="2-使用嵌套的-codingkey-枚举">2. 使用嵌套的 CodingKey 枚举</h4>
<p>使用嵌套的 CodingKey 枚举可以把具有层级的 JSON “拍扁”。</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="kd">enum</span> <span class="kt">CodingKeys</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> <span class="kt">CodingKey</span> <span class="p">{</span>
        <span class="k">case</span> <span class="n">name</span>
        <span class="k">case</span> <span class="n">accessKey</span>
        <span class="k">case</span> <span class="n">status</span>
        <span class="k">case</span> <span class="n">addressInfo</span> <span class="o">=</span> <span class="s">"AddressInfo"</span>
        <span class="k">case</span> <span class="n">contactInfo</span> <span class="o">=</span> <span class="s">"ContactInfo"</span>
    <span class="p">}</span>
    
    <span class="kd">enum</span> <span class="kt">AddressInfo</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> <span class="kt">CodingKey</span> <span class="p">{</span>
        <span class="k">case</span> <span class="n">atmCode</span>
        <span class="k">case</span> <span class="n">street</span>
        <span class="k">case</span> <span class="n">city</span>
        <span class="k">case</span> <span class="n">zip</span>
    <span class="p">}</span>
    
    <span class="kd">enum</span> <span class="kt">ContactInfo</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> <span class="kt">CodingKey</span> <span class="p">{</span>
        <span class="k">case</span> <span class="n">homePhone</span><span class="p">,</span> <span class="n">cellularPhone</span><span class="p">,</span> <span class="n">email</span><span class="p">,</span> <span class="n">website</span>
        <span class="k">case</span> <span class="n">addedOn</span>
    <span class="p">}</span>
</code></pre></div></div>
<p>还得自己实现对应的 <code class="language-plaintext highlighter-rouge">decode</code> 方法：</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nf">init</span><span class="p">(</span><span class="n">from</span> <span class="nv">decoder</span><span class="p">:</span> <span class="kt">Decoder</span><span class="p">)</span> <span class="k">throws</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">customer</span> <span class="o">=</span> <span class="k">try</span> <span class="n">decoder</span><span class="o">.</span><span class="nf">container</span><span class="p">(</span><span class="nv">keyedBy</span><span class="p">:</span> <span class="kt">CodingKeys</span><span class="o">.</span><span class="k">self</span><span class="p">)</span>
        <span class="k">let</span> <span class="nv">name</span> <span class="o">=</span> <span class="k">try</span> <span class="n">customer</span><span class="o">.</span><span class="nf">decode</span><span class="p">(</span><span class="kt">String</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">name</span><span class="p">)</span>
        <span class="k">let</span> <span class="nv">accessKey</span> <span class="o">=</span> <span class="k">try</span> <span class="n">customer</span><span class="o">.</span><span class="nf">decode</span><span class="p">(</span><span class="kt">String</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">accessKey</span><span class="p">)</span>
        <span class="k">let</span> <span class="nv">status</span> <span class="o">=</span> <span class="k">try</span> <span class="n">customer</span><span class="o">.</span><span class="nf">decode</span><span class="p">(</span><span class="kt">Int</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">status</span><span class="p">)</span>
        <span class="k">self</span><span class="o">.</span><span class="n">name</span> <span class="o">=</span> <span class="n">name</span>
        <span class="k">self</span><span class="o">.</span><span class="n">accessKey</span> <span class="o">=</span> <span class="n">accessKey</span>
        
        <span class="k">let</span> <span class="nv">addressInfo</span> <span class="o">=</span> <span class="k">try</span> <span class="n">customer</span><span class="o">.</span><span class="nf">nestedContainer</span><span class="p">(</span><span class="nv">keyedBy</span><span class="p">:</span> <span class="kt">AddressInfo</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">addressInfo</span><span class="p">)</span>
        <span class="k">let</span> <span class="nv">atmCode</span> <span class="o">=</span> <span class="k">try</span> <span class="n">addressInfo</span><span class="o">.</span><span class="nf">decode</span><span class="p">(</span><span class="kt">String</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">atmCode</span><span class="p">)</span>
        <span class="k">let</span> <span class="nv">street</span> <span class="o">=</span> <span class="k">try</span> <span class="n">addressInfo</span><span class="o">.</span><span class="nf">decode</span><span class="p">(</span><span class="kt">String</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">street</span><span class="p">)</span>
        <span class="k">let</span> <span class="nv">city</span> <span class="o">=</span> <span class="k">try</span> <span class="n">addressInfo</span><span class="o">.</span><span class="nf">decode</span><span class="p">(</span><span class="kt">String</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">city</span><span class="p">)</span>
        
        <span class="c1">// 当 JSON 对象中 `zip` key 对应的 value 类型是 String 时的兼容处理</span>
        <span class="k">if</span> <span class="k">case</span> <span class="k">let</span> <span class="nv">zipString</span><span class="p">?</span> <span class="o">=</span> <span class="k">try</span><span class="p">?</span> <span class="n">addressInfo</span><span class="o">.</span><span class="nf">decodeIfPresent</span><span class="p">(</span><span class="kt">String</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">zip</span><span class="p">),</span>
            <span class="k">let</span> <span class="nv">zipString</span> <span class="o">=</span> <span class="n">zipString</span>
        <span class="p">{</span>
            <span class="k">self</span><span class="o">.</span><span class="n">zip</span> <span class="o">=</span> <span class="kt">Int</span><span class="p">(</span><span class="n">zipString</span><span class="p">)</span> <span class="p">??</span> <span class="o">-</span><span class="mi">1</span>
        <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="k">let</span> <span class="nv">zipInt</span> <span class="o">=</span> <span class="k">try</span> <span class="n">addressInfo</span><span class="o">.</span><span class="nf">decodeIfPresent</span><span class="p">(</span><span class="kt">Int</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">zip</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">self</span><span class="o">.</span><span class="n">zip</span> <span class="o">=</span> <span class="n">zipInt</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="k">self</span><span class="o">.</span><span class="n">zip</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span>
        <span class="p">}</span>
        
        <span class="k">self</span><span class="o">.</span><span class="n">atmCode</span> <span class="o">=</span> <span class="n">atmCode</span>
        <span class="k">self</span><span class="o">.</span><span class="n">street</span> <span class="o">=</span> <span class="n">street</span>
        <span class="k">self</span><span class="o">.</span><span class="n">city</span> <span class="o">=</span> <span class="n">city</span>
        
        <span class="k">let</span> <span class="nv">contactInfo</span> <span class="o">=</span> <span class="k">try</span> <span class="n">customer</span><span class="o">.</span><span class="nf">nestedContainer</span><span class="p">(</span><span class="nv">keyedBy</span><span class="p">:</span> <span class="kt">ContactInfo</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">contactInfo</span><span class="p">)</span>
        <span class="k">let</span> <span class="nv">homePhone</span> <span class="o">=</span> <span class="k">try</span> <span class="n">contactInfo</span><span class="o">.</span><span class="nf">decode</span><span class="p">(</span><span class="kt">String</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">homePhone</span><span class="p">)</span>
        <span class="k">let</span> <span class="nv">cellularPhone</span> <span class="o">=</span> <span class="k">try</span> <span class="n">contactInfo</span><span class="o">.</span><span class="nf">decode</span><span class="p">(</span><span class="kt">String</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">cellularPhone</span><span class="p">)</span>
        <span class="k">let</span> <span class="nv">email</span> <span class="o">=</span> <span class="k">try</span> <span class="n">contactInfo</span><span class="o">.</span><span class="nf">decode</span><span class="p">(</span><span class="kt">String</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">email</span><span class="p">)</span>
        <span class="k">let</span> <span class="nv">website</span> <span class="o">=</span> <span class="k">try</span> <span class="n">contactInfo</span><span class="o">.</span><span class="nf">decode</span><span class="p">(</span><span class="kt">String</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">website</span><span class="p">)</span>
        <span class="k">let</span> <span class="nv">addedOn</span> <span class="o">=</span> <span class="k">try</span> <span class="n">contactInfo</span><span class="o">.</span><span class="nf">decode</span><span class="p">(</span><span class="kt">Date</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">addedOn</span><span class="p">)</span>
        <span class="k">self</span><span class="o">.</span><span class="n">homePhone</span> <span class="o">=</span> <span class="n">homePhone</span>
        <span class="k">self</span><span class="o">.</span><span class="n">cellularPhone</span> <span class="o">=</span> <span class="n">cellularPhone</span>
        <span class="k">self</span><span class="o">.</span><span class="n">email</span> <span class="o">=</span> <span class="n">email</span>
        <span class="k">self</span><span class="o">.</span><span class="n">website</span> <span class="o">=</span> <span class="n">website</span>
        <span class="k">self</span><span class="o">.</span><span class="n">addedOn</span> <span class="o">=</span> <span class="n">addedOn</span>
    <span class="p">}</span>
</code></pre></div></div>
<p>在自己实现 <code class="language-plaintext highlighter-rouge">decode</code> 方法时，还需要对几类容器（Container）有了解：<br />
<img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2022/Codable/codable-01.png" alt="JSON 示例中 Keyed、Nested、Unkeyed 和 Single Value 四类容器的位置" /></p>
<ul>
  <li><strong>Keyed Container</strong>：最常见的容器之一，可用来解析键值对数据，对应的 key 定义在 <code class="language-plaintext highlighter-rouge">CodingKeys</code> 枚举中。</li>
  <li><strong>Unkeyed Container</strong>：用来解析不是以字符串为 key 的结构，例如常见的数组结构。</li>
  <li><strong>Single Value Container</strong>：用来把单一的数据解析为有具体类型的数据结构。</li>
  <li><strong>Nested Container</strong>：当某个容器成为另一个容器的子容器时，它被称为嵌套容器</li>
</ul>

<p><strong>我们可以方便地把容器理解为：一个对数据编解码的环境。</strong></p>

<p>被“拍扁”后的模型为：</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">Customer2</span><span class="p">:</span> <span class="kt">Encodable</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">name</span><span class="p">:</span> <span class="kt">String</span>
    <span class="k">var</span> <span class="nv">accessKey</span><span class="p">:</span> <span class="kt">String</span>
    <span class="k">var</span> <span class="nv">family</span><span class="p">:</span> <span class="p">[</span><span class="kt">String</span><span class="p">]</span>
    
    <span class="k">var</span> <span class="nv">atmCode</span><span class="p">:</span> <span class="kt">String</span>
    <span class="k">var</span> <span class="nv">street</span><span class="p">:</span> <span class="kt">String</span>
    <span class="k">var</span> <span class="nv">city</span><span class="p">:</span> <span class="kt">String</span>
    <span class="k">var</span> <span class="nv">zip</span><span class="p">:</span> <span class="kt">Int</span>
    
    <span class="k">var</span> <span class="nv">homePhone</span><span class="p">:</span> <span class="kt">String</span>
    <span class="k">var</span> <span class="nv">cellularPhone</span><span class="p">:</span> <span class="kt">String</span>
    <span class="k">var</span> <span class="nv">email</span><span class="p">:</span> <span class="kt">String</span>
    <span class="k">let</span> <span class="nv">website</span><span class="p">:</span> <span class="kt">String</span>
    <span class="k">let</span> <span class="nv">addedOn</span> <span class="o">=</span> <span class="kt">Date</span><span class="p">()</span>
</code></pre></div></div>

<h2 id="如何-encode">如何 Encode?</h2>
<p>示例模型对象：</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">Foundation</span>

<span class="kd">struct</span> <span class="kt">Customer</span><span class="p">:</span> <span class="kt">Encodable</span> <span class="p">{</span>
  <span class="k">var</span> <span class="nv">name</span><span class="p">:</span> <span class="kt">String</span>
  <span class="k">var</span> <span class="nv">accessKey</span><span class="p">:</span> <span class="kt">String</span>
  
  <span class="k">var</span> <span class="nv">atmCode</span><span class="p">:</span> <span class="kt">String</span>
  <span class="k">var</span> <span class="nv">street</span><span class="p">:</span> <span class="kt">String</span>
  <span class="k">var</span> <span class="nv">city</span><span class="p">:</span> <span class="kt">String</span>
  <span class="k">var</span> <span class="nv">zip</span><span class="p">:</span> <span class="kt">Int</span>
  
  <span class="k">var</span> <span class="nv">homePhone</span><span class="p">:</span> <span class="kt">String</span>
  <span class="k">var</span> <span class="nv">cellularPhone</span><span class="p">:</span> <span class="kt">String</span>
  <span class="k">var</span> <span class="nv">email</span><span class="p">:</span> <span class="kt">String</span>
  <span class="k">let</span> <span class="nv">website</span><span class="p">:</span> <span class="kt">String</span>
  <span class="k">let</span> <span class="nv">addedOn</span> <span class="o">=</span> <span class="kt">Date</span><span class="p">()</span>
  <span class="k">var</span> <span class="nv">family</span><span class="p">:</span> <span class="p">[</span><span class="kt">String</span><span class="p">]</span>
<span class="p">}</span>

<span class="k">let</span> <span class="nv">customer</span> <span class="o">=</span> <span class="kt">Customer</span><span class="p">(</span>
  <span class="nv">name</span><span class="p">:</span> <span class="s">"Shai Mishali"</span><span class="p">,</span>
  <span class="nv">accessKey</span><span class="p">:</span> <span class="s">"S|_|p3rs3cr37"</span><span class="p">,</span>
  <span class="nv">atmCode</span><span class="p">:</span> <span class="s">"1132"</span><span class="p">,</span>
  <span class="nv">street</span><span class="p">:</span> <span class="s">"3828 Piermont Drive"</span><span class="p">,</span>
  <span class="nv">city</span><span class="p">:</span> <span class="s">"Albuquerque"</span><span class="p">,</span>
  <span class="nv">zip</span><span class="p">:</span> <span class="mi">87119</span><span class="p">,</span>
  <span class="nv">homePhone</span><span class="p">:</span> <span class="s">"+1 212-741-4695"</span><span class="p">,</span>
  <span class="nv">cellularPhone</span><span class="p">:</span> <span class="s">"+972 542-288-482"</span><span class="p">,</span>
  <span class="nv">email</span><span class="p">:</span> <span class="s">"freak4pc@gmail.com"</span><span class="p">,</span>
  <span class="nv">website</span><span class="p">:</span> <span class="s">"http://github.com/freak4pc"</span><span class="p">,</span>
  <span class="nv">family</span><span class="p">:</span> <span class="p">[</span><span class="s">"Tom"</span><span class="p">,</span> <span class="s">"Elia"</span><span class="p">]</span>
<span class="p">)</span>
</code></pre></div></div>

<p>我们可以利用 <code class="language-plaintext highlighter-rouge">Encodable</code> 的默认实现来把模型对象转为 JSON 对象：</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">dateFormatter</span><span class="p">:</span> <span class="kt">DateFormatter</span> <span class="o">=</span> <span class="p">{</span>
  <span class="k">let</span> <span class="nv">dateFormatter</span> <span class="o">=</span> <span class="kt">DateFormatter</span><span class="p">()</span>
  <span class="n">dateFormatter</span><span class="o">.</span><span class="n">dateFormat</span> <span class="o">=</span> <span class="s">"MM-dd-yyyy HH:mm"</span>
  <span class="k">return</span> <span class="n">dateFormatter</span>
<span class="p">}()</span>

<span class="k">let</span> <span class="nv">jsonEncoder</span> <span class="o">=</span> <span class="kt">JSONEncoder</span><span class="p">()</span>
<span class="n">jsonEncoder</span><span class="o">.</span><span class="n">dateEncodingStrategy</span> <span class="o">=</span> <span class="o">.</span><span class="nf">formatted</span><span class="p">(</span><span class="n">dateFormatter</span><span class="p">)</span>
<span class="n">jsonEncoder</span><span class="o">.</span><span class="n">outputFormatting</span> <span class="o">=</span> <span class="p">[</span><span class="o">.</span><span class="n">prettyPrinted</span><span class="p">,</span> <span class="o">.</span><span class="n">sortedKeys</span><span class="p">,</span> <span class="o">.</span><span class="n">withoutEscapingSlashes</span><span class="p">]</span>

<span class="k">if</span> <span class="k">let</span> <span class="nv">jsonObject</span> <span class="o">=</span> <span class="k">try</span><span class="p">?</span> <span class="n">jsonEncoder</span><span class="o">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">customer</span><span class="p">),</span>
<span class="k">let</span> <span class="nv">jsonString</span> <span class="o">=</span> <span class="kt">String</span><span class="p">(</span><span class="nv">data</span><span class="p">:</span> <span class="n">jsonObject</span><span class="p">,</span> <span class="nv">encoding</span><span class="p">:</span> <span class="o">.</span><span class="n">utf8</span><span class="p">)</span> <span class="p">{</span>
  <span class="nf">print</span><span class="p">(</span><span class="n">jsonString</span><span class="p">)</span>
<span class="p">}</span>

<span class="c1">// 输出：</span>
<span class="p">{</span>
  <span class="s">"accessKey"</span> <span class="p">:</span> <span class="s">"S|_|p3rs3cr37"</span><span class="p">,</span>
  <span class="s">"addedOn"</span> <span class="p">:</span> <span class="s">"04-14-2022 15:36"</span><span class="p">,</span>
  <span class="s">"atmCode"</span> <span class="p">:</span> <span class="s">"1132"</span><span class="p">,</span>
  <span class="s">"cellularPhone"</span> <span class="p">:</span> <span class="s">"+972 542-288-482"</span><span class="p">,</span>
  <span class="s">"city"</span> <span class="p">:</span> <span class="s">"Albuquerque"</span><span class="p">,</span>
  <span class="s">"email"</span> <span class="p">:</span> <span class="s">"freak4pc@gmail.com"</span><span class="p">,</span>
  <span class="s">"family"</span> <span class="p">:</span> <span class="p">[</span>
    <span class="s">"Tom"</span><span class="p">,</span>
    <span class="s">"Elia"</span>
  <span class="p">],</span>
  <span class="s">"homePhone"</span> <span class="p">:</span> <span class="s">"+1 212-741-4695"</span><span class="p">,</span>
  <span class="s">"name"</span> <span class="p">:</span> <span class="s">"Shai Mishali"</span><span class="p">,</span>
  <span class="s">"street"</span> <span class="p">:</span> <span class="s">"3828 Piermont Drive"</span><span class="p">,</span>
  <span class="s">"website"</span> <span class="p">:</span> <span class="s">"http://github.com/freak4pc"</span><span class="p">,</span>
  <span class="s">"zip"</span> <span class="p">:</span> <span class="mi">87119</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="修改-json-对象的层级结构">修改 JSON 对象的层级结构</h3>
<p>如果想修改 encode 之后的 JSON 对象的层级结构呢？还是得使用嵌套的 <code class="language-plaintext highlighter-rouge">CodingKey</code> 枚举，并自己实现 <code class="language-plaintext highlighter-rouge">encode</code> 方法：</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="kd">func</span> <span class="nf">encode</span><span class="p">(</span><span class="n">to</span> <span class="nv">encoder</span><span class="p">:</span> <span class="kt">Encoder</span><span class="p">)</span> <span class="k">throws</span> <span class="p">{</span>
        <span class="k">var</span> <span class="nv">customer</span> <span class="o">=</span> <span class="n">encoder</span><span class="o">.</span><span class="nf">container</span><span class="p">(</span><span class="nv">keyedBy</span><span class="p">:</span> <span class="kt">CodingKeys</span><span class="o">.</span><span class="k">self</span><span class="p">)</span>
        <span class="k">try</span> <span class="n">customer</span><span class="o">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">name</span><span class="p">)</span>
        <span class="k">try</span> <span class="n">customer</span><span class="o">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">accessKey</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">accessKey</span><span class="p">)</span>
        
        <span class="k">var</span> <span class="nv">addressInfo</span> <span class="o">=</span> <span class="n">customer</span><span class="o">.</span><span class="nf">nestedContainer</span><span class="p">(</span><span class="nv">keyedBy</span><span class="p">:</span> <span class="kt">AddressInfo</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">addressInfo</span><span class="p">)</span>
        <span class="k">try</span> <span class="n">addressInfo</span><span class="o">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">atmCode</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">atmCode</span><span class="p">)</span>
        <span class="k">try</span> <span class="n">addressInfo</span><span class="o">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">street</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">street</span><span class="p">)</span>
        <span class="k">try</span> <span class="n">addressInfo</span><span class="o">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">city</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">city</span><span class="p">)</span>
        <span class="k">try</span> <span class="n">addressInfo</span><span class="o">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">zip</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">zip</span><span class="p">)</span>
        
        <span class="k">var</span> <span class="nv">contactInfo</span> <span class="o">=</span> <span class="n">customer</span><span class="o">.</span><span class="nf">nestedContainer</span><span class="p">(</span><span class="nv">keyedBy</span><span class="p">:</span> <span class="kt">ContactInfo</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">contactInfo</span><span class="p">)</span>
        <span class="k">try</span> <span class="n">contactInfo</span><span class="o">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">homePhone</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">homePhone</span><span class="p">)</span>
        <span class="k">try</span> <span class="n">contactInfo</span><span class="o">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">cellularPhone</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">cellularPhone</span><span class="p">)</span>
        <span class="k">try</span> <span class="n">contactInfo</span><span class="o">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">email</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">email</span><span class="p">)</span>
        <span class="k">try</span> <span class="n">contactInfo</span><span class="o">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">website</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">website</span><span class="p">)</span>
        <span class="k">try</span> <span class="n">contactInfo</span><span class="o">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">addedOn</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">addedOn</span><span class="p">)</span>
    <span class="p">}</span>
</code></pre></div></div>

<h2 id="达成-codable">达成 Codable</h2>
<p>通过上述自定义 encode 和 decode 操作之后，就达成 Codable 。</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">Foundation</span>

<span class="kd">struct</span> <span class="kt">Customer</span><span class="p">:</span> <span class="kt">Codable</span> <span class="p">{</span>
    <span class="kd">internal</span> <span class="nf">init</span><span class="p">(</span><span class="nv">name</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> <span class="nv">accessKey</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> <span class="nv">atmCode</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> <span class="nv">street</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> <span class="nv">city</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> <span class="nv">zip</span><span class="p">:</span> <span class="kt">Int</span><span class="p">,</span> <span class="nv">homePhone</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> <span class="nv">cellularPhone</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> <span class="nv">email</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> <span class="nv">website</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> <span class="nv">addedOn</span><span class="p">:</span> <span class="kt">Date</span> <span class="o">=</span> <span class="kt">Date</span><span class="p">(),</span> <span class="nv">family</span><span class="p">:</span> <span class="p">[</span><span class="kt">String</span><span class="p">])</span> <span class="p">{</span>
        <span class="k">self</span><span class="o">.</span><span class="n">name</span> <span class="o">=</span> <span class="n">name</span>
        <span class="k">self</span><span class="o">.</span><span class="n">accessKey</span> <span class="o">=</span> <span class="n">accessKey</span>
        <span class="k">self</span><span class="o">.</span><span class="n">atmCode</span> <span class="o">=</span> <span class="n">atmCode</span>
        <span class="k">self</span><span class="o">.</span><span class="n">street</span> <span class="o">=</span> <span class="n">street</span>
        <span class="k">self</span><span class="o">.</span><span class="n">city</span> <span class="o">=</span> <span class="n">city</span>
        <span class="k">self</span><span class="o">.</span><span class="n">zip</span> <span class="o">=</span> <span class="n">zip</span>
        <span class="k">self</span><span class="o">.</span><span class="n">homePhone</span> <span class="o">=</span> <span class="n">homePhone</span>
        <span class="k">self</span><span class="o">.</span><span class="n">cellularPhone</span> <span class="o">=</span> <span class="n">cellularPhone</span>
        <span class="k">self</span><span class="o">.</span><span class="n">email</span> <span class="o">=</span> <span class="n">email</span>
        <span class="k">self</span><span class="o">.</span><span class="n">website</span> <span class="o">=</span> <span class="n">website</span>
        <span class="k">self</span><span class="o">.</span><span class="n">addedOn</span> <span class="o">=</span> <span class="n">addedOn</span>
        <span class="k">self</span><span class="o">.</span><span class="n">family</span> <span class="o">=</span> <span class="n">family</span>
    <span class="p">}</span>
    
    <span class="k">var</span> <span class="nv">name</span><span class="p">:</span> <span class="kt">String</span>
    <span class="k">var</span> <span class="nv">accessKey</span><span class="p">:</span> <span class="kt">String</span>
    
    <span class="k">var</span> <span class="nv">atmCode</span><span class="p">:</span> <span class="kt">String</span>
    <span class="k">var</span> <span class="nv">street</span><span class="p">:</span> <span class="kt">String</span>
    <span class="k">var</span> <span class="nv">city</span><span class="p">:</span> <span class="kt">String</span>
    <span class="k">var</span> <span class="nv">zip</span><span class="p">:</span> <span class="kt">Int</span>
    
    <span class="k">var</span> <span class="nv">homePhone</span><span class="p">:</span> <span class="kt">String</span>
    <span class="k">var</span> <span class="nv">cellularPhone</span><span class="p">:</span> <span class="kt">String</span>
    <span class="k">var</span> <span class="nv">email</span><span class="p">:</span> <span class="kt">String</span>
    <span class="k">let</span> <span class="nv">website</span><span class="p">:</span> <span class="kt">String</span>
    <span class="k">var</span> <span class="nv">addedOn</span><span class="p">:</span> <span class="kt">Date</span> <span class="o">=</span> <span class="kt">Date</span><span class="p">()</span>
    <span class="k">var</span> <span class="nv">family</span><span class="p">:</span> <span class="p">[</span><span class="kt">String</span><span class="p">]</span>
    
    <span class="kd">enum</span> <span class="kt">CodingKeys</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> <span class="kt">CodingKey</span> <span class="p">{</span>
        <span class="k">case</span> <span class="n">name</span>
        <span class="k">case</span> <span class="n">accessKey</span>
        <span class="k">case</span> <span class="n">family</span>
        <span class="k">case</span> <span class="n">addressInfo</span> <span class="o">=</span> <span class="s">"AddressInfo"</span>
        <span class="k">case</span> <span class="n">contactInfo</span> <span class="o">=</span> <span class="s">"ContactInfo"</span>
    <span class="p">}</span>

    <span class="kd">enum</span> <span class="kt">AddressInfo</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> <span class="kt">CodingKey</span> <span class="p">{</span>
        <span class="k">case</span> <span class="n">atmCode</span>
        <span class="k">case</span> <span class="n">street</span>
        <span class="k">case</span> <span class="n">city</span>
        <span class="k">case</span> <span class="n">zip</span>
    <span class="p">}</span>

    <span class="kd">enum</span> <span class="kt">ContactInfo</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> <span class="kt">CodingKey</span> <span class="p">{</span>
        <span class="k">case</span> <span class="n">homePhone</span><span class="p">,</span> <span class="n">cellularPhone</span><span class="p">,</span> <span class="n">email</span><span class="p">,</span> <span class="n">website</span>
        <span class="k">case</span> <span class="n">addedOn</span>
    <span class="p">}</span>
    
    <span class="kd">func</span> <span class="nf">encode</span><span class="p">(</span><span class="n">to</span> <span class="nv">encoder</span><span class="p">:</span> <span class="kt">Encoder</span><span class="p">)</span> <span class="k">throws</span> <span class="p">{</span>
        <span class="k">var</span> <span class="nv">customer</span> <span class="o">=</span> <span class="n">encoder</span><span class="o">.</span><span class="nf">container</span><span class="p">(</span><span class="nv">keyedBy</span><span class="p">:</span> <span class="kt">CodingKeys</span><span class="o">.</span><span class="k">self</span><span class="p">)</span>
        <span class="k">try</span> <span class="n">customer</span><span class="o">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">name</span><span class="p">)</span>
        <span class="k">try</span> <span class="n">customer</span><span class="o">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">accessKey</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">accessKey</span><span class="p">)</span>
        
        <span class="k">var</span> <span class="nv">family</span> <span class="o">=</span> <span class="n">customer</span><span class="o">.</span><span class="nf">nestedUnkeyedContainer</span><span class="p">(</span><span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">family</span><span class="p">)</span>
        <span class="k">try</span> <span class="n">family</span><span class="o">.</span><span class="nf">encode</span><span class="p">(</span><span class="nv">contentsOf</span><span class="p">:</span> <span class="k">self</span><span class="o">.</span><span class="n">family</span><span class="p">)</span>

        <span class="k">var</span> <span class="nv">addressInfo</span> <span class="o">=</span> <span class="n">customer</span><span class="o">.</span><span class="nf">nestedContainer</span><span class="p">(</span><span class="nv">keyedBy</span><span class="p">:</span> <span class="kt">AddressInfo</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">addressInfo</span><span class="p">)</span>
        <span class="k">try</span> <span class="n">addressInfo</span><span class="o">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">atmCode</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">atmCode</span><span class="p">)</span>
        <span class="k">try</span> <span class="n">addressInfo</span><span class="o">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">street</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">street</span><span class="p">)</span>
        <span class="k">try</span> <span class="n">addressInfo</span><span class="o">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">city</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">city</span><span class="p">)</span>
        <span class="k">try</span> <span class="n">addressInfo</span><span class="o">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">zip</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">zip</span><span class="p">)</span>

        <span class="k">var</span> <span class="nv">contactInfo</span> <span class="o">=</span> <span class="n">customer</span><span class="o">.</span><span class="nf">nestedContainer</span><span class="p">(</span><span class="nv">keyedBy</span><span class="p">:</span> <span class="kt">ContactInfo</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">contactInfo</span><span class="p">)</span>
        <span class="k">try</span> <span class="n">contactInfo</span><span class="o">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">homePhone</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">homePhone</span><span class="p">)</span>
        <span class="k">try</span> <span class="n">contactInfo</span><span class="o">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">cellularPhone</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">cellularPhone</span><span class="p">)</span>
        <span class="k">try</span> <span class="n">contactInfo</span><span class="o">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">email</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">email</span><span class="p">)</span>
        <span class="k">try</span> <span class="n">contactInfo</span><span class="o">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">website</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">website</span><span class="p">)</span>
        <span class="k">try</span> <span class="n">contactInfo</span><span class="o">.</span><span class="nf">encode</span><span class="p">(</span><span class="n">addedOn</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">addedOn</span><span class="p">)</span>
    <span class="p">}</span>
    
    <span class="nf">init</span><span class="p">(</span><span class="n">from</span> <span class="nv">decoder</span><span class="p">:</span> <span class="kt">Decoder</span><span class="p">)</span> <span class="k">throws</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">customer</span> <span class="o">=</span> <span class="k">try</span> <span class="n">decoder</span><span class="o">.</span><span class="nf">container</span><span class="p">(</span><span class="nv">keyedBy</span><span class="p">:</span> <span class="kt">CodingKeys</span><span class="o">.</span><span class="k">self</span><span class="p">)</span>
        <span class="k">let</span> <span class="nv">name</span> <span class="o">=</span> <span class="k">try</span> <span class="n">customer</span><span class="o">.</span><span class="nf">decode</span><span class="p">(</span><span class="kt">String</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">name</span><span class="p">)</span>
        <span class="k">let</span> <span class="nv">accessKey</span> <span class="o">=</span> <span class="k">try</span> <span class="n">customer</span><span class="o">.</span><span class="nf">decode</span><span class="p">(</span><span class="kt">String</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">accessKey</span><span class="p">)</span>
        <span class="k">self</span><span class="o">.</span><span class="n">name</span> <span class="o">=</span> <span class="n">name</span>
        <span class="k">self</span><span class="o">.</span><span class="n">accessKey</span> <span class="o">=</span> <span class="n">accessKey</span>

        <span class="k">let</span> <span class="nv">addressInfo</span> <span class="o">=</span> <span class="k">try</span> <span class="n">customer</span><span class="o">.</span><span class="nf">nestedContainer</span><span class="p">(</span><span class="nv">keyedBy</span><span class="p">:</span> <span class="kt">AddressInfo</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">addressInfo</span><span class="p">)</span>
        <span class="k">let</span> <span class="nv">atmCode</span> <span class="o">=</span> <span class="k">try</span> <span class="n">addressInfo</span><span class="o">.</span><span class="nf">decode</span><span class="p">(</span><span class="kt">String</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">atmCode</span><span class="p">)</span>
        <span class="k">let</span> <span class="nv">street</span> <span class="o">=</span> <span class="k">try</span> <span class="n">addressInfo</span><span class="o">.</span><span class="nf">decode</span><span class="p">(</span><span class="kt">String</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">street</span><span class="p">)</span>
        <span class="k">let</span> <span class="nv">city</span> <span class="o">=</span> <span class="k">try</span> <span class="n">addressInfo</span><span class="o">.</span><span class="nf">decode</span><span class="p">(</span><span class="kt">String</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">city</span><span class="p">)</span>

        <span class="c1">// 当 JSON 对象中 `zip` key 对应的 value 类型是 String 时的兼容处理</span>
        <span class="k">if</span> <span class="k">case</span> <span class="k">let</span> <span class="nv">zipString</span><span class="p">?</span> <span class="o">=</span> <span class="k">try</span><span class="p">?</span> <span class="n">addressInfo</span><span class="o">.</span><span class="nf">decodeIfPresent</span><span class="p">(</span><span class="kt">String</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">zip</span><span class="p">),</span>
            <span class="k">let</span> <span class="nv">zipString</span> <span class="o">=</span> <span class="n">zipString</span>
        <span class="p">{</span>
            <span class="k">self</span><span class="o">.</span><span class="n">zip</span> <span class="o">=</span> <span class="kt">Int</span><span class="p">(</span><span class="n">zipString</span><span class="p">)</span> <span class="p">??</span> <span class="o">-</span><span class="mi">1</span>
        <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="k">let</span> <span class="nv">zipInt</span> <span class="o">=</span> <span class="k">try</span> <span class="n">addressInfo</span><span class="o">.</span><span class="nf">decodeIfPresent</span><span class="p">(</span><span class="kt">Int</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">zip</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">self</span><span class="o">.</span><span class="n">zip</span> <span class="o">=</span> <span class="n">zipInt</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="k">self</span><span class="o">.</span><span class="n">zip</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span>
        <span class="p">}</span>

        <span class="k">self</span><span class="o">.</span><span class="n">atmCode</span> <span class="o">=</span> <span class="n">atmCode</span>
        <span class="k">self</span><span class="o">.</span><span class="n">street</span> <span class="o">=</span> <span class="n">street</span>
        <span class="k">self</span><span class="o">.</span><span class="n">city</span> <span class="o">=</span> <span class="n">city</span>

        <span class="k">let</span> <span class="nv">contactInfo</span> <span class="o">=</span> <span class="k">try</span> <span class="n">customer</span><span class="o">.</span><span class="nf">nestedContainer</span><span class="p">(</span><span class="nv">keyedBy</span><span class="p">:</span> <span class="kt">ContactInfo</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">contactInfo</span><span class="p">)</span>
        <span class="k">let</span> <span class="nv">homePhone</span> <span class="o">=</span> <span class="k">try</span> <span class="n">contactInfo</span><span class="o">.</span><span class="nf">decode</span><span class="p">(</span><span class="kt">String</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">homePhone</span><span class="p">)</span>
        <span class="k">let</span> <span class="nv">cellularPhone</span> <span class="o">=</span> <span class="k">try</span> <span class="n">contactInfo</span><span class="o">.</span><span class="nf">decode</span><span class="p">(</span><span class="kt">String</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">cellularPhone</span><span class="p">)</span>
        <span class="k">let</span> <span class="nv">email</span> <span class="o">=</span> <span class="k">try</span> <span class="n">contactInfo</span><span class="o">.</span><span class="nf">decode</span><span class="p">(</span><span class="kt">String</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">email</span><span class="p">)</span>
        <span class="k">let</span> <span class="nv">website</span> <span class="o">=</span> <span class="k">try</span> <span class="n">contactInfo</span><span class="o">.</span><span class="nf">decode</span><span class="p">(</span><span class="kt">String</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">website</span><span class="p">)</span>
        <span class="k">let</span> <span class="nv">addedOn</span> <span class="o">=</span> <span class="k">try</span> <span class="n">contactInfo</span><span class="o">.</span><span class="nf">decode</span><span class="p">(</span><span class="kt">Date</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">addedOn</span><span class="p">)</span>
        <span class="k">self</span><span class="o">.</span><span class="n">homePhone</span> <span class="o">=</span> <span class="n">homePhone</span>
        <span class="k">self</span><span class="o">.</span><span class="n">cellularPhone</span> <span class="o">=</span> <span class="n">cellularPhone</span>
        <span class="k">self</span><span class="o">.</span><span class="n">email</span> <span class="o">=</span> <span class="n">email</span>
        <span class="k">self</span><span class="o">.</span><span class="n">website</span> <span class="o">=</span> <span class="n">website</span>
        <span class="k">self</span><span class="o">.</span><span class="n">addedOn</span> <span class="o">=</span> <span class="n">addedOn</span>

        <span class="k">let</span> <span class="nv">family</span> <span class="o">=</span> <span class="k">try</span> <span class="n">customer</span><span class="o">.</span><span class="nf">decode</span><span class="p">([</span><span class="kt">String</span><span class="p">]</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">family</span><span class="p">)</span>
        <span class="k">self</span><span class="o">.</span><span class="n">family</span> <span class="o">=</span> <span class="n">family</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>可以看出：自定义实现 encode 和 decode 方法所带来的代码量还是不小的，然而于此同时也带来了较高的灵活度。</p>

<p>举个例子，假如 JSON 对象中 <code class="language-plaintext highlighter-rouge">zip</code> 对应的值的类型是字符串，如“87119”。而 <code class="language-plaintext highlighter-rouge">Customer</code> 模型中 <code class="language-plaintext highlighter-rouge">zip</code> 变量类型为 <code class="language-plaintext highlighter-rouge">Int</code>，则可以在自定义 decode 方法中特别处理：</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 当 JSON 对象中 `zip` key 对应的 value 类型是 String 时的兼容处理</span>
<span class="k">if</span> <span class="k">case</span> <span class="k">let</span> <span class="nv">zipString</span><span class="p">?</span> <span class="o">=</span> <span class="k">try</span><span class="p">?</span> <span class="n">addressInfo</span><span class="o">.</span><span class="nf">decodeIfPresent</span><span class="p">(</span><span class="kt">String</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">zip</span><span class="p">),</span>
<span class="k">let</span> <span class="nv">zipString</span> <span class="o">=</span> <span class="n">zipString</span>
<span class="p">{</span>
    <span class="k">self</span><span class="o">.</span><span class="n">zip</span> <span class="o">=</span> <span class="kt">Int</span><span class="p">(</span><span class="n">zipString</span><span class="p">)</span> <span class="p">??</span> <span class="o">-</span><span class="mi">1</span>
<span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="k">let</span> <span class="nv">zipInt</span> <span class="o">=</span> <span class="k">try</span> <span class="n">addressInfo</span><span class="o">.</span><span class="nf">decodeIfPresent</span><span class="p">(</span><span class="kt">Int</span><span class="o">.</span><span class="k">self</span><span class="p">,</span> <span class="nv">forKey</span><span class="p">:</span> <span class="o">.</span><span class="n">zip</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">self</span><span class="o">.</span><span class="n">zip</span> <span class="o">=</span> <span class="n">zipInt</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="k">self</span><span class="o">.</span><span class="n">zip</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span>
<span class="p">}</span>
</code></pre></div></div>

<p>再者，如果 JSON 新加一个 <code class="language-plaintext highlighter-rouge">memberType</code> 字段来表示顾客的身份：</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">enum</span> <span class="kt">MemberType</span><span class="p">:</span> <span class="kt">Int</span> <span class="p">{</span>
    <span class="k">case</span> <span class="n">visitor</span> <span class="o">=</span> <span class="mi">1</span>
    <span class="k">case</span> <span class="n">vip</span> <span class="o">=</span> <span class="mi">2</span>
    <span class="k">case</span> <span class="n">unknown</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span>
<span class="p">}</span>
</code></pre></div></div>
<p>突然某一天，收到的 JSON 对象中 <code class="language-plaintext highlighter-rouge">memberType</code> 为 3，意味超级 vip 客户，这是业务迭代新增的需求，而在 <code class="language-plaintext highlighter-rouge">Customer</code> 未对此改动作相应的更新时，也在 decode 方法里对这种情况作兼容处理，比如直接设为 <code class="language-plaintext highlighter-rouge">.unknown</code>。</p>

<h2 id="jsonencoder">JSONEncoder</h2>
<ul>
  <li><strong>KeyEncodingStrategy</strong>：内置默认的 <code class="language-plaintext highlighter-rouge">convertToSnakeCase</code> 策略，如模型中的变量名为：<code class="language-plaintext highlighter-rouge">xmlContents</code>，经过 <code class="language-plaintext highlighter-rouge">convertToSnakeCase</code> 策略之后，对应的 JSON key 会变为 <code class="language-plaintext highlighter-rouge">xml_contents</code>。也提供自定义的接口，让开发者自行决定。</li>
  <li><strong>OutputFormatting</strong>：内置三种默认的输出格式化方式
    <ul>
      <li>prettyPrinted：iOS 7 开始支持</li>
      <li>sortedKeys：iOS 11 开始支持</li>
      <li>withoutEscapingSlashes：iOS 13 开始支持</li>
    </ul>
  </li>
  <li><strong>userInfo</strong>：外部可通过此与自定义的 encode 过程进行信息传递。</li>
  <li><strong>DateEncodingStrategy</strong>：内置 <code class="language-plaintext highlighter-rouge">iso8601</code> 等编码格式，同时也提供传入自定义的 <code class="language-plaintext highlighter-rouge">DateFormatter</code> 对象对日期进行自定义处理。</li>
  <li><strong>DataEncodingStrategy</strong>：内置 <code class="language-plaintext highlighter-rouge">base64</code> 等编码格式，同时也提供自定义接口，让开发者自行决定。</li>
</ul>

<p><a href="https://developer.apple.com/documentation/foundation/jsondecoder">JSONDecoder</a> 的结构与 <a href="https://developer.apple.com/documentation/foundation/jsonencoder">JSONEncoder</a> 很相似，不再一一赘述。</p>

<h3 id="swiftyjson-源码分析未完待续">SwiftyJSON 源码分析（未完待续）</h3>
<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2022/Codable/codable-02.png" alt="SwiftyJSON 的 JSON 结构体组成及其输入和协议关系" /></p>]]></content><author><name>Sunset Wan</name></author><category term="Codable" /><summary type="html"><![CDATA[本文为 Raywenderlich 出版的《Expert Swift》第 8 章节《Codable》阅读笔记。]]></summary></entry><entry><title type="html">打造一个简单的 iOS App CI 系统</title><link href="https://sunsetwan.github.io/2022/03/30/%E6%89%93%E9%80%A0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84%20iOS%20App%20CI%20%E7%B3%BB%E7%BB%9F/" rel="alternate" type="text/html" title="打造一个简单的 iOS App CI 系统" /><published>2022-03-30T09:41:00+08:00</published><updated>2022-03-30T09:41:00+08:00</updated><id>https://sunsetwan.github.io/2022/03/30/%E6%89%93%E9%80%A0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84%20iOS%20App%20CI%20%E7%B3%BB%E7%BB%9F</id><content type="html" xml:base="https://sunsetwan.github.io/2022/03/30/%E6%89%93%E9%80%A0%E4%B8%80%E4%B8%AA%E7%AE%80%E5%8D%95%E7%9A%84%20iOS%20App%20CI%20%E7%B3%BB%E7%BB%9F/"><![CDATA[<p>除了 Xcode Cloud，你还有别的选择。<!-- more --><br />
本文介绍如何在 macOS 上使用 <a href="https://fastlane.tools/">fastlane</a> 来帮助你打造一个简单的 iOS App CI 系统。</p>

<h2 id="在安装-fastlane-之前">在安装 fastlane 之前</h2>
<h3 id="与-macos-系统自带的-ruby-环境隔离">与 macOS 系统自带的 Ruby 环境隔离</h3>
<p>fastlane 对 Ruby 的最低要求是 2.5 及以上版本。虽然 macOS 自带 Ruby 环境，但强烈建议单独配置个 Ruby 环境，与系统自带的 Ruby 环境隔离。Ruby 官方有给出管理多版本 Ruby 的 <a href="https://www.ruby-lang.org/en/documentation/installation/#managers">方法</a>，我个人常使用的是 <a href="https://github.com/rbenv/rbenv#readme">rbenv</a>。</p>

<h3 id="使用-bundler-管理-gem">使用 bundler 管理 gem</h3>
<p>使用 <a href="https://bundler.io/">bundler</a> 管理 gem，这能确保团队协作时 gem（<code class="language-plaintext highlighter-rouge">cocoapods</code> 和 <code class="language-plaintext highlighter-rouge">fastlane</code> 等） 版本的统一。举个例子，如果团队成员的 <code class="language-plaintext highlighter-rouge">cocoapods</code> 版本不统一，Podfile.lock 这个文件经常会出现不必要的改动。</p>

<p>跟 <code class="language-plaintext highlighter-rouge">cocoapods</code> 管理 pod 类似，<code class="language-plaintext highlighter-rouge">bundler</code> 通过 Gemfile 来安装所需的 gem。</p>

<p>本文所使用的 Gemfile 内容为：</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">source</span> <span class="s2">"https://rubygems.org"</span>
<span class="n">gem</span> <span class="s2">"fastlane"</span><span class="p">,</span> <span class="s1">'2.195.0'</span>
<span class="n">gem</span> <span class="s2">"cocoapods"</span>
</code></pre></div></div>
<p>值得一提的是：要在 <code class="language-plaintext highlighter-rouge">bundler</code> 管理的环境下，执行相关 <code class="language-plaintext highlighter-rouge">gem</code> 的命令，得加上 <code class="language-plaintext highlighter-rouge">bundle exec</code> 的前缀。对 <code class="language-plaintext highlighter-rouge">bundle exec</code> 命令的详细介绍，请参考这篇 <a href="https://bundler.io/v2.3/man/bundle-exec.1.html">文档</a>。</p>

<h2 id="使用-fastlane-接管应用的打包和上传-app-store-流程">使用 fastlane 接管应用的打包和上传 App store 流程</h2>
<h3 id="1-设置打包参数">1. 设置打包参数</h3>
<p>一般地，在开始打包之前，需要的参数为：</p>
<ol>
  <li>代码对应的分支名：<code class="language-plaintext highlighter-rouge">branchName</code>（String Type）</li>
  <li>App scheme 名：<code class="language-plaintext highlighter-rouge">scheme</code>（String Type）</li>
  <li>是否需要上传 App store：<code class="language-plaintext highlighter-rouge">deployToProd</code> （Boolean）</li>
</ol>

<p>其他的参数，可以根据自己的需要来设定。</p>

<h3 id="2-自动同步签名证书和-provisioning-profile">2. 自动同步签名证书和 Provisioning Profile</h3>
<p>fastlane 的 <a href="https://docs.fastlane.tools/actions/match/">match</a>  可以帮助在多台 macOS 设备自动同步签名证书和 Provisioning Profile。<code class="language-plaintext highlighter-rouge">match</code> 支持以多种方式存储签名证书和 Provisioning Profile，如存在 Git 仓库中、存在云上（Azure、Google Cloud 和 AWS）等。</p>

<p>一般地，会选择使用 <code class="language-plaintext highlighter-rouge">match</code> 把签名证书和  Provisioning Profile 加密保存在 Git 仓库上。此时，为了安全，<code class="language-plaintext highlighter-rouge">match</code> 会提醒你设置一个密码，这样在新的 macOS 设备上同步证书信息时，会提示输入密码，提高数据的安全性。</p>

<p>在 <code class="language-plaintext highlighter-rouge">match</code> 初始化配置完毕后，敲以下命令，可以同步三种导出方式对应的证书信息至本地，然后重启下 Xcode，点进 <code class="language-plaintext highlighter-rouge">Signing &amp; Capabilities</code> 查看是否生效。</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bundle <span class="nb">exec </span>fastlane match appstore
bundle <span class="nb">exec </span>fastlane match development
bundle <span class="nb">exec </span>fastlane match adhoc
</code></pre></div></div>

<p>虽然 <code class="language-plaintext highlighter-rouge">match</code> 的文档上写明：第一次初始化 <code class="language-plaintext highlighter-rouge">match</code> 前，建议使用 <code class="language-plaintext highlighter-rouge">match nuke</code> 命令来清理现有的签名证书和 Provisioning Profiles，但经验而谈，最好是用 <code class="language-plaintext highlighter-rouge">match</code> 管理签名证书和 Provisioning Profiles 的这套做法在组内成功推广开来之后再用 😜。</p>

<h3 id="3-更新代码">3. 更新代码</h3>
<p>根据第一步获取的 <code class="language-plaintext highlighter-rouge">branchName</code>，在 ruby 脚本内执行 git 命令即可。</p>

<h3 id="4-安装-pod-依赖">4. 安装 Pod 依赖</h3>
<p>fastlane 的 <a href="https://docs.fastlane.tools/actions/cocoapods/">cocoapods</a> 可用来安装 iOS 工程的 Pod 依赖，指定 <code class="language-plaintext highlighter-rouge">Podfile</code> 的路径即可。</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">podFilePath</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">absolute_path</span><span class="p">(</span><span class="s2">"../"</span><span class="p">).</span><span class="nf">to_s</span>
<span class="c1"># install_dependencies</span>
<span class="n">cocoapods</span><span class="p">(</span>
	<span class="ss">clean_install: </span><span class="kp">true</span><span class="p">,</span>
	<span class="ss">podfile: </span><span class="n">podFilePath</span><span class="p">,</span>
	<span class="ss">verbose: </span><span class="kp">true</span><span class="p">,</span>
	<span class="ss">repo_update: </span><span class="kp">true</span>
<span class="p">)</span>
</code></pre></div></div>

<h3 id="5-清除-xcode-缓存">5. 清除 Xcode 缓存</h3>
<p>fastlane 的 <a href="https://docs.fastlane.tools/actions/clear_derived_data/">clear_derived_data</a> 可用来清除 Xcode 缓存。</p>

<h3 id="6-开始打包">6. 开始打包</h3>
<p>fastlane 的 <a href="https://docs.fastlane.tools/actions/gym/">gym</a> 可用来执行 Xcode 打包命令。</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">gym</span><span class="p">(</span>
	<span class="ss">workspace: </span><span class="n">workspaceName</span><span class="p">,</span> <span class="c1"># 以 .xcworkspace.xcworkspace 结尾的 iOS 工程名</span>
	<span class="ss">scheme: </span><span class="n">fullScheme</span><span class="p">,</span> <span class="c1"># iOS 工程的 scheme 名</span>
	<span class="ss">export_method: </span><span class="n">exportMethod</span><span class="p">,</span> <span class="c1"># 所选的导出方法，与 fastlane match type 有关</span>
	<span class="ss">silent: </span><span class="kp">false</span><span class="p">,</span>
	<span class="ss">clean: </span><span class="kp">false</span><span class="p">,</span>
	<span class="ss">output_directory: </span><span class="n">ipaOutputPath</span><span class="p">,</span>
	<span class="ss">output_name: </span><span class="n">iPAOutputName</span><span class="p">,</span>
	<span class="ss">suppress_xcode_output: </span><span class="kp">true</span><span class="p">,</span>
	<span class="ss">include_symbols: </span><span class="kp">false</span><span class="p">,</span>
	<span class="ss">include_bitcode: </span><span class="kp">false</span><span class="p">,</span>
	<span class="ss">build_path: </span><span class="n">fastlaneBuildPath</span><span class="p">,</span>
	<span class="ss">archive_path: </span><span class="n">archivePath</span><span class="p">,</span>
	<span class="ss">derived_data_path: </span><span class="n">fastlaneDerivedDataPath</span><span class="p">,</span>
	<span class="ss">buildlog_path: </span><span class="n">logPath</span><span class="p">,</span>
	<span class="ss">analyze_build_time: </span><span class="kp">false</span><span class="p">,</span>
	<span class="ss">destination: </span><span class="s2">"generic/platform=iOS"</span>
<span class="p">)</span>
</code></pre></div></div>
<p>iOS App scheme、match type 和 gym export method 三者之间可能对应的关系如下图所示：<br />
<img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2022/iOS_simple_CI/ios_ci_01.png" alt="test、pre-prod 与 prod 三种 scheme 对应的 match type 和 gym export method" /></p>
<h3 id="7-上传至-app-store">7. 上传至 App store</h3>
<p>fastlane 老式的做法是用 Apple ID 的账号密码去登陆 App store connect，这样不光得设置一大堆环境变量，如下：</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">FASTLANE_USER</span> <span class="o">=</span> <span class="s2">"sunsetwan@gmail.com"</span>
<span class="no">FASTLANE_ITC_TEAM_NAME</span> <span class="o">=</span> <span class="s2">"Sunset Technology Co. LTD"</span>
<span class="no">FASTLANE_PASSWORD</span> <span class="o">=</span> <span class="s2">"xxx"</span>
<span class="n">app_identifier</span> <span class="o">=</span> <span class="s2">"com.sunset.test"</span>
<span class="no">SPACESHIP_2FA_SMS_DEFAULT_PHONE_NUMBER</span> <span class="o">=</span> <span class="s2">"+86xxx"</span>
<span class="no">FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD</span> <span class="o">=</span> <span class="s2">"xxx"</span>
</code></pre></div></div>
<p>而且登陆态一般只能维持两个礼拜。一旦过期，得重新输入 Apple ID 两步验证发来的短信验证码，这一点是比较令人困扰的，会直接打断自动化的流程，且现在 Apple ID 一旦开启了两步验证，就没法关闭了。</p>

<p>现在推荐使用 <a href="https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api">App Store Connect API</a> 的方式来完成 Apple ID 的认证授权。</p>

<p>主要的三个步骤如下：</p>
<ol>
  <li>用「账户持有者」权限的 Apple ID 登陆 App Store Connect，创建 API Key 文件（<code class="language-plaintext highlighter-rouge">.p8</code>）。</li>
  <li>把这 API Key 文件保存在 CI 机器上</li>
  <li>使用 fastlane 的 <a href="https://docs.fastlane.tools/actions/app_store_connect_api_key/">app_store_connect_api_key</a> 来完成验证。</li>
</ol>

<p>参考代码：</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">desc</span> <span class="s2">"使用 App Store Connect API 的 deliver"</span>
<span class="n">private_lane</span> <span class="ss">:swizzed_deliver</span> <span class="k">do</span> <span class="o">|</span><span class="n">options</span><span class="o">|</span>
	<span class="n">ipa_file_absolute_path</span> <span class="o">=</span> <span class="n">options</span><span class="p">[</span><span class="ss">:ipa_file_absolute_path</span><span class="p">]</span>
	<span class="k">if</span> <span class="n">ipa_file_absolute_path</span><span class="p">.</span><span class="nf">to_s</span><span class="p">.</span><span class="nf">empty?</span> 
		<span class="k">raise</span> <span class="s2">"ipa_file_absolute_path is empty"</span>
	<span class="k">end</span>
	
	<span class="n">app_connect_api_key_path</span> <span class="o">=</span> <span class="s2">""</span>
	<span class="no">Dir</span><span class="p">.</span><span class="nf">chdir</span><span class="p">(</span><span class="s2">"../app_connect_api_key"</span><span class="p">)</span> <span class="k">do</span>
		<span class="n">searchPattern</span> <span class="o">=</span> <span class="s2">"AuthKey_xxx.p8"</span>
		<span class="n">files</span> <span class="o">=</span> <span class="no">Dir</span><span class="p">.</span><span class="nf">glob</span><span class="p">(</span><span class="n">searchPattern</span><span class="p">)</span>
		<span class="k">if</span> <span class="n">files</span><span class="p">.</span><span class="nf">empty?</span>
			<span class="k">raise</span> <span class="s2">"Error: can't find AuthKey_xxx.p8</span><span class="se">\n</span><span class="s2">searchPattern: </span><span class="si">#{</span><span class="n">searchPattern</span><span class="si">}</span><span class="se">\n</span><span class="s2">CurrentPWD: </span><span class="si">#{</span><span class="no">Dir</span><span class="p">.</span><span class="nf">pwd</span><span class="si">}</span><span class="s2">"</span>
		<span class="k">else</span>
			<span class="n">path</span> <span class="o">=</span> <span class="n">files</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nf">to_s</span>
			<span class="n">app_connect_api_key_path</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">absolute_path</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
		<span class="k">end</span> 
	<span class="k">end</span> 
	
	<span class="n">api_key</span> <span class="o">=</span> <span class="n">app_store_connect_api_key</span><span class="p">(</span>
		<span class="ss">key_id: </span><span class="s2">"xxx"</span><span class="p">,</span>
		<span class="ss">issuer_id: </span><span class="s2">"xxx"</span><span class="p">,</span>
		<span class="ss">key_filepath: </span><span class="n">app_connect_api_key_path</span><span class="p">,</span> 
		<span class="ss">duration: </span><span class="mi">1200</span><span class="p">,</span> <span class="c1"># optional (maximum 1200)</span>
		<span class="ss">in_house: </span><span class="kp">false</span> <span class="c1"># optional but may be required if using match/sigh</span>
	<span class="p">)</span>

	<span class="k">if</span> <span class="n">api_key</span><span class="p">.</span><span class="nf">nil?</span> 
		<span class="k">raise</span> <span class="s2">"api key is missing!"</span>
	<span class="k">end</span>
	
	<span class="n">deliver</span><span class="p">(</span>
		<span class="ss">ipa: </span><span class="n">ipa_file_absolute_path</span><span class="p">,</span>
		<span class="ss">skip_screenshots: </span><span class="kp">true</span><span class="p">,</span>
		<span class="ss">skip_metadata: </span><span class="kp">true</span><span class="p">,</span>
		<span class="ss">skip_app_version_update: </span><span class="kp">true</span><span class="p">,</span>
		<span class="ss">force: </span><span class="kp">true</span><span class="p">,</span>
		<span class="ss">api_key: </span><span class="n">api_key</span><span class="p">,</span>
		<span class="ss">run_precheck_before_submit: </span><span class="kp">false</span>
	<span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>最后，可以配合使用 Gitlab Runner，来自动触发上述构建流程。</p>

<h2 id="ci-的核心是什么">CI 的核心是什么？</h2>
<p>我带着这问题去读了下《Software Engineering at Google》的第 23 章节 Continuous Integration。我个人觉得这问题的答案是：</p>
<blockquote>
  <p>A CI system decides what tests to use, and when.</p>
</blockquote>

<p>当然，我认为这里提到的 <code class="language-plaintext highlighter-rouge">test</code> 不一定就是单元测试，可能还包含 lint 工具的执行结果等。</p>

<p>我前阵子读 <code class="language-plaintext highlighter-rouge">Kingfisher</code> 源码的时候，发现它也有写不少测试代码。</p>

<p>于是，我找到 Jon Reid 写的《iOS Unit Testing by Example: XCTest Tips and Techniques Using Swift》，决定读一读，学习如何使用 <code class="language-plaintext highlighter-rouge">XCTest</code>。</p>

<p>其中有关 <code class="language-plaintext highlighter-rouge">XCTestCase</code> 的生命周期的介绍，令人印象深刻，记录于此。<br />
假设我们要测试的类为 <code class="language-plaintext highlighter-rouge">MyClass</code>：</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="kt">MyClass</span> <span class="p">{</span>
    
    <span class="kd">private</span> <span class="kd">static</span> <span class="k">var</span> <span class="nv">allInstances</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="kd">private</span> <span class="k">let</span> <span class="nv">instance</span><span class="p">:</span> <span class="kt">Int</span>

    <span class="nf">init</span><span class="p">()</span> <span class="p">{</span>
        <span class="kt">MyClass</span><span class="o">.</span><span class="n">allInstances</span> <span class="o">+=</span> <span class="mi">1</span>
        <span class="n">instance</span> <span class="o">=</span> <span class="kt">MyClass</span><span class="o">.</span><span class="n">allInstances</span>
        <span class="nf">print</span><span class="p">(</span><span class="s">"&gt;&gt; MyClass.init() #</span><span class="se">\(</span><span class="n">instance</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
    <span class="p">}</span>
    
    <span class="kd">deinit</span> <span class="p">{</span>
        <span class="nf">print</span><span class="p">(</span><span class="s">"&gt;&gt; MyClass.deinit #</span><span class="se">\(</span><span class="n">instance</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
    <span class="p">}</span>
    
    <span class="kd">func</span> <span class="nf">methodOne</span><span class="p">()</span> <span class="p">{</span>
        <span class="nf">print</span><span class="p">(</span><span class="s">"&gt;&gt; methodOne"</span><span class="p">)</span>
    <span class="p">}</span>
    
    <span class="kd">func</span> <span class="nf">methodTwo</span><span class="p">()</span> <span class="p">{</span>
        <span class="nf">print</span><span class="p">(</span><span class="s">"&gt;&gt; methodTwo"</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>对应的测试代码 <code class="language-plaintext highlighter-rouge">MyClassTests</code> 为：</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">XCTest</span>
<span class="kd">@testable</span> <span class="kd">import</span> <span class="kt">TestLifeCycle</span>

<span class="kd">class</span> <span class="kt">MyClassTests</span><span class="p">:</span> <span class="kt">XCTestCase</span> <span class="p">{</span>

    <span class="kd">func</span> <span class="nf">test_methodOne</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">sut</span> <span class="o">=</span> <span class="kt">MyClass</span><span class="p">()</span>
        <span class="n">sut</span><span class="o">.</span><span class="nf">methodOne</span><span class="p">()</span>
    <span class="p">}</span>
    
    <span class="kd">func</span> <span class="nf">test_methodTwo</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">sut</span> <span class="o">=</span> <span class="kt">MyClass</span><span class="p">()</span>
        <span class="n">sut</span><span class="o">.</span><span class="nf">methodTwo</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2022/iOS_simple_CI/ios_ci_02.jpg" alt="Xcode 中包含 test_methodOne 和 test_methodTwo 的 MyClassTests 测试类，左侧可运行整组测试" /><br />
点击运行按钮，可以看到如下测试日志：<br />
<img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2022/iOS_simple_CI/ios_ci_03.jpg" alt="两个测试均各自创建、调用并销毁一个 MyClass 实例的测试日志" /><br />
以 <code class="language-plaintext highlighter-rouge">test_methodOne</code> 方法为例，整个测试流程方法调用顺序为：</p>
<ol>
  <li>MyClass.init()</li>
  <li>methodOne</li>
  <li>MyClass.deinit()</li>
</ol>

<p>但 <code class="language-plaintext highlighter-rouge">test_methodOne</code> 方法和 <code class="language-plaintext highlighter-rouge">test_methodTwo</code> 方法内分别创建了一个 <code class="language-plaintext highlighter-rouge">MyClass</code> 实例，是不是代码有些重复了？</p>

<p>于是，我尝试这样优化：</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="kt">MyClassTests</span><span class="p">:</span> <span class="kt">XCTestCase</span> <span class="p">{</span>
    
    <span class="kd">private</span> <span class="k">let</span> <span class="nv">sut</span> <span class="o">=</span> <span class="kt">MyClass</span><span class="p">()</span>

    <span class="kd">func</span> <span class="nf">test_methodOne</span><span class="p">()</span> <span class="p">{</span>
        <span class="n">sut</span><span class="o">.</span><span class="nf">methodOne</span><span class="p">()</span>
    <span class="p">}</span>
    
    <span class="kd">func</span> <span class="nf">test_methodTwo</span><span class="p">()</span> <span class="p">{</span>
        <span class="n">sut</span><span class="o">.</span><span class="nf">methodTwo</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>再次点击运行按钮，得到如下测试日志：<br />
<img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2022/iOS_simple_CI/ios_ci_04.jpg" alt="把 MyClass 设为测试类属性后，测试运行前连续初始化两个实例且没有销毁日志" /><br />
咦！怎么 <code class="language-plaintext highlighter-rouge">MyClass.init()</code> 调用了两次？<code class="language-plaintext highlighter-rouge">MyClass.deinit()</code> 方法没被调用！<br />
在测试代码开始运行之前，两个 <code class="language-plaintext highlighter-rouge">MyClass</code> 的实例被创建了，且从未被销毁！这样会有两个潜在的问题：</p>
<ol>
  <li>如果 <code class="language-plaintext highlighter-rouge">MyClass</code> 的 <code class="language-plaintext highlighter-rouge">init</code> 方法出了问题，测试代码很难捕捉到这个问题，因为问题发生的时机在任何测试代码运行之前。</li>
  <li>如果 <code class="language-plaintext highlighter-rouge">MyClass</code> 的 <code class="language-plaintext highlighter-rouge">deinit</code> 方法出了问题，测试代码很难捕捉到这个问题，因为这些对象未被销毁。</li>
</ol>

<h3 id="xctest-是如何管理-test-case-的">XCTest 是如何管理 Test case 的？</h3>
<p>我想当然觉得是这样的：</p>
<ol>
  <li>创建 <code class="language-plaintext highlighter-rouge">XCTestCase</code> 子类的实例对象</li>
  <li>执行创建好的实例对象的测试方法</li>
  <li>执行完毕，销毁创建好的实例对象</li>
</ol>

<p>但实际上是这样的：</p>
<ol>
  <li><code class="language-plaintext highlighter-rouge">XCTest</code> 找到所有 <code class="language-plaintext highlighter-rouge">XCTestCase</code> 的子类</li>
  <li>在每一个这样的子类中，找到每个测试方法</li>
  <li>对于每个测试方法，<code class="language-plaintext highlighter-rouge">XCTest</code> 会创建一个对应的实例</li>
  <li><code class="language-plaintext highlighter-rouge">XCTest</code>  把每种 <code class="language-plaintext highlighter-rouge">XCTestCase</code> 的子类的实例包装成一个个的 <code class="language-plaintext highlighter-rouge">test suite</code></li>
  <li>当前四个步骤完成后，<code class="language-plaintext highlighter-rouge">XCTest</code> 开始运行测试代码。</li>
</ol>

<p>以 <code class="language-plaintext highlighter-rouge">MyClassTests</code> 为例：</p>
<ol>
  <li><code class="language-plaintext highlighter-rouge">XCTest</code> 找到 <code class="language-plaintext highlighter-rouge">XCTestCase</code> 的子类：<code class="language-plaintext highlighter-rouge">MyClassTests</code></li>
  <li>在 <code class="language-plaintext highlighter-rouge">MyClassTests</code> 中，找到两个测试方法：<code class="language-plaintext highlighter-rouge">test_methodOne</code> 和 <code class="language-plaintext highlighter-rouge">test_methodTwo</code></li>
  <li>创建两个 <code class="language-plaintext highlighter-rouge">MyClassTests</code> 的实例</li>
  <li>把这两个实例包装成一个 <code class="language-plaintext highlighter-rouge">test suite</code></li>
</ol>

<p>因为这两个 <code class="language-plaintext highlighter-rouge">MyClassTests</code> 的实例均持有 <code class="language-plaintext highlighter-rouge">MyClass</code> 属性，所以这就是 <code class="language-plaintext highlighter-rouge">MyClass.init()</code> 调用了两次的原因。</p>

<h3 id="避免重复测试代码的正确方式">避免重复测试代码的正确方式</h3>
<p>这里介绍 <code class="language-plaintext highlighter-rouge">XCTest</code> 的两个方法：<code class="language-plaintext highlighter-rouge">setUp()</code> 和 <code class="language-plaintext highlighter-rouge">tearDown()</code><br />
<code class="language-plaintext highlighter-rouge">setUp()</code> 方法是在每个测试方法执行<strong>前</strong>执行，用来设置初始状态。<br />
<code class="language-plaintext highlighter-rouge">tearDown()</code> 方法是在每个测试方法执行<strong>后</strong>执行，用来执行清理动作。<br />
使用这两个方法优化之后的代码：</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">XCTest</span>
<span class="kd">@testable</span> <span class="kd">import</span> <span class="kt">TestLifeCycle</span>

<span class="kd">class</span> <span class="kt">MyClassTests</span><span class="p">:</span> <span class="kt">XCTestCase</span> <span class="p">{</span>
    
    <span class="kd">private</span> <span class="k">var</span> <span class="nv">sut</span><span class="p">:</span> <span class="kt">MyClass</span><span class="o">!</span>
    
    <span class="k">override</span> <span class="kd">func</span> <span class="nf">setUp</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">super</span><span class="o">.</span><span class="nf">setUp</span><span class="p">()</span>
        <span class="n">sut</span> <span class="o">=</span> <span class="kt">MyClass</span><span class="p">()</span>
    <span class="p">}</span>
    
    <span class="k">override</span> <span class="kd">func</span> <span class="nf">tearDown</span><span class="p">()</span> <span class="p">{</span>
        <span class="n">sut</span> <span class="o">=</span> <span class="kc">nil</span>
        <span class="k">super</span><span class="o">.</span><span class="nf">tearDown</span><span class="p">()</span>
    <span class="p">}</span>

    <span class="kd">func</span> <span class="nf">test_methodOne</span><span class="p">()</span> <span class="p">{</span>
        <span class="n">sut</span><span class="o">.</span><span class="nf">methodOne</span><span class="p">()</span>
    <span class="p">}</span>
    
    <span class="kd">func</span> <span class="nf">test_methodTwo</span><span class="p">()</span> <span class="p">{</span>
        <span class="n">sut</span><span class="o">.</span><span class="nf">methodTwo</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>再次点击运行按钮，得到如下测试日志：<br />
<img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2022/iOS_simple_CI/ios_ci_05.jpg" alt="使用 setUp 与 tearDown 后，每个测试都正确初始化并销毁 MyClass 实例的日志" /><br />
Wow！实例对象正确地初始化了，最终也被正确地销毁了！</p>

<p>fastlane 的 <a href="https://docs.fastlane.tools/getting-started/ios/running-tests/">run_tests</a> 可用来在 ruby 脚本中执行测试代码。</p>

<h2 id="结语">结语</h2>
<p>有人会说：搞个 CI 成本太高了，但我觉得它带来的价值是高于成本的。<br />
这里引用《Software Engineering at Google》第 23 章节的一段话：</p>
<blockquote>
  <p>CI leads to a more stable product and happier developer culture in which engineers feel more confident that “the system” will catch problems, and they can focus more on features and less on fixing.</p>
</blockquote>

<p>让工程师们 focus more on features and less on fixing，这一点真的很重要。</p>]]></content><author><name>Sunset Wan</name></author><category term="Continuous Integration" /><summary type="html"><![CDATA[除了 Xcode Cloud，你还有别的选择。]]></summary></entry><entry><title type="html">Kingfisher 源码阅读（一）</title><link href="https://sunsetwan.github.io/2022/03/28/Kingfisher%20%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%EF%BC%88%E4%B8%80%EF%BC%89/" rel="alternate" type="text/html" title="Kingfisher 源码阅读（一）" /><published>2022-03-28T09:41:00+08:00</published><updated>2022-03-28T09:41:00+08:00</updated><id>https://sunsetwan.github.io/2022/03/28/Kingfisher%20%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%EF%BC%88%E4%B8%80%EF%BC%89</id><content type="html" xml:base="https://sunsetwan.github.io/2022/03/28/Kingfisher%20%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%EF%BC%88%E4%B8%80%EF%BC%89/"><![CDATA[<p>最近想了解如何设计一个简单的缓存系统，于是找了 Kingfisher 7.1.2 版本的源代码读一读，有些收获，记录于此。<!-- more --></p>

<p>缓存系统一般可分为：内存缓存（memory cache）和硬盘缓存（disk cache）。</p>

<h2 id="内存缓存">内存缓存</h2>
<p>内存缓存背后支撑的数据结构是 Foundation 框架中的 <code class="language-plaintext highlighter-rouge">NSCache</code>。<br />
<code class="language-plaintext highlighter-rouge">NSCache</code> 有两个限制：<code class="language-plaintext highlighter-rouge">countLimit</code> 和 <code class="language-plaintext highlighter-rouge">totalCostLimit</code>。前者是表示能存储多少个对象，后者是表示缓存的容量有多大。但这两个限制是软限制——如果超过限制，<code class="language-plaintext highlighter-rouge">NSCache</code> 中保存的对象不一定会被及时地从其中移出。根据 <a href="https://developer.apple.com/documentation/foundation/nscache?language=objc">官方文档</a>，超过限制之后，对象的移出时机取决与 <code class="language-plaintext highlighter-rouge">NSCache</code> 背后的实现。</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2022/kf_01.jpg" alt="NSCache 的泛型声明要求 KeyType 和 ObjectType 都是 AnyObject 引用类型" /></p>

<p><code class="language-plaintext highlighter-rouge">NSCache</code> 是个泛型类，所以我们得定义好 <code class="language-plaintext highlighter-rouge">KeyType</code> 和 <code class="language-plaintext highlighter-rouge">ObjectType</code>。<br />
根据声明的类型约束，<code class="language-plaintext highlighter-rouge">KeyType</code> 的类型得是一个类。一般地，类似 key 的数据结构会采用 <code class="language-plaintext highlighter-rouge">String</code>。而这里 <code class="language-plaintext highlighter-rouge">KeyType</code> 得是引用类型而不是值类型，所以我们会选择 <code class="language-plaintext highlighter-rouge">NSString</code>。但在 API 设计时，对外暴露的那一层 API，还是可以使用 <code class="language-plaintext highlighter-rouge">String</code>。到调用 <code class="language-plaintext highlighter-rouge">NSCache</code> 的 <code class="language-plaintext highlighter-rouge">setObject(_:forKey:cost:)</code> 方法时，再把 <code class="language-plaintext highlighter-rouge">String</code> 转为 <code class="language-plaintext highlighter-rouge">NSString</code>。之所以这样做，是因为值类型更符合当前场景的需求。<br />
Kingfisher 中 <code class="language-plaintext highlighter-rouge">ObjectType</code> 对应的数据结构是这么设计的：</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="kt">StorageObject</span><span class="o">&lt;</span><span class="kt">T</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">value</span><span class="p">:</span> <span class="kt">T</span> <span class="c1">// 图片的数据</span>
    <span class="k">let</span> <span class="nv">expiration</span><span class="p">:</span> <span class="kt">StorageExpiration</span> <span class="c1">// 过期策略</span>
    <span class="k">let</span> <span class="nv">key</span><span class="p">:</span> <span class="kt">String</span>
    
    <span class="kd">private(set)</span> <span class="k">var</span> <span class="nv">estimatedExpiration</span><span class="p">:</span> <span class="kt">Date</span> <span class="c1">// 预计到期时间</span>
    
    <span class="nf">init</span><span class="p">(</span><span class="n">_</span> <span class="nv">value</span><span class="p">:</span> <span class="kt">T</span><span class="p">,</span> <span class="nv">key</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> <span class="nv">expiration</span><span class="p">:</span> <span class="kt">StorageExpiration</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">self</span><span class="o">.</span><span class="n">value</span> <span class="o">=</span> <span class="n">value</span>
        <span class="k">self</span><span class="o">.</span><span class="n">key</span> <span class="o">=</span> <span class="n">key</span>
        <span class="k">self</span><span class="o">.</span><span class="n">expiration</span> <span class="o">=</span> <span class="n">expiration</span>
        
        <span class="k">self</span><span class="o">.</span><span class="n">estimatedExpiration</span> <span class="o">=</span> <span class="n">expiration</span><span class="o">.</span><span class="n">estimatedExpirationSinceNow</span>
    <span class="p">}</span>
    
    <span class="c1">// 根据当前缓存对象的续期策略，进行续期。</span>
    <span class="kd">func</span> <span class="nf">extendExpiration</span><span class="p">(</span><span class="n">_</span> <span class="nv">extendingExpiration</span><span class="p">:</span> <span class="kt">ExpirationExtending</span> <span class="o">=</span> <span class="o">.</span><span class="n">cacheTime</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">switch</span> <span class="n">extendingExpiration</span> <span class="p">{</span>
            <span class="k">case</span> <span class="o">.</span><span class="nv">none</span><span class="p">:</span>
            <span class="k">return</span>
            <span class="k">case</span> <span class="o">.</span><span class="nv">cacheTime</span><span class="p">:</span>
            <span class="k">self</span><span class="o">.</span><span class="n">estimatedExpiration</span> <span class="o">=</span> <span class="n">expiration</span><span class="o">.</span><span class="n">estimatedExpirationSinceNow</span>
            <span class="k">case</span> <span class="o">.</span><span class="nf">expirationTime</span><span class="p">(</span><span class="k">let</span> <span class="nv">expirationTime</span><span class="p">):</span>
            <span class="k">self</span><span class="o">.</span><span class="n">estimatedExpiration</span> <span class="o">=</span> <span class="n">expirationTime</span><span class="o">.</span><span class="n">estimatedExpirationSinceNow</span>
        <span class="p">}</span>
    <span class="p">}</span>
    
    <span class="k">var</span> <span class="nv">expired</span><span class="p">:</span> <span class="kt">Bool</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">estimatedExpiration</span><span class="o">.</span><span class="n">isPast</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="硬盘缓存">硬盘缓存</h2>
<p>所谓硬盘缓存，就是把缓存数据写入一个文件。当对象写入硬盘缓存时，需要的是图片的数据，缓存的 Key 以及过期策略（StorageExpiration）。写入文件的 URL 是通过缓存的 Key 计算可得。</p>

<p>Kingfisher 利用操作系统中的文件系统来快捷地存储该缓存对象的 <strong>上一次访问时间</strong> 和 <strong>预计到期时间</strong>。Kingfisher 把写入文件的 <strong>创建时间</strong> 写为 <strong>上一次访问时间</strong>，把写入文件的 <strong>修改日期</strong> 写为 <strong>预计到期时间</strong>，还挺巧妙的 🤯。</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">now</span> <span class="o">=</span> <span class="kt">Date</span><span class="p">()</span>
<span class="k">let</span> <span class="nv">attributes</span><span class="p">:</span> <span class="p">[</span><span class="kt">FileAttributeKey</span> <span class="p">:</span> <span class="kt">Any</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span>
    <span class="c1">// The last access date.</span>
    <span class="o">.</span><span class="nv">creationDate</span><span class="p">:</span> <span class="n">now</span><span class="o">.</span><span class="n">fileAttributeDate</span><span class="p">,</span>
    <span class="c1">// The estimated expiration date.</span>
    <span class="o">.</span><span class="nv">modificationDate</span><span class="p">:</span> <span class="n">expiration</span><span class="o">.</span><span class="n">estimatedExpirationSinceNow</span><span class="o">.</span><span class="n">fileAttributeDate</span>
<span class="p">]</span>
<span class="k">do</span> <span class="p">{</span>
    <span class="k">try</span> <span class="n">config</span><span class="o">.</span><span class="n">fileManager</span><span class="o">.</span><span class="nf">setAttributes</span><span class="p">(</span><span class="n">attributes</span><span class="p">,</span> <span class="nv">ofItemAtPath</span><span class="p">:</span> <span class="n">fileURL</span><span class="o">.</span><span class="n">path</span><span class="p">)</span>
<span class="p">}</span> <span class="k">catch</span> <span class="p">{</span>
    <span class="k">try</span><span class="p">?</span> <span class="n">config</span><span class="o">.</span><span class="n">fileManager</span><span class="o">.</span><span class="nf">removeItem</span><span class="p">(</span><span class="nv">at</span><span class="p">:</span> <span class="n">fileURL</span><span class="p">)</span>
    <span class="k">throw</span> <span class="kt">KingfisherError</span><span class="o">.</span><span class="nf">cacheError</span><span class="p">(</span>
        <span class="nv">reason</span><span class="p">:</span> <span class="o">.</span><span class="nf">cannotSetCacheFileAttribute</span><span class="p">(</span>
            <span class="nv">filePath</span><span class="p">:</span> <span class="n">fileURL</span><span class="o">.</span><span class="n">path</span><span class="p">,</span>
            <span class="nv">attributes</span><span class="p">:</span> <span class="n">attributes</span><span class="p">,</span>
            <span class="nv">error</span><span class="p">:</span> <span class="n">error</span>
        <span class="p">)</span>
    <span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p>当从硬盘中访问缓存时，Kingfisher 设计了 <code class="language-plaintext highlighter-rouge">FileMeta</code> 数据结构：</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">FileMeta</span> <span class="p">{</span>
    
    <span class="k">let</span> <span class="nv">url</span><span class="p">:</span> <span class="kt">URL</span>
    
    <span class="k">let</span> <span class="nv">lastAccessDate</span><span class="p">:</span> <span class="kt">Date</span><span class="p">?</span>
    <span class="k">let</span> <span class="nv">estimatedExpirationDate</span><span class="p">:</span> <span class="kt">Date</span><span class="p">?</span>
    <span class="k">let</span> <span class="nv">isDirectory</span><span class="p">:</span> <span class="kt">Bool</span>
    <span class="k">let</span> <span class="nv">fileSize</span><span class="p">:</span> <span class="kt">Int</span>
    
    <span class="nf">init</span><span class="p">(</span>
        <span class="nv">fileURL</span><span class="p">:</span> <span class="kt">URL</span><span class="p">,</span>
        <span class="nv">lastAccessDate</span><span class="p">:</span> <span class="kt">Date</span><span class="p">?,</span>
        <span class="nv">estimatedExpirationDate</span><span class="p">:</span> <span class="kt">Date</span><span class="p">?,</span>
        <span class="nv">isDirectory</span><span class="p">:</span> <span class="kt">Bool</span><span class="p">,</span>
        <span class="nv">fileSize</span><span class="p">:</span> <span class="kt">Int</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">self</span><span class="o">.</span><span class="n">url</span> <span class="o">=</span> <span class="n">fileURL</span>
        <span class="k">self</span><span class="o">.</span><span class="n">lastAccessDate</span> <span class="o">=</span> <span class="n">lastAccessDate</span>
        <span class="k">self</span><span class="o">.</span><span class="n">estimatedExpirationDate</span> <span class="o">=</span> <span class="n">estimatedExpirationDate</span>
        <span class="k">self</span><span class="o">.</span><span class="n">isDirectory</span> <span class="o">=</span> <span class="n">isDirectory</span>
        <span class="k">self</span><span class="o">.</span><span class="n">fileSize</span> <span class="o">=</span> <span class="n">fileSize</span>
    <span class="p">}</span>
    
    <span class="kd">func</span> <span class="nf">expired</span><span class="p">(</span><span class="nv">referenceDate</span><span class="p">:</span> <span class="kt">Date</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Bool</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">estimatedExpirationDate</span><span class="p">?</span><span class="o">.</span><span class="nf">isPast</span><span class="p">(</span><span class="nv">referenceDate</span><span class="p">:</span> <span class="n">referenceDate</span><span class="p">)</span> <span class="p">??</span> <span class="kc">true</span>
    <span class="p">}</span>
    
    <span class="c1">// 根据当前缓存对象的续期策略，进行续期。</span>
    <span class="kd">func</span> <span class="nf">extendExpiration</span><span class="p">(</span><span class="n">with</span> <span class="nv">fileManager</span><span class="p">:</span> <span class="kt">FileManager</span><span class="p">,</span> <span class="nv">extendingExpiration</span><span class="p">:</span> <span class="kt">ExpirationExtending</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">guard</span> <span class="k">let</span> <span class="nv">lastAccessDate</span> <span class="o">=</span> <span class="n">lastAccessDate</span><span class="p">,</span>
        <span class="k">let</span> <span class="nv">lastEstimatedExpiration</span> <span class="o">=</span> <span class="n">estimatedExpirationDate</span> <span class="k">else</span>
        <span class="p">{</span>
            <span class="k">return</span>
        <span class="p">}</span>
        
        <span class="k">let</span> <span class="nv">attributes</span><span class="p">:</span> <span class="p">[</span><span class="kt">FileAttributeKey</span> <span class="p">:</span> <span class="kt">Any</span><span class="p">]</span>
        
        <span class="k">switch</span> <span class="n">extendingExpiration</span> <span class="p">{</span>
            <span class="k">case</span> <span class="o">.</span><span class="nv">none</span><span class="p">:</span>
            <span class="c1">// not extending expiration time here</span>
            <span class="k">return</span>
            <span class="k">case</span> <span class="o">.</span><span class="nv">cacheTime</span><span class="p">:</span>
            <span class="k">let</span> <span class="nv">originalExpiration</span><span class="p">:</span> <span class="kt">StorageExpiration</span> <span class="o">=</span>
            <span class="o">.</span><span class="nf">seconds</span><span class="p">(</span><span class="n">lastEstimatedExpiration</span><span class="o">.</span><span class="nf">timeIntervalSince</span><span class="p">(</span><span class="n">lastAccessDate</span><span class="p">))</span>
            <span class="n">attributes</span> <span class="o">=</span> <span class="p">[</span>
                <span class="o">.</span><span class="nv">creationDate</span><span class="p">:</span> <span class="kt">Date</span><span class="p">()</span><span class="o">.</span><span class="n">fileAttributeDate</span><span class="p">,</span>
                <span class="o">.</span><span class="nv">modificationDate</span><span class="p">:</span> <span class="n">originalExpiration</span><span class="o">.</span><span class="n">estimatedExpirationSinceNow</span><span class="o">.</span><span class="n">fileAttributeDate</span>
            <span class="p">]</span>
            <span class="k">case</span> <span class="o">.</span><span class="nf">expirationTime</span><span class="p">(</span><span class="k">let</span> <span class="nv">expirationTime</span><span class="p">):</span>
            <span class="n">attributes</span> <span class="o">=</span> <span class="p">[</span>
                <span class="o">.</span><span class="nv">creationDate</span><span class="p">:</span> <span class="kt">Date</span><span class="p">()</span><span class="o">.</span><span class="n">fileAttributeDate</span><span class="p">,</span>
                <span class="o">.</span><span class="nv">modificationDate</span><span class="p">:</span> <span class="n">expirationTime</span><span class="o">.</span><span class="n">estimatedExpirationSinceNow</span><span class="o">.</span><span class="n">fileAttributeDate</span>
            <span class="p">]</span>
        <span class="p">}</span>
        
        <span class="k">try</span><span class="p">?</span> <span class="n">fileManager</span><span class="o">.</span><span class="nf">setAttributes</span><span class="p">(</span><span class="n">attributes</span><span class="p">,</span> <span class="nv">ofItemAtPath</span><span class="p">:</span> <span class="n">url</span><span class="o">.</span><span class="n">path</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>不难看出：<code class="language-plaintext highlighter-rouge">FileMeta</code> 的设计思路与 <code class="language-plaintext highlighter-rouge">StorageObject</code> 的设计思路有不少相似的地方。</p>

<h2 id="缓存过期策略">缓存过期策略</h2>
<p>Kingfisher 有设计 5 种缓存对象的过期策略：</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">enum</span> <span class="kt">StorageExpiration</span> <span class="p">{</span>
    <span class="k">case</span> <span class="n">never</span> <span class="c1">// 缓存对象永不过期</span>
    <span class="k">case</span> <span class="nf">seconds</span><span class="p">(</span><span class="kt">TimeInterval</span><span class="p">)</span> <span class="c1">// 缓存对象在当前时间点后的 x 秒过期</span>
    <span class="k">case</span> <span class="nf">days</span><span class="p">(</span><span class="kt">Int</span><span class="p">)</span> <span class="c1">// 缓存对象在当前时间点后的 x 天过期</span>
    <span class="k">case</span> <span class="nf">date</span><span class="p">(</span><span class="kt">Date</span><span class="p">)</span> <span class="c1">// 缓存对象在指定 Date 过期</span>
    <span class="k">case</span> <span class="n">expired</span> <span class="c1">// 缓存对象已过期</span>
		<span class="o">...</span>
		<span class="o">...</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="缓存续期策略">缓存续期策略</h2>
<p>根据程序局部性的原理，被访问过的缓存很有可能再被访问一次，所以也应有缓存续期策略。<br />
Kingfisher 有设计 3 种缓存续期策略：</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">enum</span> <span class="kt">ExpirationExtending</span> <span class="p">{</span>
    <span class="k">case</span> <span class="k">none</span> <span class="c1">// 不续期，到期时间不变</span>
    <span class="k">case</span> <span class="n">cacheTime</span> <span class="c1">// 以当前时间点为基础续期</span>
    <span class="k">case</span> <span class="nf">expirationTime</span><span class="p">(</span><span class="n">_</span> <span class="nv">expiration</span><span class="p">:</span> <span class="kt">StorageExpiration</span><span class="p">)</span> <span class="c1">// 以指定到期策略续期</span>
<span class="p">}</span>
</code></pre></div></div>
<p>无论是内存缓存还是硬盘缓存，默认的缓存续期策略均为：<code class="language-plaintext highlighter-rouge">cacheTime</code>。</p>

<h2 id="缓存清除策略">缓存清除策略</h2>
<h3 id="内存缓存清除策略">内存缓存清除策略</h3>
<p>内存缓存的清除策略分为两种：</p>
<ol>
  <li>系统控制：超过 <code class="language-plaintext highlighter-rouge">NSCache</code> 的“软限制”，系统自动清除。</li>
  <li>自己控制：定期扫描已缓存的对象，移出已过期的对象。</li>
</ol>

<p>对于缓存系统来说，策略 1 是可能有负面影响的，因为系统可能会清除一个仍未过期的对象，从而降低了缓存的命中率。可行的解决办法是：提高 <code class="language-plaintext highlighter-rouge">NSCache</code> 的 <code class="language-plaintext highlighter-rouge">totalCostLimit</code>。然而这会导致缓存对象所占用的内存变大，需要自己把握这之间的平衡。</p>

<p>内存缓存的对象默认的有效期是 5 分钟。关于策略 2，Kingfisher 设计了一个 <code class="language-plaintext highlighter-rouge">timer</code> 来周期性（默认周期为 2 分钟）地清除过期的缓存对象。</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">cleanTimer</span> <span class="o">=</span> <span class="o">.</span><span class="nf">scheduledTimer</span><span class="p">(</span><span class="nv">withTimeInterval</span><span class="p">:</span> <span class="n">config</span><span class="o">.</span><span class="n">cleanInterval</span><span class="p">,</span> <span class="nv">repeats</span><span class="p">:</span> <span class="kc">true</span><span class="p">)</span> <span class="p">{</span> <span class="p">[</span><span class="k">weak</span> <span class="k">self</span><span class="p">]</span> <span class="n">_</span> <span class="k">in</span>
    <span class="k">guard</span> <span class="k">let</span> <span class="nv">self</span> <span class="o">=</span> <span class="k">self</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="p">}</span>
    <span class="k">self</span><span class="o">.</span><span class="nf">removeExpired</span><span class="p">()</span>
<span class="p">}</span>

<span class="o">...</span>
<span class="o">...</span>

<span class="kd">public</span> <span class="kd">func</span> <span class="nf">removeExpired</span><span class="p">()</span> <span class="p">{</span>
    <span class="n">lock</span><span class="o">.</span><span class="nf">lock</span><span class="p">()</span>
    <span class="k">defer</span> <span class="p">{</span> <span class="n">lock</span><span class="o">.</span><span class="nf">unlock</span><span class="p">()</span> <span class="p">}</span>
    <span class="k">for</span> <span class="n">key</span> <span class="k">in</span> <span class="n">keys</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">nsKey</span> <span class="o">=</span> <span class="n">key</span> <span class="k">as</span> <span class="kt">NSString</span>
        <span class="k">guard</span> <span class="k">let</span> <span class="nv">object</span> <span class="o">=</span> <span class="n">storage</span><span class="o">.</span><span class="nf">object</span><span class="p">(</span><span class="nv">forKey</span><span class="p">:</span> <span class="n">nsKey</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span>
            <span class="n">keys</span><span class="o">.</span><span class="nf">remove</span><span class="p">(</span><span class="n">key</span><span class="p">)</span>
            <span class="k">continue</span>
        <span class="p">}</span>
        <span class="k">if</span> <span class="n">object</span><span class="o">.</span><span class="n">estimatedExpiration</span><span class="o">.</span><span class="n">isPast</span> <span class="p">{</span>
            <span class="n">storage</span><span class="o">.</span><span class="nf">removeObject</span><span class="p">(</span><span class="nv">forKey</span><span class="p">:</span> <span class="n">nsKey</span><span class="p">)</span>
            <span class="n">keys</span><span class="o">.</span><span class="nf">remove</span><span class="p">(</span><span class="n">key</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>

</code></pre></div></div>

<h3 id="硬盘缓存清除策略">硬盘缓存清除策略</h3>
<p>硬盘缓存的对象默认的有效期是 7 天，其清除策略是自己控制的。<br />
类似地，Kingfisher 也对硬盘缓存的可用容量做了限制，通过 <code class="language-plaintext highlighter-rouge">sizeLimit</code> 来控制。但 Kingfisher 在对象存入硬盘缓存时，并未对对象的大小做检查。我的猜想是一方面是可以提高缓存的命中率，另一方面是有其他更好的时机来核查缓存对象的大小，并清除过大的缓存对象。</p>

<p>如果 app 进入了 <code class="language-plaintext highlighter-rouge">UIApplication.State.background</code> 状态，Kingfisher 就会开始清理过期的对象和过大的缓存对象。</p>

<p>清理过期对象的做法：</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">removeExpiredValues</span><span class="p">(</span><span class="nv">referenceDate</span><span class="p">:</span> <span class="kt">Date</span><span class="p">)</span> <span class="k">throws</span> <span class="o">-&gt;</span> <span class="p">[</span><span class="kt">URL</span><span class="p">]</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">propertyKeys</span><span class="p">:</span> <span class="p">[</span><span class="kt">URLResourceKey</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span>
        <span class="o">.</span><span class="n">isDirectoryKey</span><span class="p">,</span>
        <span class="o">.</span><span class="n">contentModificationDateKey</span>
    <span class="p">]</span>
    
    <span class="k">let</span> <span class="nv">urls</span> <span class="o">=</span> <span class="k">try</span> <span class="nf">allFileURLs</span><span class="p">(</span><span class="nv">for</span><span class="p">:</span> <span class="n">propertyKeys</span><span class="p">)</span>
    <span class="k">let</span> <span class="nv">keys</span> <span class="o">=</span> <span class="kt">Set</span><span class="p">(</span><span class="n">propertyKeys</span><span class="p">)</span>
    <span class="k">let</span> <span class="nv">expiredFiles</span> <span class="o">=</span> <span class="n">urls</span><span class="o">.</span><span class="n">filter</span> <span class="p">{</span> <span class="n">fileURL</span> <span class="k">in</span>
        <span class="k">do</span> <span class="p">{</span>
            <span class="k">let</span> <span class="nv">meta</span> <span class="o">=</span> <span class="k">try</span> <span class="kt">FileMeta</span><span class="p">(</span><span class="nv">fileURL</span><span class="p">:</span> <span class="n">fileURL</span><span class="p">,</span> <span class="nv">resourceKeys</span><span class="p">:</span> <span class="n">keys</span><span class="p">)</span>
            <span class="k">if</span> <span class="n">meta</span><span class="o">.</span><span class="n">isDirectory</span> <span class="p">{</span>
                <span class="k">return</span> <span class="kc">false</span>
            <span class="p">}</span>
            <span class="k">return</span> <span class="n">meta</span><span class="o">.</span><span class="nf">expired</span><span class="p">(</span><span class="nv">referenceDate</span><span class="p">:</span> <span class="n">referenceDate</span><span class="p">)</span>
        <span class="p">}</span> <span class="k">catch</span> <span class="p">{</span>
            <span class="k">return</span> <span class="kc">true</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">try</span> <span class="n">expiredFiles</span><span class="o">.</span><span class="n">forEach</span> <span class="p">{</span> <span class="n">url</span> <span class="k">in</span>
        <span class="k">try</span> <span class="nf">removeFile</span><span class="p">(</span><span class="nv">at</span><span class="p">:</span> <span class="n">url</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">expiredFiles</span>
<span class="p">}</span>
</code></pre></div></div>

<p>清理过大对象的做法：</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">removeSizeExceededValues</span><span class="p">()</span> <span class="k">throws</span> <span class="o">-&gt;</span> <span class="p">[</span><span class="kt">URL</span><span class="p">]</span> <span class="p">{</span>
    
    <span class="k">if</span> <span class="n">config</span><span class="o">.</span><span class="n">sizeLimit</span> <span class="o">==</span> <span class="mi">0</span> <span class="p">{</span> <span class="k">return</span> <span class="p">[]</span> <span class="p">}</span> <span class="c1">// 0 意味着无大小限制。</span>
    
    <span class="k">var</span> <span class="nv">size</span> <span class="o">=</span> <span class="k">try</span> <span class="nf">totalSize</span><span class="p">()</span>
    <span class="k">if</span> <span class="n">size</span> <span class="o">&lt;</span> <span class="n">config</span><span class="o">.</span><span class="n">sizeLimit</span> <span class="p">{</span> <span class="k">return</span> <span class="p">[]</span> <span class="p">}</span>
    
    <span class="k">let</span> <span class="nv">propertyKeys</span><span class="p">:</span> <span class="p">[</span><span class="kt">URLResourceKey</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span>
        <span class="o">.</span><span class="n">isDirectoryKey</span><span class="p">,</span>
        <span class="o">.</span><span class="n">creationDateKey</span><span class="p">,</span>
        <span class="o">.</span><span class="n">fileSizeKey</span>
    <span class="p">]</span>
    <span class="k">let</span> <span class="nv">keys</span> <span class="o">=</span> <span class="kt">Set</span><span class="p">(</span><span class="n">propertyKeys</span><span class="p">)</span>
    
    <span class="k">let</span> <span class="nv">urls</span> <span class="o">=</span> <span class="k">try</span> <span class="nf">allFileURLs</span><span class="p">(</span><span class="nv">for</span><span class="p">:</span> <span class="n">propertyKeys</span><span class="p">)</span>
    <span class="k">var</span> <span class="nv">pendings</span><span class="p">:</span> <span class="p">[</span><span class="kt">FileMeta</span><span class="p">]</span> <span class="o">=</span> <span class="n">urls</span><span class="o">.</span><span class="n">compactMap</span> <span class="p">{</span> <span class="n">fileURL</span> <span class="k">in</span>
        <span class="k">guard</span> <span class="k">let</span> <span class="nv">meta</span> <span class="o">=</span> <span class="k">try</span><span class="p">?</span> <span class="kt">FileMeta</span><span class="p">(</span><span class="nv">fileURL</span><span class="p">:</span> <span class="n">fileURL</span><span class="p">,</span> <span class="nv">resourceKeys</span><span class="p">:</span> <span class="n">keys</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span>
            <span class="k">return</span> <span class="kc">nil</span>
        <span class="p">}</span>
        <span class="k">return</span> <span class="n">meta</span>
    <span class="p">}</span>

    <span class="c1">// 以上一次访问时间从大到小排序，最近访问的对象排在最前。</span>
    <span class="n">pendings</span><span class="o">.</span><span class="nf">sort</span><span class="p">(</span><span class="nv">by</span><span class="p">:</span> <span class="kt">FileMeta</span><span class="o">.</span><span class="n">lastAccessDate</span><span class="p">)</span>
    
    <span class="k">var</span> <span class="nv">removed</span><span class="p">:</span> <span class="p">[</span><span class="kt">URL</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">let</span> <span class="nv">target</span> <span class="o">=</span> <span class="n">config</span><span class="o">.</span><span class="n">sizeLimit</span> <span class="o">/</span> <span class="mi">2</span>
    <span class="k">while</span> <span class="n">size</span> <span class="o">&gt;</span> <span class="n">target</span><span class="p">,</span> <span class="k">let</span> <span class="nv">meta</span> <span class="o">=</span> <span class="n">pendings</span><span class="o">.</span><span class="nf">popLast</span><span class="p">()</span> <span class="p">{</span>
        <span class="n">size</span> <span class="o">-=</span> <span class="kt">UInt</span><span class="p">(</span><span class="n">meta</span><span class="o">.</span><span class="n">fileSize</span><span class="p">)</span>
        <span class="k">try</span> <span class="nf">removeFile</span><span class="p">(</span><span class="nv">at</span><span class="p">:</span> <span class="n">meta</span><span class="o">.</span><span class="n">url</span><span class="p">)</span>
        <span class="n">removed</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="n">meta</span><span class="o">.</span><span class="n">url</span><span class="p">)</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">removed</span>
<span class="p">}</span>
</code></pre></div></div>

<p>「清理过大对象」这个说法其实有些模糊，更准确地说明是：如果当前硬盘缓存的容量大于 <code class="language-plaintext highlighter-rouge">sizeLimit</code>，则根据对象的上一次访问时间进行排序，最近访问的对象排在前面。Kingfisher 的做法是：从最不经常访问的对象开始删除，直到当前存储的对象的总大小缩减至一半及其以下，才停止删除。这是经典的 LRU 缓存淘汰算法。</p>

<h2 id="出错处理">出错处理</h2>
<p>Kingfisher 把自身模块内的错误细分为 5 类，分别为：</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">/// Represents the error reason during networking request phase.</span>
<span class="k">case</span> <span class="nf">requestError</span><span class="p">(</span><span class="nv">reason</span><span class="p">:</span> <span class="kt">RequestErrorReason</span><span class="p">)</span>
<span class="c1">/// Represents the error reason during networking response phase.</span>
<span class="k">case</span> <span class="nf">responseError</span><span class="p">(</span><span class="nv">reason</span><span class="p">:</span> <span class="kt">ResponseErrorReason</span><span class="p">)</span>
<span class="c1">/// Represents the error reason during Kingfisher caching system.</span>
<span class="k">case</span> <span class="nf">cacheError</span><span class="p">(</span><span class="nv">reason</span><span class="p">:</span> <span class="kt">CacheErrorReason</span><span class="p">)</span>
<span class="c1">/// Represents the error reason during image processing phase.</span>
<span class="k">case</span> <span class="nf">processorError</span><span class="p">(</span><span class="nv">reason</span><span class="p">:</span> <span class="kt">ProcessorErrorReason</span><span class="p">)</span>
<span class="c1">/// Represents the error reason during image setting in a view related class.</span>
<span class="k">case</span> <span class="nf">imageSettingError</span><span class="p">(</span><span class="nv">reason</span><span class="p">:</span> <span class="kt">ImageSettingErrorReason</span><span class="p">)</span>
</code></pre></div></div>
<p>每类错误还会携带对应出错原因。从错误的分类，可以大致看出 Kingfisher 分为图片下载模块、图片缓存模块、图片处理模块等模块。</p>

<h2 id="其他">其他</h2>
<p>在阅读 Kingfisher 缓存系统相关代码时，我还发现了其他值得学习的代码，一并记录。</p>
<h3 id="网络请求重试机制">网络请求重试机制</h3>
<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2022/kf_02.png" alt="Kingfisher 重试机制：RetryStrategy 根据数据源、错误和重试次数决定继续重试或停止" /><br />
Kingfisher 设计一个 <code class="language-plaintext highlighter-rouge">RetryStrategy</code> 协议，由遵循该协议的对象来作出决定（<code class="language-plaintext highlighter-rouge">RetryDecision</code>）。<code class="language-plaintext highlighter-rouge">RetryDecision</code> 有两种：继续重试和停止重试。</p>

<p>遵循 <code class="language-plaintext highlighter-rouge">RetryStrategy</code> 协议的对象内部设计了三种重试模式：</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">struct</span> <span class="kt">DelayRetryStrategy</span><span class="p">:</span> <span class="kt">RetryStrategy</span> <span class="p">{</span>
    
    <span class="kd">public</span> <span class="kd">enum</span> <span class="kt">Interval</span> <span class="p">{</span>
        <span class="k">case</span> <span class="nf">seconds</span><span class="p">(</span><span class="kt">TimeInterval</span><span class="p">)</span> <span class="c1">// 指定 n 秒后重试</span>
        <span class="k">case</span> <span class="nf">accumulated</span><span class="p">(</span><span class="kt">TimeInterval</span><span class="p">)</span> <span class="c1">// 指定 n 秒以累加的方式重试，例如第一次为 3s 后重试，第二次就是 6s 后重试，以此类推。</span>
        <span class="k">case</span> <span class="nf">custom</span><span class="p">(</span><span class="nv">block</span><span class="p">:</span> <span class="p">(</span><span class="n">_</span> <span class="nv">retriedCount</span><span class="p">:</span> <span class="kt">Int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">TimeInterval</span><span class="p">)</span> <span class="c1">// 告诉你已经重试的次数，自行决定几秒后进行重试</span>
        
        <span class="kd">func</span> <span class="nf">timeInterval</span><span class="p">(</span><span class="k">for</span> <span class="nv">retriedCount</span><span class="p">:</span> <span class="kt">Int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">TimeInterval</span> <span class="p">{</span>
            <span class="k">let</span> <span class="nv">retryAfter</span><span class="p">:</span> <span class="kt">TimeInterval</span>
            <span class="k">switch</span> <span class="k">self</span> <span class="p">{</span>
                <span class="k">case</span> <span class="o">.</span><span class="nf">seconds</span><span class="p">(</span><span class="k">let</span> <span class="nv">interval</span><span class="p">):</span>
                <span class="n">retryAfter</span> <span class="o">=</span> <span class="n">interval</span>
                <span class="k">case</span> <span class="o">.</span><span class="nf">accumulated</span><span class="p">(</span><span class="k">let</span> <span class="nv">interval</span><span class="p">):</span>
                <span class="n">retryAfter</span> <span class="o">=</span> <span class="kt">Double</span><span class="p">(</span><span class="n">retriedCount</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">*</span> <span class="n">interval</span>
                <span class="k">case</span> <span class="o">.</span><span class="nf">custom</span><span class="p">(</span><span class="k">let</span> <span class="nv">block</span><span class="p">):</span>
                <span class="n">retryAfter</span> <span class="o">=</span> <span class="nf">block</span><span class="p">(</span><span class="n">retriedCount</span><span class="p">)</span>
            <span class="p">}</span>
            <span class="k">return</span> <span class="n">retryAfter</span>
        <span class="p">}</span>
    <span class="p">}</span>
    
    <span class="o">...</span>
    <span class="o">...</span>
    
<span class="p">}</span>
</code></pre></div></div>

<p>其核心方法实现如下：</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">func</span> <span class="nf">retry</span><span class="p">(</span><span class="nv">context</span><span class="p">:</span> <span class="kt">RetryContext</span><span class="p">,</span> <span class="nv">retryHandler</span><span class="p">:</span> <span class="kd">@escaping</span> <span class="p">(</span><span class="kt">RetryDecision</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Void</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// 超过最大重试次数后，停止重试</span>
    <span class="k">guard</span> <span class="n">context</span><span class="o">.</span><span class="n">retriedCount</span> <span class="o">&lt;</span> <span class="n">maxRetryCount</span> <span class="k">else</span> <span class="p">{</span>
        <span class="nf">retryHandler</span><span class="p">(</span><span class="o">.</span><span class="n">stop</span><span class="p">)</span>
        <span class="k">return</span>
    <span class="p">}</span>
    
    <span class="c1">// 当出错原因是用户取消该任务时，不进行重试</span>
    <span class="k">guard</span> <span class="o">!</span><span class="n">context</span><span class="o">.</span><span class="n">error</span><span class="o">.</span><span class="n">isTaskCancelled</span> <span class="k">else</span> <span class="p">{</span>
        <span class="nf">retryHandler</span><span class="p">(</span><span class="o">.</span><span class="n">stop</span><span class="p">)</span>
        <span class="k">return</span>
    <span class="p">}</span>
    
    <span class="c1">// 仅在网络请求响应出错时，进行重试</span>
    <span class="k">guard</span> <span class="k">case</span> <span class="kt">KingfisherError</span><span class="o">.</span><span class="n">responseError</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">error</span> <span class="k">else</span> <span class="p">{</span>
        <span class="nf">retryHandler</span><span class="p">(</span><span class="o">.</span><span class="n">stop</span><span class="p">)</span>
        <span class="k">return</span>
    <span class="p">}</span>
    
    <span class="c1">// 根据当前所使用的重试策略，结合当前已重试次数，计算出 interval 秒后进行重试</span>
    <span class="k">let</span> <span class="nv">interval</span> <span class="o">=</span> <span class="n">retryInterval</span><span class="o">.</span><span class="nf">timeInterval</span><span class="p">(</span><span class="nv">for</span><span class="p">:</span> <span class="n">context</span><span class="o">.</span><span class="n">retriedCount</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">interval</span> <span class="o">==</span> <span class="mi">0</span> <span class="p">{</span> <span class="c1">// 立即进行重试</span>
        <span class="nf">retryHandler</span><span class="p">(</span><span class="o">.</span><span class="nf">retry</span><span class="p">(</span><span class="nv">userInfo</span><span class="p">:</span> <span class="kc">nil</span><span class="p">))</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="c1">// 延迟 interval 秒后，提交重试任务。</span>
        <span class="kt">DispatchQueue</span><span class="o">.</span><span class="n">main</span><span class="o">.</span><span class="nf">asyncAfter</span><span class="p">(</span><span class="nv">deadline</span><span class="p">:</span> <span class="o">.</span><span class="nf">now</span><span class="p">()</span> <span class="o">+</span> <span class="n">interval</span><span class="p">)</span> <span class="p">{</span>
            <span class="nf">retryHandler</span><span class="p">(</span><span class="o">.</span><span class="nf">retry</span><span class="p">(</span><span class="nv">userInfo</span><span class="p">:</span> <span class="kc">nil</span><span class="p">))</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="委托模式">委托模式</h3>
<p>iOS 开发者最熟悉的设计模式之一就是 <code class="language-plaintext highlighter-rouge">Delegate</code>，常见于 <code class="language-plaintext highlighter-rouge">UITableViewDelegate</code> 等。<br />
平时自定义的类有时也需要把某个方法的实现委托给另一个类去实现。<br />
例如，<code class="language-plaintext highlighter-rouge">Task</code> 的 <code class="language-plaintext highlighter-rouge">finish(taskID: String)</code> 方法需要交给外部去实现：</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="kt">Task</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">onFinished</span><span class="p">:</span> <span class="p">((</span><span class="kt">String</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">String</span><span class="p">?)?</span>
    
    <span class="kd">func</span> <span class="nf">finish</span><span class="p">(</span><span class="nv">taskID</span><span class="p">:</span> <span class="kt">String</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">string</span> <span class="o">=</span> <span class="nf">onFinished</span><span class="p">?(</span><span class="n">taskID</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="kt">String</span><span class="p">(</span><span class="nv">describing</span><span class="p">:</span> <span class="n">string</span><span class="p">))</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">class</span> <span class="kt">KFViewController</span><span class="p">:</span> <span class="kt">UIViewController</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">subtaskAmount</span> <span class="o">=</span> <span class="mi">3</span>
    <span class="k">override</span> <span class="kd">func</span> <span class="nf">viewDidLoad</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">super</span><span class="o">.</span><span class="nf">viewDidLoad</span><span class="p">()</span>
    
        <span class="k">let</span> <span class="nv">task</span> <span class="o">=</span> <span class="kt">Task</span><span class="p">()</span>
        <span class="n">task</span><span class="o">.</span><span class="n">onFinished</span> <span class="o">=</span> <span class="p">{</span> <span class="p">[</span><span class="k">weak</span> <span class="k">self</span><span class="p">]</span> <span class="n">taskID</span> <span class="k">in</span>
            <span class="k">guard</span> <span class="k">let</span> <span class="nv">self</span> <span class="o">=</span> <span class="k">self</span> <span class="k">else</span> <span class="p">{</span>
                <span class="k">return</span> <span class="kc">nil</span>
            <span class="p">}</span>
            
            <span class="k">let</span> <span class="nv">string</span> <span class="o">=</span> <span class="s">"Task </span><span class="se">\(</span><span class="n">taskID</span><span class="se">)</span><span class="s"> is finished, the number of subtasks is </span><span class="se">\(</span><span class="k">self</span><span class="o">.</span><span class="n">subtaskAmount</span><span class="se">)</span><span class="s">"</span>
            <span class="k">return</span> <span class="n">string</span>
        <span class="p">}</span>
        <span class="c1">// 输出：Optional("Task 123 is finished, the number of subtasks is 3")</span>
        <span class="n">task</span><span class="o">.</span><span class="nf">finish</span><span class="p">(</span><span class="nv">taskID</span><span class="p">:</span> <span class="s">"123"</span><span class="p">)</span>

        <span class="o">...</span>
        <span class="o">...</span>

</code></pre></div></div>
<p>这样的写法是可行的，但每次必须记得在 <code class="language-plaintext highlighter-rouge">onFinished</code> 闭包中弱引用 <code class="language-plaintext highlighter-rouge">self</code> 以免出现循环引用的问题，导致内存泄漏。</p>

<p>Kingfisher 中有这样一个好用的工具类：<code class="language-plaintext highlighter-rouge">class Delegate&lt;Input, Output&gt;</code>：</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="kt">Delegate</span><span class="o">&lt;</span><span class="kt">Input</span><span class="p">,</span> <span class="kt">Output</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="kd">public</span> <span class="nf">init</span><span class="p">()</span> <span class="p">{}</span>

    <span class="kd">private</span> <span class="k">var</span> <span class="nv">block</span><span class="p">:</span> <span class="p">((</span><span class="kt">Input</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Output</span><span class="p">?)?</span>
    <span class="kd">public</span> <span class="kd">func</span> <span class="n">delegate</span><span class="o">&lt;</span><span class="kt">T</span><span class="p">:</span> <span class="kt">AnyObject</span><span class="o">&gt;</span><span class="p">(</span><span class="n">on</span> <span class="nv">target</span><span class="p">:</span> <span class="kt">T</span><span class="p">,</span> <span class="nv">block</span><span class="p">:</span> <span class="p">((</span><span class="kt">T</span><span class="p">,</span> <span class="kt">Input</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Output</span><span class="p">)?)</span> <span class="p">{</span>
        <span class="k">self</span><span class="o">.</span><span class="n">block</span> <span class="o">=</span> <span class="p">{</span> <span class="p">[</span><span class="k">weak</span> <span class="n">target</span><span class="p">]</span> <span class="n">input</span> <span class="k">in</span>
            <span class="k">guard</span> <span class="k">let</span> <span class="nv">target</span> <span class="o">=</span> <span class="n">target</span> <span class="k">else</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">nil</span> <span class="p">}</span>
            <span class="k">return</span> <span class="nf">block</span><span class="p">?(</span><span class="n">target</span><span class="p">,</span> <span class="n">input</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="kd">public</span> <span class="kd">func</span> <span class="nf">call</span><span class="p">(</span><span class="n">_</span> <span class="nv">input</span><span class="p">:</span> <span class="kt">Input</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Output</span><span class="p">?</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nf">block</span><span class="p">?(</span><span class="n">input</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="kd">public</span> <span class="kd">func</span> <span class="nf">callAsFunction</span><span class="p">(</span><span class="n">_</span> <span class="nv">input</span><span class="p">:</span> <span class="kt">Input</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Output</span><span class="p">?</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nf">call</span><span class="p">(</span><span class="n">input</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>借助这个工具类，让我们看看优化后的版本：</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="kt">Task</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">onFinished</span> <span class="o">=</span> <span class="kt">Delegate</span><span class="o">&lt;</span><span class="kt">String</span><span class="p">,</span> <span class="kt">String</span><span class="o">&gt;</span><span class="p">()</span>
    
    <span class="kd">func</span> <span class="nf">finish</span><span class="p">(</span><span class="n">_</span> <span class="nv">taskID</span><span class="p">:</span> <span class="kt">String</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">let</span> <span class="nv">string</span> <span class="o">=</span> <span class="n">onFinished</span><span class="o">.</span><span class="nf">call</span><span class="p">(</span><span class="n">taskID</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="kt">String</span><span class="p">(</span><span class="nv">describing</span><span class="p">:</span> <span class="n">string</span><span class="p">))</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">class</span> <span class="kt">KFViewController</span><span class="p">:</span> <span class="kt">UIViewController</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">subtaskAmount</span> <span class="o">=</span> <span class="mi">3</span>
    <span class="k">override</span> <span class="kd">func</span> <span class="nf">viewDidLoad</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">super</span><span class="o">.</span><span class="nf">viewDidLoad</span><span class="p">()</span>
        
        <span class="k">let</span> <span class="nv">task</span> <span class="o">=</span> <span class="kt">Task</span><span class="p">()</span>
        <span class="n">task</span><span class="o">.</span><span class="n">onFinished</span><span class="o">.</span><span class="nf">delegate</span><span class="p">(</span><span class="nv">on</span><span class="p">:</span> <span class="k">self</span><span class="p">)</span> <span class="p">{</span> <span class="p">(</span><span class="k">self</span><span class="p">,</span> <span class="n">taskID</span><span class="p">)</span> <span class="k">in</span>
            <span class="k">let</span> <span class="nv">string</span> <span class="o">=</span> <span class="s">"Task </span><span class="se">\(</span><span class="n">taskID</span><span class="se">)</span><span class="s"> is finished, the amount of its subtask is </span><span class="se">\(</span><span class="k">self</span><span class="o">.</span><span class="n">subtaskAmount</span><span class="se">)</span><span class="s">"</span>
            <span class="k">return</span> <span class="n">string</span>
        <span class="p">}</span>
        
        <span class="n">task</span><span class="o">.</span><span class="nf">finish</span><span class="p">(</span><span class="s">"123"</span><span class="p">)</span>
        <span class="o">...</span>
        <span class="o">...</span>
</code></pre></div></div>

<p>下面第二个 <code class="language-plaintext highlighter-rouge">self</code> 已是弱引用后的版本，无需每次得加 <code class="language-plaintext highlighter-rouge">weak</code> 关键字了。</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">task</span><span class="o">.</span><span class="n">onFinished</span><span class="o">.</span><span class="nf">delegate</span><span class="p">(</span><span class="nv">on</span><span class="p">:</span> <span class="k">self</span><span class="p">)</span> <span class="p">{</span> <span class="p">(</span><span class="k">self</span><span class="p">,</span> <span class="n">taskID</span><span class="p">)</span> <span class="k">in</span>
</code></pre></div></div>
<p>再者，把匿名函数（Closure）包装成一个 <code class="language-plaintext highlighter-rouge">Delegate</code> 对象使用使得代码更加清晰。</p>

<h4 id="提问-1为啥-onfinishedcalltaskid-返回的是可选类型-string">提问 1：为啥 onFinished.call(taskID) 返回的是可选类型 String？</h4>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 这里的泛型约束明明是 String 类型</span>
<span class="k">var</span> <span class="nv">onFinished</span> <span class="o">=</span> <span class="kt">Delegate</span><span class="o">&lt;</span><span class="kt">String</span><span class="p">,</span> <span class="kt">String</span><span class="o">&gt;</span><span class="p">()</span>
</code></pre></div></div>
<p>因为 <code class="language-plaintext highlighter-rouge">Delegate</code> 内部实现是弱引用 <code class="language-plaintext highlighter-rouge">self</code>，所以 <code class="language-plaintext highlighter-rouge">self</code> 处于一种可能有可能没有的状态，这就导致返回值的类型均是可选类型的了。</p>

<h4 id="提问-2-如果把-output-泛型约束为可选类型那返回值类型不就是可选的可选类型吗">提问 2: 如果把 Output 泛型约束为可选类型，那返回值类型不就是可选的可选类型（??）吗？</h4>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="kt">Delegate</span><span class="o">&lt;</span><span class="kt">Input</span><span class="p">,</span> <span class="kt">Output</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="kd">public</span> <span class="nf">init</span><span class="p">()</span> <span class="p">{}</span>
    <span class="kd">private</span> <span class="k">var</span> <span class="nv">block</span><span class="p">:</span> <span class="p">((</span><span class="kt">Input</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Output</span><span class="p">?)?</span>
    <span class="o">...</span>
    <span class="o">...</span>
</code></pre></div></div>
<p>Kingfisher 已对这种情况做了处理：</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">extension</span> <span class="kt">Delegate</span> <span class="k">where</span> <span class="kt">Output</span><span class="p">:</span> <span class="kt">OptionalProtocol</span> <span class="p">{</span>
    <span class="c1">// 此时 Output 的类型为可选类型</span>
    <span class="kd">public</span> <span class="kd">func</span> <span class="nf">call</span><span class="p">(</span><span class="n">_</span> <span class="nv">input</span><span class="p">:</span> <span class="kt">Input</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Output</span> <span class="p">{</span>
        <span class="k">if</span> <span class="k">let</span> <span class="nv">result</span> <span class="o">=</span> <span class="nf">block</span><span class="p">?(</span><span class="n">input</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">return</span> <span class="n">result</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="k">return</span> <span class="kt">Output</span><span class="o">.</span><span class="n">_createNil</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="kd">public</span> <span class="kd">func</span> <span class="nf">callAsFunction</span><span class="p">(</span><span class="n">_</span> <span class="nv">input</span><span class="p">:</span> <span class="kt">Input</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Output</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nf">call</span><span class="p">(</span><span class="n">input</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">public</span> <span class="kd">protocol</span> <span class="kt">OptionalProtocol</span> <span class="p">{</span>
    <span class="kd">static</span> <span class="k">var</span> <span class="nv">_createNil</span><span class="p">:</span> <span class="k">Self</span> <span class="p">{</span> <span class="k">get</span> <span class="p">}</span>
<span class="p">}</span>
<span class="kd">extension</span> <span class="kt">Optional</span> <span class="p">:</span> <span class="kt">OptionalProtocol</span> <span class="p">{</span>
    <span class="kd">public</span> <span class="kd">static</span> <span class="k">var</span> <span class="nv">_createNil</span><span class="p">:</span> <span class="kt">Optional</span><span class="o">&lt;</span><span class="kt">Wrapped</span><span class="o">&gt;</span> <span class="p">{</span>
         <span class="k">return</span> <span class="kc">nil</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>当 <code class="language-plaintext highlighter-rouge">Output</code> 类型为可选类型时，新增一个直接返回 <code class="language-plaintext highlighter-rouge">Output</code> 的 <code class="language-plaintext highlighter-rouge">call</code> 方法。</p>

<p>Kingfisher 还使用了 Swift <code class="language-plaintext highlighter-rouge">callAsFunction</code> 的新特性，这意味着可以无需再调用 <code class="language-plaintext highlighter-rouge">call</code> 方法，如：</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="kt">Task</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">onFinished</span> <span class="o">=</span> <span class="kt">Delegate</span><span class="o">&lt;</span><span class="kt">String</span><span class="p">,</span> <span class="kt">String</span><span class="o">&gt;</span><span class="p">()</span>
    
    <span class="kd">func</span> <span class="nf">finish</span><span class="p">(</span><span class="n">_</span> <span class="nv">taskID</span><span class="p">:</span> <span class="kt">String</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">//        let string = onFinished.call(taskID)</span>
        <span class="c1">/// 利用 callAsFunction 的特性</span>
        <span class="k">let</span> <span class="nv">string</span> <span class="o">=</span> <span class="nf">onFinished</span><span class="p">(</span><span class="n">taskID</span><span class="p">)</span>
        <span class="nf">print</span><span class="p">(</span><span class="kt">String</span><span class="p">(</span><span class="nv">describing</span><span class="p">:</span> <span class="n">string</span><span class="p">))</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="cachecallbackcoordinator">CacheCallbackCoordinator</h3>
<p>顾名思义就是缓存模块内的异步回调方法的协调器。</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 是否需要缓存原始图片</span>
<span class="k">let</span> <span class="nv">needToCacheOriginalImage</span> <span class="o">=</span> <span class="n">options</span><span class="o">.</span><span class="n">cacheOriginalImage</span> <span class="o">&amp;&amp;</span>
<span class="n">options</span><span class="o">.</span><span class="n">processor</span> <span class="o">!=</span> <span class="kt">DefaultImageProcessor</span><span class="o">.</span><span class="k">default</span>

<span class="k">let</span> <span class="nv">coordinator</span> <span class="o">=</span> <span class="kt">CacheCallbackCoordinator</span><span class="p">(</span>
    <span class="nv">shouldWaitForCache</span><span class="p">:</span> <span class="n">options</span><span class="o">.</span><span class="n">waitForCache</span><span class="p">,</span> <span class="nv">shouldCacheOriginal</span><span class="p">:</span> <span class="n">needToCacheOriginalImage</span><span class="p">)</span>
<span class="k">let</span> <span class="nv">result</span> <span class="o">=</span> <span class="kt">RetrieveImageResult</span><span class="p">(</span>
    <span class="nv">image</span><span class="p">:</span> <span class="n">options</span><span class="o">.</span><span class="n">imageModifier</span><span class="p">?</span><span class="o">.</span><span class="nf">modify</span><span class="p">(</span><span class="n">value</span><span class="o">.</span><span class="n">image</span><span class="p">)</span> <span class="p">??</span> <span class="n">value</span><span class="o">.</span><span class="n">image</span><span class="p">,</span>
    <span class="nv">cacheType</span><span class="p">:</span> <span class="o">.</span><span class="k">none</span><span class="p">,</span>
    <span class="nv">source</span><span class="p">:</span> <span class="n">source</span><span class="p">,</span>
    <span class="nv">originalSource</span><span class="p">:</span> <span class="n">context</span><span class="o">.</span><span class="n">originalSource</span>
<span class="p">)</span>
<span class="c1">// Add image to cache.</span>
<span class="k">let</span> <span class="nv">targetCache</span> <span class="o">=</span> <span class="n">options</span><span class="o">.</span><span class="n">targetCache</span> <span class="p">??</span> <span class="k">self</span><span class="o">.</span><span class="n">cache</span>
<span class="n">targetCache</span><span class="o">.</span><span class="nf">store</span><span class="p">(</span>
    <span class="n">value</span><span class="o">.</span><span class="n">image</span><span class="p">,</span>
    <span class="nv">original</span><span class="p">:</span> <span class="n">value</span><span class="o">.</span><span class="n">originalData</span><span class="p">,</span>
    <span class="nv">forKey</span><span class="p">:</span> <span class="n">source</span><span class="o">.</span><span class="n">cacheKey</span><span class="p">,</span>
    <span class="nv">options</span><span class="p">:</span> <span class="n">options</span><span class="p">,</span>
    <span class="nv">toDisk</span><span class="p">:</span> <span class="o">!</span><span class="n">options</span><span class="o">.</span><span class="n">cacheMemoryOnly</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">_</span> <span class="k">in</span>
    <span class="n">coordinator</span><span class="o">.</span><span class="nf">apply</span><span class="p">(</span><span class="o">.</span><span class="n">cachingImage</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">completionHandler</span><span class="p">?(</span><span class="o">.</span><span class="nf">success</span><span class="p">(</span><span class="n">result</span><span class="p">))</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">if</span> <span class="n">needToCacheOriginalImage</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">originalCache</span> <span class="o">=</span> <span class="n">options</span><span class="o">.</span><span class="n">originalCache</span> <span class="p">??</span> <span class="n">targetCache</span>
    <span class="n">originalCache</span><span class="o">.</span><span class="nf">storeToDisk</span><span class="p">(</span>
        <span class="n">value</span><span class="o">.</span><span class="n">originalData</span><span class="p">,</span>
        <span class="nv">forKey</span><span class="p">:</span> <span class="n">source</span><span class="o">.</span><span class="n">cacheKey</span><span class="p">,</span>
        <span class="nv">processorIdentifier</span><span class="p">:</span> <span class="kt">DefaultImageProcessor</span><span class="o">.</span><span class="k">default</span><span class="o">.</span><span class="n">identifier</span><span class="p">,</span>
        <span class="nv">expiration</span><span class="p">:</span> <span class="n">options</span><span class="o">.</span><span class="n">diskCacheExpiration</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">_</span> <span class="k">in</span>
        <span class="n">coordinator</span><span class="o">.</span><span class="nf">apply</span><span class="p">(</span><span class="o">.</span><span class="n">cachingOriginalImage</span><span class="p">)</span> <span class="p">{</span>
            <span class="nf">completionHandler</span><span class="p">?(</span><span class="o">.</span><span class="nf">success</span><span class="p">(</span><span class="n">result</span><span class="p">))</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="n">coordinator</span><span class="o">.</span><span class="nf">apply</span><span class="p">(</span><span class="o">.</span><span class="n">cacheInitiated</span><span class="p">)</span> <span class="p">{</span>
    <span class="nf">completionHandler</span><span class="p">?(</span><span class="o">.</span><span class="nf">success</span><span class="p">(</span><span class="n">result</span><span class="p">))</span>
<span class="p">}</span>
</code></pre></div></div>
<p>有两个布尔值在控制 <code class="language-plaintext highlighter-rouge">completionHandler?(.success(result))</code> 执行的时机，它们分别是：</p>
<ol>
  <li>是否需要缓存原始图片（needToCacheOriginalImage）</li>
  <li>是否等待缓存过程结束（shouldWaitForCache）</li>
</ol>

<p>在这两个变量的排列组合下，Kingfisher 需要确保该回调执行的次数<strong>有且只有一次！</strong></p>

<p>因此，<code class="language-plaintext highlighter-rouge">CacheCallbackCoordinator</code> 内部维护了一个包含四种状态的枚举 <code class="language-plaintext highlighter-rouge">State</code>、导致状态发生转移的三种 <code class="language-plaintext highlighter-rouge">Action</code>，以及描述状态是如何转移的 <code class="language-plaintext highlighter-rouge">apply</code> 方法：</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">enum</span> <span class="kt">State</span> <span class="p">{</span>
    <span class="k">case</span> <span class="n">idle</span>
    <span class="k">case</span> <span class="n">imageCached</span>
    <span class="k">case</span> <span class="n">originalImageCached</span>
    <span class="k">case</span> <span class="n">done</span>
<span class="p">}</span>

<span class="kd">enum</span> <span class="kt">Action</span> <span class="p">{</span>
    <span class="k">case</span> <span class="n">cacheInitiated</span>
    <span class="k">case</span> <span class="n">cachingImage</span>
    <span class="k">case</span> <span class="n">cachingOriginalImage</span>
<span class="p">}</span>

<span class="kd">func</span> <span class="nf">apply</span><span class="p">(</span><span class="n">_</span> <span class="nv">action</span><span class="p">:</span> <span class="kt">Action</span><span class="p">,</span> <span class="nv">trigger</span><span class="p">:</span> <span class="p">()</span> <span class="o">-&gt;</span> <span class="kt">Void</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">switch</span> <span class="p">(</span><span class="n">state</span><span class="p">,</span> <span class="n">action</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">case</span> <span class="p">(</span><span class="o">.</span><span class="n">done</span><span class="p">,</span> <span class="n">_</span><span class="p">):</span>
        <span class="k">break</span>
        
        <span class="c1">// From .idle</span>
        <span class="k">case</span> <span class="p">(</span><span class="o">.</span><span class="n">idle</span><span class="p">,</span> <span class="o">.</span><span class="n">cacheInitiated</span><span class="p">):</span>
        <span class="k">if</span> <span class="o">!</span><span class="n">shouldWaitForCache</span> <span class="p">{</span>
            <span class="n">state</span> <span class="o">=</span> <span class="o">.</span><span class="n">done</span>
            <span class="nf">trigger</span><span class="p">()</span>
        <span class="p">}</span>
        <span class="k">case</span> <span class="p">(</span><span class="o">.</span><span class="n">idle</span><span class="p">,</span> <span class="o">.</span><span class="n">cachingImage</span><span class="p">):</span>
        <span class="k">if</span> <span class="n">shouldCacheOriginal</span> <span class="p">{</span>
            <span class="n">state</span> <span class="o">=</span> <span class="o">.</span><span class="n">imageCached</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="n">state</span> <span class="o">=</span> <span class="o">.</span><span class="n">done</span>
            <span class="nf">trigger</span><span class="p">()</span>
        <span class="p">}</span>
        <span class="k">case</span> <span class="p">(</span><span class="o">.</span><span class="n">idle</span><span class="p">,</span> <span class="o">.</span><span class="n">cachingOriginalImage</span><span class="p">):</span>
        <span class="n">state</span> <span class="o">=</span> <span class="o">.</span><span class="n">originalImageCached</span>
        
        <span class="c1">// From .imageCached</span>
        <span class="k">case</span> <span class="p">(</span><span class="o">.</span><span class="n">imageCached</span><span class="p">,</span> <span class="o">.</span><span class="n">cachingOriginalImage</span><span class="p">):</span>
        <span class="n">state</span> <span class="o">=</span> <span class="o">.</span><span class="n">done</span>
        <span class="nf">trigger</span><span class="p">()</span>
        
        <span class="c1">// From .originalImageCached</span>
        <span class="k">case</span> <span class="p">(</span><span class="o">.</span><span class="n">originalImageCached</span><span class="p">,</span> <span class="o">.</span><span class="n">cachingImage</span><span class="p">):</span>
        <span class="n">state</span> <span class="o">=</span> <span class="o">.</span><span class="n">done</span>
        <span class="nf">trigger</span><span class="p">()</span>
        
        <span class="k">default</span><span class="p">:</span>
        <span class="nf">assertionFailure</span><span class="p">(</span><span class="s">"This case should not happen in CacheCallbackCoordinator: </span><span class="se">\(</span><span class="n">state</span><span class="se">)</span><span class="s"> - </span><span class="se">\(</span><span class="n">action</span><span class="se">)</span><span class="s">"</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>有了这些，便可以画出一个有限状态机的状态转移图：<img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/2022/kf_03.png" alt="CacheCallbackCoordinator 状态机" /><br />
从图中可以看出：在从 <code class="language-plaintext highlighter-rouge">idle</code> 状态到 <code class="language-plaintext highlighter-rouge">done</code> 状态的任意一条路线上，<code class="language-plaintext highlighter-rouge">trigger</code> 方法的执行次数<strong>有且只有一次</strong>！这样设计的代码清晰，也容易测试，是很值得学习的案例了！</p>

<h2 id="结语">结语</h2>
<p>读 Kingfisher 源码要分享的第一部分就到此结束了，还有图片下载流程、图片处理流程、对多平台的支持（Kingfisher 还支持 watchOS、tvOS 和 macOS）等。</p>

<p>这也是我第一次比较认真地花大段时间仔细读开源代码，收获颇丰。</p>

<p>就这样慢慢进步吧！</p>]]></content><author><name>Sunset Wan</name></author><category term="Kingfisher" /><summary type="html"><![CDATA[最近想了解如何设计一个简单的缓存系统，于是找了 Kingfisher 7.1.2 版本的源代码读一读，有些收获，记录于此。]]></summary></entry><entry><title type="html">WWDC21 10267 一窥 Xcode Cloud</title><link href="https://sunsetwan.github.io/2021/08/09/WWDC%2021%2010267%20%E4%B8%80%E7%AA%A5%20Xcode%20Cloud/" rel="alternate" type="text/html" title="WWDC21 10267 一窥 Xcode Cloud" /><published>2021-08-09T09:41:00+08:00</published><updated>2021-08-09T09:41:00+08:00</updated><id>https://sunsetwan.github.io/2021/08/09/WWDC%2021%2010267%20%E4%B8%80%E7%AA%A5%20Xcode%20Cloud</id><content type="html" xml:base="https://sunsetwan.github.io/2021/08/09/WWDC%2021%2010267%20%E4%B8%80%E7%AA%A5%20Xcode%20Cloud/"><![CDATA[<p>有幸参与《WWDC 21 内参》的工作，欢迎 <a href="https://xiaozhuanlan.com/wwdc21">订阅</a> 该专栏！<!-- more --></p>

<p>Xcode Cloud 是为 Apple 平台开发者所设计的一项易用的集持续集成（Continuous Integration）和持续交付（Continuous Delivery）于一体的服务。</p>

<blockquote>
  <p>持续集成指的是周期性地合并发生变动的代码，这样能尽早地发现问题解决问题，以提高代码质量。</p>
</blockquote>

<p>一个典型的持续集成工作流程如下图所示：<br />
<img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/WWDC%2021/%E6%AD%A3%E5%BC%8F/%E6%88%AA%E5%9B%BE%201.jpg" alt="典型持续集成流程：推送 Git 变更后执行构建、测试与自定义任务，再生成产物和通知" /></p>
<ol>
  <li>将发生变动的代码推送至远端代码仓库。</li>
  <li>项目开始构建、运行测试代码和其他自定义步骤。</li>
  <li>如若通过质量测试（构建成功、测试通过等），保存构建产物，并发出通知至其他团队成员。</li>
</ol>

<p>再让我们看一下 iOS 应用常见的开发流程：<br />
<img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/WWDC%2021/%E6%AD%A3%E5%BC%8F/%E6%88%AA%E5%9B%BE%202.jpg" alt="iOS 应用开发循环：开发团队构建测试并分发，用户手动测试反馈后继续优化" /></p>
<ol>
  <li>修复 Bug 和完成新的需求。</li>
  <li>根据团队内部 Code Review 的反馈，对代码作相应的修改。</li>
  <li>通过自测后，将应用分发至测试团队手中。</li>
  <li>根据测试用户的反馈，及时地修复问题。</li>
</ol>

<p>高效地运作上图所示流程是构建高质量应用的关键。</p>

<p>嘭! Xcode Cloud 此时闪亮登场！<br />
<img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/WWDC%2021/%E6%AD%A3%E5%BC%8F/%E6%88%AA%E5%9B%BE%203.jpg" alt="Xcode Cloud 将 Xcode 开发、TestFlight 测试与 App Store 分发连接成闭环" /><br />
在上述持续集成的流程之上，Xcode Cloud 把应用开发、应用测试和应用分发三点连接起来，提供给开发者一条完整的开发流水线：</p>
<ol>
  <li>应用开发</li>
  <li>应用测试</li>
  <li>应用分发</li>
  <li>收集使用反馈</li>
  <li>根据反馈，进行快速迭代</li>
</ol>

<h3 id="立马上手-xcode-cloud">立马上手 Xcode Cloud</h3>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/WWDC%2021/%E6%AD%A3%E5%BC%8F/%E6%88%AA%E5%9B%BE%204%20%E6%96%B0.jpg" alt="Xcode 中 Fruta 的 Xcode Cloud 总览，左侧列出工作流与构建状态，右侧汇总各次构建" /></p>

<p>Fruta 是 Apple 用来演示 Xcode Cloud 的 App。在 Xcode 13 本地打开的 Xcode Cloud 控制台页面大致如上图所示，左边侧边栏展示了已完成/正在执行中的工作流，右边则是对多个工作流的总览，从中我们可以粗略了解整个团队开发的进度。</p>

<h4 id="什么是-xcode-cloud-workflow">什么是 Xcode Cloud Workflow？</h4>
<p>我们可以把 Xcode Cloud 工作流理解成某种配置。Xcode Cloud 会读取该配置的内容，从而明白自己在何时执行何种任务。某个工作流执行的过程称为一次构建。Xcode Cloud 依托于 Apple 自管理的云端基础设施。该基础设施提供代码签名，同时也能帮助开发者访问多种不同版本的操作系统和 Xcode，例如最新版的 macOS Monterey 和 Xcode 13。</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/WWDC%2021/%E6%AD%A3%E5%BC%8F/%E6%88%AA%E5%9B%BE%205.jpg" alt="Fruta Build 595 总览：iOS 和 macOS 归档成功，iOS 测试失败，TestFlight 后置操作完成" /></p>

<p>在左侧边栏中点选某个工作流，展现在我们面前的是该工作流单次运行的总览。从这里，我们能看到测试代码运行的结果与相应的日志，甚至可以双击报错处，直接定位到导致出错的某一行代码。</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/WWDC%2021/%E6%AD%A3%E5%BC%8F/%E6%88%AA%E5%9B%BE%206.jpg" alt="App Store Connect 网页中的 Fruta Xcode Cloud 构建列表" /></p>

<p>这些内容可不只是在 Xcode 本地才能看到，也可以在 App Store Connect 网页中获取到，包括开始构建、管理工作流、检阅构建产物、与团队成员共享构建报告和管理通知等。比如，在构建完成后，可以在网页中设置结果通知到 Slack 等交流协作工具，以帮助团队成员在第一时间知道结果。</p>

<p>值得一提的是：网页版的 Xcode Cloud 提供与 Xcode 应用内一致的<strong>全功能体验</strong>。</p>

<h4 id="one-more-thing">One More Thing…</h4>
<p>Xcode Cloud 不光是团队开发以及协作流程的好帮手，而且 Apple 一贯良好的隐私保护也未曾在它身上缺席，源代码的管理是软件工程中的核心之一。Xcode Cloud 的方方面面均是为保护用户数据而设计的：</p>
<ul>
  <li>构建环境的信息过一段时间会被删除</li>
  <li>从不存储项目源代码</li>
  <li>构建数据会被加密存储在 CloudKit 数据库中</li>
  <li>用户可随时随地删除数据，且数据会被从 Xcode Cloud 系统中完全移除</li>
</ul>

<h2 id="为你的工程配置-xcode-cloud">为你的工程配置 Xcode Cloud</h2>
<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/WWDC%2021/%E6%AD%A3%E5%BC%8F/%E6%88%AA%E5%9B%BE%207.jpg" alt="Xcode 的 Product 菜单中选择 Xcode Cloud，再点击 Create Workflow" /><br />
 在 Xcode 13 中（满足一些前提条件之后）仅需几步操作即可为你的工程配置 Xcode Cloud。</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/WWDC%2021/%E6%AD%A3%E5%BC%8F/%E6%88%AA%E5%9B%BE%208.jpg" alt="Xcode Cloud 产品选择页中选中支持 iOS 和 macOS 的 Fruta App" /><br />
接着，选中接入 Xcode Cloud 的应用（这里以 Fruta 为例），·</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/WWDC%2021/%E6%AD%A3%E5%BC%8F/%E6%88%AA%E5%9B%BE%209.jpg" alt="Default 工作流预览：main 分支变更触发，使用最新 Xcode，归档 iOS 与 macOS" /><br />
然后，开始预览一个工作流。工作流最基本的四个要素为：</p>
<ul>
  <li>启动的条件</li>
  <li>构建的环境</li>
  <li>执行的任务</li>
  <li>结束后执行的任务</li>
</ul>

<p>想要深入了解工作流的编辑过程，请参考 <a href="https://developer.apple.com/videos/play/wwdc2021/10268/">WWDC21 10268 探索 Xcode Cloud 工作流</a>。</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/WWDC%2021/%E6%AD%A3%E5%BC%8F/%E6%88%AA%E5%9B%BE%2010.jpg" alt="Xcode Cloud 请求访问 GitHub 上的 FrutaApp 主仓库与 scripts 仓库" /><br />
然后需要给 Xcode Cloud 授权以获取 Fruta 源代码。授权范围不光光是代码主仓库，也包括名下的子模块以及私有的 Swift Package。</p>

<p>Fruta App 使用到两个托管于 GitHub 上的私有仓库，所以点击「Grant Access」会跳转到 App Store Connect 网页界面。值得一提的是：这个流程取决于工程代码是如何托管的。Xcode Cloud 要求使用 Git 来管理工程代码，且仅支持以下几种源代码管理提供商：</p>
<ul>
  <li>Bitbucket Cloud 和 Bitbucket Server</li>
  <li>GitHub 和 GitHub 企业版</li>
  <li>GitLab 和自建版本的 GitLab</li>
</ul>

<p>Xcode Cloud 也支持 Git 大文件存储（Git LFS）。</p>

<p>整个授权过程将 Apple ID 与源代码管理提供商的账号相关联，并使用源代码管理提供商原生的授权方式与 Xcode Cloud 的安全加密，以确保源代码和用户信息处于保护之下。</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/WWDC%2021/%E6%AD%A3%E5%BC%8F/%E6%88%AA%E5%9B%BE%2012.jpg" alt="FrutaApp 主仓库与 scripts 仓库均已授权 Xcode Cloud 访问" /><br />
在 App Store Connect 上完成授权后就可以回到 Xcode 本地继续之前的流程。</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/WWDC%2021/%E6%AD%A3%E5%BC%8F/%E6%88%AA%E5%9B%BE%2018%20%EF%BC%88%E6%9C%AA%E4%B8%8A%E4%BC%A0%E8%87%B3%E5%9B%BE%E5%BA%8A%EF%BC%89.jpg" alt="确认将 Fruta App 与 App Store Connect 中的 Xcode Cloud 构建关联" /><br />
点击 <code class="language-plaintext highlighter-rouge">Complete</code> 按钮，Xcode Cloud 将会帮助开发者把应用信息注册至 App Store Connect。</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/WWDC%2021/%E6%AD%A3%E5%BC%8F/%E6%88%AA%E5%9B%BE%2014.jpg" alt="Fruta 已配置 Xcode Cloud，选择 main 分支并点击 Start Build 开始首次构建" /><br />
终于到了最后一步！点击 <code class="language-plaintext highlighter-rouge">Start Build</code> 按钮，就可以开始第一次构建。</p>

<h2 id="检阅-xcode-cloud-构建报告">检阅 Xcode Cloud 构建报告</h2>
<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/WWDC%2021/%E6%AD%A3%E5%BC%8F/%E6%88%AA%E5%9B%BE%2015%20%E6%96%B0.jpg" alt="Build 1 总览：iOS 归档完成，macOS 归档因两个问题失败" /><br />
以 Fruta 这次构建为例：从该构建报告中，我们可以清晰明了地知道构建耗时以及构建环境的配置。点击右上角的 <code class="language-plaintext highlighter-rouge">Rebuild</code> 按钮也能帮助我们重新开始构建。</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/WWDC%2021/%E6%AD%A3%E5%BC%8F/%E6%88%AA%E5%9B%BE%2016.jpg" alt="Build 1 的 iOS 归档日志与各构建步骤耗时" /><br />
<img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/WWDC%2021/%E6%AD%A3%E5%BC%8F/%E6%88%AA%E5%9B%BE%2017.jpg" alt="Build 1 的 iOS 归档产物列表，包含日志、安装包和 xcarchive" /><br />
在 <code class="language-plaintext highlighter-rouge">Actions</code> 一列中，点击单个 action，可以看到该次 Action 的日志和相应的产物。这样的设计使得整个团队都能很方便地获取到当前 CI 的信息。</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/WWDC%2021/%E6%AD%A3%E5%BC%8F/%E6%96%B0%E6%96%B0%E6%88%AA%E5%9B%BE%2019.jpg" alt="macOS 归档失败：UIKit 在 macOS 不可用且找不到 UIKit 模块" /><br />
喔嚯，在 Build 1 中的「 在 macOS 上归档（Archive）」这一任务失败了！是因为无法在 macOS 上使用 UIKit 框架。我们可以把鼠标移至报错处，点击 ➡️ 小箭头，会直接跳转到对应出错的代码文件。</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/WWDC%2021/%E6%AD%A3%E5%BC%8F/%E6%96%B0%E6%88%AA%E5%9B%BE%2020.jpg" alt="在 MeasurementView 中用条件编译避免 macOS 构建导入 UIKit" /><br />
根据之前的报错提示，我们可以快速定位问题。</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/WWDC%2021/%E6%AD%A3%E5%BC%8F/%E6%96%B0%E6%96%B0%E6%88%AA%E5%9B%BE%2021.jpg" alt="修复 macOS 导入错误后自动触发 Build 2，Build 1 仍显示失败" /><br />
在 Git 上推送最新的代码之后，Xcode Cloud 自动触发了新的构建——Build 2。</p>

<h2 id="xcode-cloud-中的团队协作">Xcode Cloud 中的团队协作</h2>
<p>当团队中的另一位成员打开 Xcode Cloud 时，他/她也可以看到之前构建失败的 Build 1 和构建成功的 Build 2。</p>

<p>Build 1 和 Build 2 均是名为 <code class="language-plaintext highlighter-rouge">Default</code> 的工作流的两次不同的构建。但 Default 这个名字取得不够具体，让我们在它上面点击鼠标右键并在弹出的菜单中选择 <code class="language-plaintext highlighter-rouge">Edit Workflow...</code>，来看看是如何简单地编辑一个 Xcode Cloud 工作流吧！</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/WWDC%2021/%E6%AD%A3%E5%BC%8F/%E6%96%B0%E6%88%AA%E5%9B%BE%2022.jpg" alt="在工作流 General 设置中将名称从 Default 改为 Releases" /><br />
将工作流的名称从 <code class="language-plaintext highlighter-rouge">Default</code> 修改为 <code class="language-plaintext highlighter-rouge">Releases</code> 更加符合该工作流的意图。</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/WWDC%2021/%E6%AD%A3%E5%BC%8F/%E6%96%B0%E6%88%AA%E5%9B%BE%2023.jpg" alt="Releases 工作流触发条件：监听 main 和 release/v1 分支并自动取消旧构建" /><br />
点选左侧边栏的 <code class="language-plaintext highlighter-rouge">Start Condition</code> 就可以开始编辑该工作流的触发条件。<br />
类似地，我们在 <code class="language-plaintext highlighter-rouge">Custom Branches</code> 一栏中点击 + 号，添加 <code class="language-plaintext highlighter-rouge">release/v1</code> 分支。</p>

<p><img src="https://imagebed-1301918945.cos.ap-shanghai.myqcloud.com/WWDC%2021/%E6%AD%A3%E5%BC%8F/%E6%96%B0%E6%88%AA%E5%9B%BE%2024.jpg" alt="Releases 工作流添加必过的 iOS 测试任务，覆盖 iPhone 12 与推荐 iPad" /><br />
<code class="language-plaintext highlighter-rouge">Releases</code> 工作流怎么能少了测试任务呢！点击 <code class="language-plaintext highlighter-rouge">Actions</code> 右边的小加号，并在弹出的菜单中选择 <code class="language-plaintext highlighter-rouge">Test</code> 即可添加对应的测试任务。</p>

<p>点选左侧边栏的 <code class="language-plaintext highlighter-rouge">Environment</code> 就可以开始编辑该工作流的环境配置。其中有一个可勾选的 <code class="language-plaintext highlighter-rouge">Clean</code> 选项。一旦勾选，Xcode Cloud 会在每次开始构建时清空 <code class="language-plaintext highlighter-rouge">DerivedData</code> 缓存文件夹。更多详细内容，请参考 <a href="https://developer.apple.com/videos/play/wwdc2021/10268/">WWDC21 10268 探索 Xcode Cloud 工作流</a> [06:02] 处。</p>

<p>由此可见，借助于 Apple 云基础设施的力量，整个团队的协作效能将大大提高。</p>

<h2 id="一点自己的思考">一点自己的思考</h2>
<h3 id="鱼儿游于水却困于水">鱼儿游于水，却困于水</h3>
<p>Xcode Cloud 在 Apple 生态圈中如鱼得水，然而现实是：公司中往往需要搭建一套通用 CI、CD 流程，把 iOS 与 Android 两端统一管理。在这种情况下，第一顺位可能是社区内容更完善的 <a href="https://docs.fastlane.tools/">Fastlane</a>。Fastlane 对 iOS、Android 和跨平台方案（如 Flutter）均提供不同程度的支持，也能方便地配合 GitLab CI、Jenkins 等一起协同工作。</p>

<h3 id="何何时能开始使用它呢">何…何时能开始使用它呢？</h3>
<p>WWDC21 上一公布 Xcode Cloud，我就第一时间申请 beta 版测试资格，到现在过去大半个月了，也没拥有测试资格。利用搜索引擎，我也只发现 @PastePalApp 的作者有收到测试资格，他用简单的图文描述了下 Xcode Cloud 的体验（<a href="https://twitter.com/onmyway133/status/1404856151058141185">相关链接</a>）。</p>

<p>Xcode Cloud 应该会是依托于 Apple 基础设施的云服务，回想起国区 iBooks Store、iTunes Store 的昙花一现，Xcode Cloud 会如期而至吗？</p>

<h3 id="那么古尔丹代价是什么呢">“那么，古尔丹，代价是什么呢？”</h3>
<p>你以为你的项目会像 Fruta 那样点一点就能很顺利地接入 Xcode Cloud 了吗？你错啦！<br />
接入 Xcode Cloud 的前置条件还不少：</p>
<ul>
  <li>得使用自 Xcode 10 以来的新版构建系统</li>
  <li>得使用自动代码签名</li>
  <li>得使用符合条件的源代码管理服务</li>
  <li>……</li>
</ul>

<p>更多详情，请参考 <a href="https://developer.apple.com/documentation/xcode/requirements-for-using-xcode-cloud">Xcode Cloud 的使用要求</a>。</p>]]></content><author><name>Sunset Wan</name></author><category term="WWDC21" /><summary type="html"><![CDATA[有幸参与《WWDC 21 内参》的工作，欢迎 订阅 该专栏！]]></summary></entry></feed>