<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://dovgopol.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://dovgopol.dev/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-05-31T01:25:16+10:00</updated><id>https://dovgopol.dev/feed.xml</id><title type="html">Theo Dovgopol</title><subtitle>iOS engineer writing about SwiftUI, Swift Concurrency, and building production apps.</subtitle><author><name>Theo Dovgopol</name></author><entry><title type="html">Using @Entry for Custom SwiftUI Environment Values</title><link href="https://dovgopol.dev/blog/using-entry-macro-custom-environment-values/" rel="alternate" type="text/html" title="Using @Entry for Custom SwiftUI Environment Values" /><published>2026-05-30T21:18:00+10:00</published><updated>2026-05-30T21:18:00+10:00</updated><id>https://dovgopol.dev/blog/using-entry-macro-custom-environment-values</id><content type="html" xml:base="https://dovgopol.dev/blog/using-entry-macro-custom-environment-values/"><![CDATA[<p>Custom environment values are useful when a whole part of the view tree needs access to the same app-level dependency. Navigation is a good example: a feature root can own a navigator, and child views can ask it to open another screen without passing that navigator through every initializer.</p>

<p>The annoying part has always been the setup code.</p>

<p>For a long time, this was the normal shape:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@MainActor</span>
<span class="kd">@Observable</span>
<span class="kd">final</span> <span class="kd">class</span> <span class="kt">Navigator</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">path</span><span class="p">:</span> <span class="p">[</span><span class="kt">Route</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span>

    <span class="kd">func</span> <span class="nf">openProfile</span><span class="p">(</span><span class="nv">id</span><span class="p">:</span> <span class="kt">User</span><span class="o">.</span><span class="kt">ID</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">path</span><span class="o">.</span><span class="nf">append</span><span class="p">(</span><span class="o">.</span><span class="nf">profile</span><span class="p">(</span><span class="n">id</span><span class="p">))</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">private</span> <span class="kd">struct</span> <span class="kt">NavigatorEnvironmentKey</span><span class="p">:</span> <span class="kt">EnvironmentKey</span> <span class="p">{</span>
    <span class="kd">static</span> <span class="k">let</span> <span class="nv">defaultValue</span> <span class="o">=</span> <span class="kt">Navigator</span><span class="p">()</span>
<span class="p">}</span>

<span class="kd">extension</span> <span class="kt">EnvironmentValues</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">navigator</span><span class="p">:</span> <span class="kt">Navigator</span> <span class="p">{</span>
        <span class="k">get</span> <span class="p">{</span> <span class="k">self</span><span class="p">[</span><span class="kt">NavigatorEnvironmentKey</span><span class="o">.</span><span class="k">self</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="p">[</span><span class="kt">NavigatorEnvironmentKey</span><span class="o">.</span><span class="k">self</span><span class="p">]</span> <span class="o">=</span> <span class="n">newValue</span> <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>That still works, but most of the code is just scaffolding around one stored value.</p>

<p>SwiftUI’s <code class="language-plaintext highlighter-rouge">@Entry</code> macro lets the declaration say the same thing directly:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">extension</span> <span class="kt">EnvironmentValues</span> <span class="p">{</span>
    <span class="kd">@Entry</span> <span class="k">var</span> <span class="nv">navigator</span> <span class="o">=</span> <span class="kt">Navigator</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The default keeps previews and isolated views easy to render, but the real feature still injects the navigator owned by its <code class="language-plaintext highlighter-rouge">NavigationStack</code>.</p>

<p>You read it the same way as before:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">ProductRow</span><span class="p">:</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">product</span><span class="p">:</span> <span class="kt">Product</span>

    <span class="kd">@Environment</span><span class="p">(\</span><span class="o">.</span><span class="n">navigator</span><span class="p">)</span> <span class="kd">private</span> <span class="k">var</span> <span class="nv">navigator</span>

    <span class="k">var</span> <span class="nv">body</span><span class="p">:</span> <span class="kd">some</span> <span class="kt">View</span> <span class="p">{</span>
        <span class="kt">Button</span><span class="p">(</span><span class="n">product</span><span class="o">.</span><span class="n">title</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">navigator</span><span class="o">.</span><span class="nf">openProfile</span><span class="p">(</span><span class="nv">id</span><span class="p">:</span> <span class="n">product</span><span class="o">.</span><span class="n">ownerID</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And you set it from the feature root that owns the <code class="language-plaintext highlighter-rouge">NavigationStack</code>:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">struct</span> <span class="kt">ProductsFeature</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">navigator</span> <span class="o">=</span> <span class="kt">Navigator</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="nv">path</span><span class="p">:</span> <span class="n">$navigator</span><span class="o">.</span><span class="n">path</span><span class="p">)</span> <span class="p">{</span>
            <span class="kt">ProductListView</span><span class="p">()</span>
                <span class="o">.</span><span class="nf">environment</span><span class="p">(\</span><span class="o">.</span><span class="n">navigator</span><span class="p">,</span> <span class="n">navigator</span><span class="p">)</span>
                <span class="o">.</span><span class="nf">navigationDestination</span><span class="p">(</span><span class="nv">for</span><span class="p">:</span> <span class="kt">Route</span><span class="o">.</span><span class="k">self</span><span class="p">)</span> <span class="p">{</span> <span class="n">route</span> <span class="k">in</span>
                    <span class="k">switch</span> <span class="n">route</span> <span class="p">{</span>
                    <span class="k">case</span> <span class="o">.</span><span class="nf">profile</span><span class="p">(</span><span class="k">let</span> <span class="nv">id</span><span class="p">):</span>
                        <span class="kt">ProfileView</span><span class="p">(</span><span class="nv">id</span><span class="p">:</span> <span class="n">id</span><span class="p">)</span>
                    <span class="p">}</span>
                <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The macro creates the storage key and the accessor that <code class="language-plaintext highlighter-rouge">EnvironmentValues</code> needs. The public API your views use is still the key path, <code class="language-plaintext highlighter-rouge">\.navigator</code>.</p>

<h2 id="adding-a-view-modifier">Adding a View Modifier</h2>

<p>I usually still add a small view modifier for values that are part of my app’s own UI vocabulary:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">extension</span> <span class="kt">View</span> <span class="p">{</span>
    <span class="kd">func</span> <span class="nf">navigator</span><span class="p">(</span><span class="n">_</span> <span class="nv">navigator</span><span class="p">:</span> <span class="kt">Navigator</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="nf">environment</span><span class="p">(\</span><span class="o">.</span><span class="n">navigator</span><span class="p">,</span> <span class="n">navigator</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>That gives the call site a nicer shape:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">ProductListView</span><span class="p">()</span>
    <span class="o">.</span><span class="nf">navigator</span><span class="p">(</span><span class="n">navigator</span><span class="p">)</span>
</code></pre></div></div>

<p>This is not required, but it is often worth doing for values that appear in previews, feature roots, or tests. It also mirrors SwiftUI’s own style: you normally write <code class="language-plaintext highlighter-rouge">.lineLimit(2)</code>, not <code class="language-plaintext highlighter-rouge">.environment(\.lineLimit, 2)</code>.</p>

<h2 id="why-i-prefer-entry">Why I Prefer <code class="language-plaintext highlighter-rouge">@Entry</code></h2>

<p>The main benefit is not that it saves a few lines. The benefit is that the important part becomes visible.</p>

<p>With the old version, every custom value needs a key type, a default value, a getter, and a setter. In code review, I do not want to spend attention checking whether the getter and setter point at the same key type.</p>

<p>With <code class="language-plaintext highlighter-rouge">@Entry</code>, the declaration says exactly what matters:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">extension</span> <span class="kt">EnvironmentValues</span> <span class="p">{</span>
    <span class="kd">@Entry</span> <span class="k">var</span> <span class="nv">isPaywallEnabled</span> <span class="o">=</span> <span class="kc">false</span>
    <span class="kd">@Entry</span> <span class="k">var</span> <span class="nv">analyticsClient</span><span class="p">:</span> <span class="kt">AnalyticsClient</span> <span class="o">=</span> <span class="o">.</span><span class="n">telemetry</span>
    <span class="kd">@Entry</span> <span class="k">var</span> <span class="nv">navigator</span> <span class="o">=</span> <span class="kt">Navigator</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

<p>That is easier to scan. It is also harder to accidentally copy the wrong key name from another environment value.</p>

<p>It is especially useful for app-level values that are simple by design:</p>

<ul>
  <li>feature flags used by a group of views</li>
  <li>lightweight clients or closures for actions</li>
  <li>display configuration for a component family</li>
  <li>app or feature dependencies with a sensible preview or test default</li>
</ul>

<p>The old pattern made these values feel heavier than they really were. <code class="language-plaintext highlighter-rouge">@Entry</code> puts them closer to the amount of code they deserve.</p>

<h2 id="the-trade-offs">The Trade-Offs</h2>

<p>The trade-off is that a macro hides generated code. In this case I think that is a good default, but it is still a trade-off.</p>

<p>When I see this:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@Entry</span> <span class="k">var</span> <span class="nv">navigator</span> <span class="o">=</span> <span class="kt">Navigator</span><span class="p">()</span>
</code></pre></div></div>

<p>I know SwiftUI is generating the key type for me, but I am no longer naming that key myself. For most app code, that is fine. The key type was implementation detail anyway.</p>

<p>It also means the default value is part of the declaration. That is usually pleasant, but it can make the declaration too dense if the default is expensive, needs setup, or deserves a name. In those cases I would move the default somewhere explicit:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">extension</span> <span class="kt">AnalyticsClient</span> <span class="p">{</span>
    <span class="kd">static</span> <span class="k">let</span> <span class="nv">preview</span> <span class="o">=</span> <span class="kt">AnalyticsClient</span> <span class="p">{</span> <span class="n">_</span> <span class="k">in</span> <span class="p">}</span>
<span class="p">}</span>

<span class="kd">extension</span> <span class="kt">EnvironmentValues</span> <span class="p">{</span>
    <span class="kd">@Entry</span> <span class="k">var</span> <span class="nv">analyticsClient</span><span class="p">:</span> <span class="kt">AnalyticsClient</span> <span class="o">=</span> <span class="o">.</span><span class="n">preview</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The other practical trade-off is tooling. Apple introduced <code class="language-plaintext highlighter-rouge">@Entry</code> at WWDC24 as a SwiftUI API, and it depends on the compiler understanding macros. Swift macros arrived with Swift 5.9, but the generated code can still run on older deployment targets, including iOS 13, because the macro expands at compile time into ordinary environment-key code. If a package or app still needs to build with older Xcode versions, the explicit <code class="language-plaintext highlighter-rouge">EnvironmentKey</code> version is the safer choice.</p>

<h2 id="limitations">Limitations</h2>

<p>The main limitation is simple: <code class="language-plaintext highlighter-rouge">@Entry</code> is for straightforward stored values. If the environment accessor needs custom behavior, use the explicit <code class="language-plaintext highlighter-rouge">EnvironmentKey</code> version.</p>

<p>For example, if I wanted to clamp a value before it enters the environment, I would keep the old pattern:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">struct</span> <span class="kt">RefreshIntervalKey</span><span class="p">:</span> <span class="kt">EnvironmentKey</span> <span class="p">{</span>
    <span class="kd">static</span> <span class="k">let</span> <span class="nv">defaultValue</span><span class="p">:</span> <span class="kt">TimeInterval</span> <span class="o">=</span> <span class="mi">30</span>
<span class="p">}</span>

<span class="kd">extension</span> <span class="kt">EnvironmentValues</span> <span class="p">{</span>
    <span class="k">var</span> <span class="nv">refreshInterval</span><span class="p">:</span> <span class="kt">TimeInterval</span> <span class="p">{</span>
        <span class="k">get</span> <span class="p">{</span> <span class="k">self</span><span class="p">[</span><span class="kt">RefreshIntervalKey</span><span class="o">.</span><span class="k">self</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="p">[</span><span class="kt">RefreshIntervalKey</span><span class="o">.</span><span class="k">self</span><span class="p">]</span> <span class="o">=</span> <span class="nf">max</span><span class="p">(</span><span class="mi">5</span><span class="p">,</span> <span class="n">newValue</span><span class="p">)</span> <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>That extra code is doing real work. The setter documents a rule, and the rule is enforced at the boundary where the value enters the environment.</p>

<p>The other limitation is compatibility. <code class="language-plaintext highlighter-rouge">@Entry</code> needs a SwiftUI SDK and compiler that understand the macro. If a module still has to build with older Xcode versions, the explicit key is safer.</p>

<h2 id="when-i-would-still-use-the-old-pattern">When I Would Still Use the Old Pattern</h2>

<p>I would keep the old <code class="language-plaintext highlighter-rouge">EnvironmentKey</code> style when:</p>

<ul>
  <li>the getter or setter needs custom behavior</li>
  <li>the code must build with older Xcode versions</li>
</ul>

<p>That is about it. <code class="language-plaintext highlighter-rouge">@Entry</code> should be the default for ordinary custom environment values. Reach for the longer <code class="language-plaintext highlighter-rouge">EnvironmentKey</code> version only when the accessor needs to do something special, or when the project still has to build with an older toolchain.</p>]]></content><author><name>Theo Dovgopol</name></author><category term="iOS" /><category term="SwiftUI" /><category term="Swift" /><summary type="html"><![CDATA[How SwiftUI's @Entry macro reduces custom environment value boilerplate, and when the old EnvironmentKey pattern still makes sense.]]></summary></entry></feed>