<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>swiftyplace</title>
	<atom:link href="https://www.swiftyplace.com/feed" rel="self" type="application/rss+xml" />
	<link>https://www.swiftyplace.com</link>
	<description>Learn how to build amazing apps with SwiftUI and Combine</description>
	<lastBuildDate>Wed, 01 Apr 2026 08:10:00 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://www.swiftyplace.com/wp-content/uploads/2023/08/cropped-logo-1-32x32.png</url>
	<title>swiftyplace</title>
	<link>https://www.swiftyplace.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>SwiftUI View Lifecycle: When onAppear Actually fires</title>
		<link>https://www.swiftyplace.com/blog/swiftui-view-lifecycle-onappear?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=swiftui-view-lifecycle-onappear</link>
					<comments>https://www.swiftyplace.com/blog/swiftui-view-lifecycle-onappear#respond</comments>
		
		<dc:creator><![CDATA[Karin Prater]]></dc:creator>
		<pubDate>Wed, 01 Apr 2026 08:04:28 +0000</pubDate>
				<category><![CDATA[SwiftUI]]></category>
		<guid isPermaLink="false">https://www.swiftyplace.com/?p=1005672</guid>

					<description><![CDATA[<p>In simple setups — a view behind an if condition, a sheet, a fullscreen cover — onAppear works exactly how you&#8217;d expect. Show the view, onAppear fires. Remove it, onDisappear fires. State is gone. Clean ... <a title="SwiftUI View Lifecycle: When onAppear Actually fires" class="read-more" href="https://www.swiftyplace.com/blog/swiftui-view-lifecycle-onappear" aria-label="More on SwiftUI View Lifecycle: When onAppear Actually fires">Read more</a></p>
<p>The post <a rel="nofollow" href="https://www.swiftyplace.com/blog/swiftui-view-lifecycle-onappear">SwiftUI View Lifecycle: When onAppear Actually fires</a> appeared first on <a rel="nofollow" href="https://www.swiftyplace.com">swiftyplace</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<div class="wp-block-group is-vertical is-content-justification-left is-layout-flex wp-container-core-group-is-layout-dd225191 wp-block-group-is-layout-flex">
<p>In simple setups — a view behind an <code>if</code> condition, a sheet, a fullscreen cover — <code>onAppear</code> works exactly how you&#8217;d expect. Show the view, <code>onAppear</code> fires. Remove it, <code>onDisappear</code> fires. State is gone. Clean and predictable.</p>



<p>Then you use <code>TabView</code>.</p>



<p>You put a data fetch in <code>onAppear</code> on your second tab. On iOS 17, that fetch ran at app launch — even though the user was looking at the first tab. On iOS 18, same code, the fetch didn&#8217;t run until someone actually tapped the tab. Apple made <code>TabView</code> lazy. Same code. Different OS. Different behavior.</p>



<p><code>onAppear</code> has had genuinely unpredictable moments across SwiftUI&#8217;s lifetime — firing twice, firing in unexpected order, not firing when you&#8217;d swear it should. For an API that every app relies on, that&#8217;s a problem.</p>



<p>So I spent time testing it properly, and I want to share what I found — because once you redefine one word, it all makes sense.</p>



<p>The word is <strong>&#8220;appears.&#8221;</strong></p>



<p><em>This post builds on concepts from <a href="https://www.swiftyplace.com/blog/the-attributegraph-the-engine-behind-every-swiftui-view" data-type="post" data-id="1005648">The SwiftUI AttributeGraph</a>. If terms like &#8220;node,&#8221; &#8220;identity,&#8221; or &#8220;graph&#8221; are unfamiliar, start there.</em></p>



<div style="height:0px" aria-hidden="true" class="wp-block-spacer wp-container-content-83393143"></div>
</div>



<div class="wp-block-group is-vertical is-content-justification-stretch is-layout-flex wp-container-core-group-is-layout-353c4f5a wp-block-group-is-layout-flex">
<h2 class="wp-block-heading">View Node Lifetime and Visibility</h2>



<p>If you&#8217;ve read <a href="https://www.swiftyplace.com/blog/the-attributegraph-the-engine-behind-every-swiftui-view" data-type="post" data-id="1005648">The SwiftUI AttributeGraph</a>, you know that every view has a <strong>node</strong> in the graph — a slot that holds its <code>@State</code>, tracks its dependencies, and persists as long as the view&#8217;s identity exists in the tree.</p>



<p>In order to understand when <code>onAppear</code> fires we need to look at 2 different concepts:</p>



<div style="height:0px" aria-hidden="true" class="wp-block-spacer wp-container-content-16d1eb73"></div>



<h3 class="wp-block-heading">Node Lifetime</h3>



<p>When the node is <strong>created</strong> in the attribute graph and when it&#8217;s <strong>destroyed</strong>. This happens once per identity. When the node is created, <code>@State</code> gets its initial value. When the node is destroyed, that state is torn down. Gone.</p>



<div style="height:0px" aria-hidden="true" class="wp-block-spacer wp-container-content-16d1eb73"></div>



<h3 class="wp-block-heading">View Visibility</h3>



<p>When a view becomes <strong>visible on screen</strong> and when it stops being visible. This can happen <strong>multiple times</strong> for the same node. A tab you switch away from disappears but its node can stay alive. A view inside a <code>NavigationStack</code> that gets covered by a push disappears but its node might still exist.</p>



<p><code>onAppear</code> doesn&#8217;t mean &#8220;this view was created.&#8221; It doesn&#8217;t mean &#8220;this view is new.&#8221; It means <strong>&#8220;this view just became visible.&#8221;</strong> And &#8220;visible&#8221; is doing all the heavy lifting.</p>



<p><code>@State</code> tracks <strong>lifetime</strong>. <code>onAppear</code>/<code>onDisappear</code> track <strong>visibility</strong>. They&#8217;re independent systems that sometimes align and sometimes don&#8217;t.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>&nbsp;</th><th>What it tracks</th><th>How many times</th><th>What it affects</th></tr></thead><tbody><tr><td>Node lifetime</td><td>Creation/destruction in the graph</td><td>Once</td><td>lifetime of state (declared with <code>@State</code>, <code>@StateObject</code>)</td></tr><tr><td>View visibility</td><td>Visible/not visible on screen</td><td>Multiple times</td><td><code>onAppear</code>, <code>onDisappear</code>, <code>task</code></td></tr></tbody></table></figure>



<div style="height:0px" aria-hidden="true" class="wp-block-spacer wp-container-content-28f0cd9b"></div>
</div>



<div class="wp-block-group is-vertical is-content-justification-stretch is-layout-flex wp-container-core-group-is-layout-353c4f5a wp-block-group-is-layout-flex">
<h2 class="wp-block-heading">The Simple Case: When onAppear fires with conditional views</h2>



<p>Here&#8217;s a case where lifetime and visibility happen at the same time, so everything feels intuitive:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>struct ParentView: View {
    @State var showChild = true

    var body: some View {
        VStack {
            Toggle("Show Child", isOn: $showChild)
            if showChild {
                ChildView()
            }
        }
    }
}

struct ChildView: View {
    @State var userInput = ""

    var body: some View {
        TextField("Type something", text: $userInput)
            .onAppear { print("onAppear") }
            .onDisappear { print("onDisappear") }
            .task { print("task started") }
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ParentView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@State</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> showChild </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">true</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">VStack</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">Toggle</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Show Child</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">, </span><span style="color: #97E1F1">isOn</span><span style="color: #F6F6F4">: $showChild)</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #F286C4">if</span><span style="color: #F6F6F4"> showChild {</span></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #97E1F1">ChildView</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">            }</span></span>
<span class="line"><span style="color: #F6F6F4">        }</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ChildView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@State</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> userInput </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;&quot;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">TextField</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Type something</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">, </span><span style="color: #97E1F1">text</span><span style="color: #F6F6F4">: $userInput)</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">onAppear</span><span style="color: #F6F6F4"> { </span><span style="color: #97E1F1">print</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">onAppear</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) }</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">onDisappear</span><span style="color: #F6F6F4"> { </span><span style="color: #97E1F1">print</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">onDisappear</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) }</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">task</span><span style="color: #F6F6F4"> { </span><span style="color: #97E1F1">print</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">task started</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) }</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<p>Try this:</p>



<ol class="wp-block-list">
<li>Type something into the text field</li>



<li>Toggle <code>showChild</code> to <code>false</code></li>



<li>Toggle it back to <code>true</code></li>
</ol>



<p><strong>Your text is gone.</strong></p>



<p>Here&#8217;s what happened:</p>



<p><strong>Toggle off:</strong></p>



<ul class="wp-block-list">
<li>The <code>if</code> branch removes <code>ChildView</code> from the view tree</li>



<li>The node is destroyed — <code>@State userInput</code> is torn down</li>



<li><code>onDisappear</code> fires</li>



<li><code>task</code> is cancelled</li>
</ul>



<p><strong>Toggle on:</strong></p>



<ul class="wp-block-list">
<li>A new node is created — <code>@State userInput</code> is initialized fresh as <code>""</code></li>



<li><code>onAppear</code> fires</li>



<li><code>task</code> starts</li>
</ul>



<p>One <code>onAppear</code> per lifetime. One <code>onDisappear</code> per lifetime. Lifetime and visibility are the same event. This is the case most tutorials show you, which is why you build the wrong mental model early.</p>



<div style="height:0px" aria-hidden="true" class="wp-block-spacer wp-container-content-c0740251"></div>
</div>



<div class="wp-block-group is-vertical is-content-justification-stretch is-layout-flex wp-container-core-group-is-layout-353c4f5a wp-block-group-is-layout-flex">
<h2 class="wp-block-heading">TabView: onAppear fires on every tab switch</h2>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>struct ContentView: View {
    var body: some View {
        TabView {
            Tab("First", systemImage: "1.circle") {
                FirstTab()
            }
            Tab("Second", systemImage: "2.circle") {
                SecondTab()
            }
        }
    }
}

struct FirstTab: View {
    @State var userInput = ""
    var body: some View {
        Self._printChanges()
        return TextField("Type something", text: $userInput)
            .onAppear { print("FirstTab onAppear") }
            .onDisappear { print("FirstTab onDisappear") }
    }
}

struct SecondTab: View {
    var body: some View {
        Self._printChanges()
        return Text("Second tab")
            .onAppear { print("SecondTab onAppear") }
            .onDisappear { print("SecondTab onDisappear") }
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ContentView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">TabView</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">Tab</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">First</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">, </span><span style="color: #97E1F1">systemImage</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">1.circle</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) {</span></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #97E1F1">FirstTab</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">            }</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">Tab</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Second</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">, </span><span style="color: #97E1F1">systemImage</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">2.circle</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) {</span></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #97E1F1">SecondTab</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">            }</span></span>
<span class="line"><span style="color: #F6F6F4">        }</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">FirstTab</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@State</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> userInput </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;&quot;</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #BF9EEE; font-style: italic">Self</span><span style="color: #F6F6F4">.</span><span style="color: #97E1F1">_printChanges</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">TextField</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Type something</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">, </span><span style="color: #97E1F1">text</span><span style="color: #F6F6F4">: $userInput)</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">onAppear</span><span style="color: #F6F6F4"> { </span><span style="color: #97E1F1">print</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">FirstTab onAppear</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) }</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">onDisappear</span><span style="color: #F6F6F4"> { </span><span style="color: #97E1F1">print</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">FirstTab onDisappear</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) }</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">SecondTab</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #BF9EEE; font-style: italic">Self</span><span style="color: #F6F6F4">.</span><span style="color: #97E1F1">_printChanges</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">Text</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Second tab</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">onAppear</span><span style="color: #F6F6F4"> { </span><span style="color: #97E1F1">print</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">SecondTab onAppear</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) }</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">onDisappear</span><span style="color: #F6F6F4"> { </span><span style="color: #97E1F1">print</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">SecondTab onDisappear</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) }</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<p>Run this. Before you tap anything, look at the console:</p>



<pre class="wp-block-code"><code>FirstTab: @self, @identity, _userInput changed.
FirstTab onAppear</code></pre>



<p>That&#8217;s it. Only the first tab. <code>SecondTab</code> didn&#8217;t print anything — no <code>onAppear</code>, no body call, no node created, nothing. It doesn&#8217;t exist yet. <code>TabView</code> only builds a tab when you navigate to it for the first time.</p>



<figure class="gb-block-image gb-block-image-19f990e7"><img fetchpriority="high" decoding="async" width="1546" height="956" class="gb-image gb-image-19f990e7" src="https://www.swiftyplace.com/wp-content/uploads/2026/03/tabview-first.webp" alt="" title="tabview-first" srcset="https://www.swiftyplace.com/wp-content/uploads/2026/03/tabview-first.webp 1546w, https://www.swiftyplace.com/wp-content/uploads/2026/03/tabview-first-300x186.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2026/03/tabview-first-1024x633.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2026/03/tabview-first-768x475.webp 768w, https://www.swiftyplace.com/wp-content/uploads/2026/03/tabview-first-1536x950.webp 1536w" sizes="(max-width: 1546px) 100vw, 1546px" /></figure>



<p>Now tap the second tab:</p>



<pre class="wp-block-code"><code>SecondTab: @self changed.
SecondTab onAppear
FirstTab onDisappear</code></pre>



<p><code>SecondTab</code>&#8216;s node is created now, its body is called. <code>FirstTab</code> disappears — but its node stays alive. I illustrated the visible view nodes in black and the &#8220;hidden&#8221; views in gray. I am not sure how this is exactly done in the AttributeGraph. I am guessing SwiftUI sets a flag on the nodes during <code>TabView</code> tab switching, which then triggers the<code> onAppear/onDisappear</code> calls that depend on this flag:</p>



<figure class="gb-block-image gb-block-image-d4d610a9"><img decoding="async" width="1546" height="956" class="gb-image gb-image-d4d610a9" src="https://www.swiftyplace.com/wp-content/uploads/2026/03/tabview-second.webp" alt="" title="tabview-second" srcset="https://www.swiftyplace.com/wp-content/uploads/2026/03/tabview-second.webp 1546w, https://www.swiftyplace.com/wp-content/uploads/2026/03/tabview-second-300x186.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2026/03/tabview-second-1024x633.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2026/03/tabview-second-768x475.webp 768w, https://www.swiftyplace.com/wp-content/uploads/2026/03/tabview-second-1536x950.webp 1536w" sizes="(max-width: 1546px) 100vw, 1546px" /></figure>



<p>Tap back to the first tab:</p>



<pre class="wp-block-code"><code>FirstTab onAppear
SecondTab onDisappear</code></pre>



<p><code>FirstTab onAppear</code> fires again. But here&#8217;s the thing — <strong>if you typed something in that text field earlier, it&#8217;s still there.</strong> The node was never destroyed. <code>onAppear</code> fired because the view became visible again, not because it was recreated.</p>



<figure class="gb-block-image gb-block-image-ee4a97f5"><img decoding="async" width="1546" height="956" class="gb-image gb-image-ee4a97f5" src="https://www.swiftyplace.com/wp-content/uploads/2026/03/tabview-third.webp" alt="" title="tabview-third" srcset="https://www.swiftyplace.com/wp-content/uploads/2026/03/tabview-third.webp 1546w, https://www.swiftyplace.com/wp-content/uploads/2026/03/tabview-third-300x186.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2026/03/tabview-third-1024x633.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2026/03/tabview-third-768x475.webp 768w, https://www.swiftyplace.com/wp-content/uploads/2026/03/tabview-third-1536x950.webp 1536w" sizes="(max-width: 1546px) 100vw, 1546px" /></figure>



<p>After you&#8217;ve visited both tabs, their nodes persist indefinitely. Switching tabs only triggers visibility events. Your <code>@State</code> in both tabs survives.</p>



<p>This is the split in action. <code>onDisappear</code> fired, but the state survived. The node was never destroyed — the view just went off screen. <code>onAppear</code> and <code>onDisappear</code> responded to <strong>visibility</strong>. <code>@State</code> is tied to <strong>lifetime</strong>. In <code>TabView</code>, those are completely different timelines.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><strong>iOS 17 note:</strong> In earlier versions, <code>TabView</code> built all tabs eagerly at launch. Your second tab&#8217;s <code>onAppear</code> would fire immediately — even though the user was looking at the first tab. If you had a data fetch in <code>onAppear</code> on a background tab, it ran at app launch. The lazy behavior described above is iOS 18+. Same code, different OS, different behavior. If you&#8217;re supporting iOS 17, account for both.</p>
</blockquote>



<div style="height:0px" aria-hidden="true" class="wp-block-spacer wp-container-content-6388d5dc"></div>
</div>



<div class="wp-block-group is-vertical is-content-justification-stretch is-layout-flex wp-container-core-group-is-layout-353c4f5a wp-block-group-is-layout-flex">
<h2 class="wp-block-heading">NavigationStack: onAppear fires during push/pop</h2>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>struct ContentView: View {
    var body: some View {
        NavigationStack {
            RootView()
            .navigationDestination
        }
    }
}

struct RootView: View {
    var body: some View {
        NavigationLink("Go to Detail") {
            DetailView()
        }
        .onAppear { print("RootView onAppear") }
        .onDisappear { print("RootView onDisappear") }
    }
}

struct DetailView: View {
    @State var notes = ""

    var body: some View {
        TextField("Notes", text: $notes)
            .onAppear { print("DetailView onAppear") }
            .onDisappear { print("DetailView onDisappear") }
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ContentView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">NavigationStack</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">RootView</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">            .navigationDestination</span></span>
<span class="line"><span style="color: #F6F6F4">        }</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">RootView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">NavigationLink</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Go to Detail</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) {</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">DetailView</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        }</span></span>
<span class="line"><span style="color: #F6F6F4">        .</span><span style="color: #97E1F1">onAppear</span><span style="color: #F6F6F4"> { </span><span style="color: #97E1F1">print</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">RootView onAppear</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) }</span></span>
<span class="line"><span style="color: #F6F6F4">        .</span><span style="color: #97E1F1">onDisappear</span><span style="color: #F6F6F4"> { </span><span style="color: #97E1F1">print</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">RootView onDisappear</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) }</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">DetailView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@State</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> notes </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;&quot;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">TextField</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Notes</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">, </span><span style="color: #97E1F1">text</span><span style="color: #F6F6F4">: $notes)</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">onAppear</span><span style="color: #F6F6F4"> { </span><span style="color: #97E1F1">print</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">DetailView onAppear</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) }</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">onDisappear</span><span style="color: #F6F6F4"> { </span><span style="color: #97E1F1">print</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">DetailView onDisappear</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) }</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<p>Initially, you will see the root view. The AttributeGraph builds the view hierarchy for the NavigationStack:</p>


<div class="gb-container gb-container-d35b89ca">

<figure class="gb-block-image gb-block-image-d27bd351"><img loading="lazy" decoding="async" width="984" height="956" class="gb-image gb-image-d27bd351" src="https://www.swiftyplace.com/wp-content/uploads/2026/03/navstack-root.webp" alt="" title="navstack-root" srcset="https://www.swiftyplace.com/wp-content/uploads/2026/03/navstack-root.webp 984w, https://www.swiftyplace.com/wp-content/uploads/2026/03/navstack-root-300x291.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2026/03/navstack-root-768x746.webp 768w" sizes="auto, (max-width: 984px) 100vw, 984px" /></figure>

</div>


<p><strong>Push to DetailView:</strong></p>



<ul class="wp-block-list">
<li><code>RootView</code>: <code>onDisappear</code> fires, node stays alive but becomes invisible (tagged grayed out in the AttributeGraph)</li>



<li><code>DetailView</code>: node created, <code>onAppear</code> fires</li>
</ul>


<div class="gb-container gb-container-ea1181b2">

<figure class="gb-block-image gb-block-image-1793812a"><img loading="lazy" decoding="async" width="1546" height="956" class="gb-image gb-image-1793812a" src="https://www.swiftyplace.com/wp-content/uploads/2026/03/navstack-detail.webp" alt="" title="navstack-detail" srcset="https://www.swiftyplace.com/wp-content/uploads/2026/03/navstack-detail.webp 1546w, https://www.swiftyplace.com/wp-content/uploads/2026/03/navstack-detail-300x186.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2026/03/navstack-detail-1024x633.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2026/03/navstack-detail-768x475.webp 768w, https://www.swiftyplace.com/wp-content/uploads/2026/03/navstack-detail-1536x950.webp 1536w" sizes="auto, (max-width: 1546px) 100vw, 1546px" /></figure>

</div>


<p><strong>Pop back:</strong></p>



<ul class="wp-block-list">
<li><code>DetailView</code>: <code>onDisappear</code> fires, <strong>node is destroyed</strong>, <code>@State notes</code> is gone</li>



<li><code>RootView</code>: <code>onAppear</code> fires, node was alive the whole time, state intact</li>
</ul>



<p><code>NavigationStack</code> <strong>destroys</strong> the node when you pop. <code>TabView</code> <strong>keeps</strong> it. Same <code>onDisappear</code> call, completely different lifetime behavior underneath.</p>



<p>Push to <code>DetailView</code> again? Fresh node. <code>notes</code> starts again with the inital empty String.</p>



<div style="height:0px" aria-hidden="true" class="wp-block-spacer wp-container-content-28f0cd9b"></div>
</div>



<div class="wp-block-group is-vertical is-content-justification-stretch is-layout-flex wp-container-core-group-is-layout-353c4f5a wp-block-group-is-layout-flex">
<h2 class="wp-block-heading">List and LazyVStack: onAppear fires during scrolling</h2>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>struct ContentView: View {
    var body: some View {
        List(0..&lt;1000) { index in
            RowView(index: index)
        }
    }
}

struct RowView: View {
    let index: Int
    @State var isExpanded = false

    var body: some View {
        Text("Row (index)")
            .padding(40)
            .onAppear { print("Row (index) onAppear") }
            .onDisappear { print("Row (index) onDisappear") }
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ContentView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">List</span><span style="color: #F6F6F4">(</span><span style="color: #BF9EEE">0</span><span style="color: #F286C4">..&lt;</span><span style="color: #BF9EEE">1000</span><span style="color: #F6F6F4">) { index </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">RowView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">index</span><span style="color: #F6F6F4">: index)</span></span>
<span class="line"><span style="color: #F6F6F4">        }</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">RowView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> index: </span><span style="color: #97E1F1; font-style: italic">Int</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@State</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> isExpanded </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">false</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">Text</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Row (index)</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">padding</span><span style="color: #F6F6F4">(</span><span style="color: #BF9EEE">40</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">onAppear</span><span style="color: #F6F6F4"> { </span><span style="color: #97E1F1">print</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Row (index) onAppear</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) }</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">onDisappear</span><span style="color: #F6F6F4"> { </span><span style="color: #97E1F1">print</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Row (index) onDisappear</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) }</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<p><code>List</code> is lazy. It only creates nodes for visible rows. As you scroll:</p>



<ul class="wp-block-list">
<li>Rows scrolling <strong>into view</strong>: node created (or reconnected), <code>onAppear</code> fires</li>



<li>Rows scrolling <strong>out of view</strong>: <code>onDisappear</code> fires, node <strong>may be destroyed</strong> (SwiftUI decides based on memory pressure and caching)</li>
</ul>



<p>This means <code>@State</code> inside a <code>List</code> row is <strong>unreliable</strong> for long-term storage. Scroll far enough away and come back — the node might have been destroyed and recreated. Your <code>isExpanded</code> resets to the initial value <code>false</code>.</p>



<div style="height:0px" aria-hidden="true" class="wp-block-spacer wp-container-content-c0740251"></div>
</div>



<div class="wp-block-group is-vertical is-content-justification-stretch is-layout-flex wp-container-core-group-is-layout-353c4f5a wp-block-group-is-layout-flex">
<h2 class="wp-block-heading">Where init Fits (It Doesn&#8217;t)</h2>



<p>One more thing worth clarifying: init is not a lifecycle event.</p>



<p>Your view struct&#8217;s init runs during the render pass — as part of SwiftUI evaluating the parent&#8217;s body. SwiftUI might evaluate your struct and decide nothing changed and skip rendering entirely.</p>



<p>First render pass order (during launch):</p>



<pre class="wp-block-code"><code>1. Parent body evaluates
2. Child init runs (struct created)         ← @State is populated with initial values
3. Child body evaluates                     ← @State is read HERE
4. Layout is computed
5. onAppear fires                           
6. task starts                             
7. Rendering happens      
8. NOW on screen</code></pre>



<p>later, when a state changes, these phases happen:</p>



<pre class="wp-block-code"><code>1. Parent body evaluates
2. Child init runs (struct created)         
3. Child body evaluates                     
4. Layout is computed                      
5. Rendering happens      
6. NOW on screen</code></pre>



<p>You will see that body properties are recomputed and with it init of child views are called. Init calls happen often especially during hot paths (high frequency UI updates e.g. scroll animations). Don&#8217;t fetch data in init. Don&#8217;t start work in init. It&#8217;s not a signal that anything is happening — it&#8217;s SwiftUI trying to check for UI updates when state changes.</p>



<p>And here&#8217;s a crash that comes directly from misunderstanding this order:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>struct ItemListView: View {
    @State var items: &#91;String&#93; = []

    var body: some View {
        Text(items&#91;0&#93;) // <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a5.png" alt="💥" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Index out of range
            .onAppear {
                items = &#91;"Apple", "Banana", "Cherry"&#93;
            }
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ItemListView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@State</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> items: &#91;</span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4">&#93; </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> []</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">Text</span><span style="color: #F6F6F4">(items&#91;</span><span style="color: #BF9EEE">0</span><span style="color: #F6F6F4">&#93;) </span><span style="color: #7B7F8B">// &#x1f4a5; Index out of range</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">onAppear</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">                items </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> &#91;</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Apple</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">, </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Banana</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">, </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Cherry</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">&#93;</span></span>
<span class="line"><span style="color: #F6F6F4">            }</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<p>This crashes. Look at the order above — body runs at step 3. onAppear runs at step 5. By the time onAppear populates the array, body has already tried to access items[0] on an empty array.</p>



<p>The fix is to make your body handle the empty state:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>var body: some View {
    if let first = items.first {
        Text(first)
    } else {
        ProgressView()
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">if</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> first </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> items.</span><span style="color: #97E1F1">first</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">Text</span><span style="color: #F6F6F4">(first)</span></span>
<span class="line"><span style="color: #F6F6F4">    } </span><span style="color: #F286C4">else</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">ProgressView</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<p>Your body must be valid for the initial @State value. Always. onAppear, .task, and any async work all run after body has already been evaluated. If your initial state is an empty array, your body needs to handle an empty array. There&#8217;s no way to populate data &#8220;before&#8221; body runs — that&#8217;s not how the render pass works.</p>



<p>The rule is simple: body runs before everything else. Your initial @State is all you have when body first evaluates. Design accordingly.</p>



<div style="height:0px" aria-hidden="true" class="wp-block-spacer wp-container-content-c0740251"></div>
</div>



<div class="wp-block-group is-vertical is-content-justification-stretch is-layout-flex wp-container-core-group-is-layout-353c4f5a wp-block-group-is-layout-flex">
<h2 class="wp-block-heading">What This Means for Production Code</h2>



<h3 class="wp-block-heading">Guard against multiple fetches</h3>



<p><code>onAppear</code> and <code>task</code> fires every time you switch back to a tab. Without a guard, you&#8217;re firing a network request every single time:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>// <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Fires on every tab switch
.task {
    await loadData()
}

// <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Only fetch if you don't already have data
.task {
    guard items.isEmpty else { return }
    await loadData()
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #7B7F8B">// &#x274c; Fires on every tab switch</span></span>
<span class="line"><span style="color: #F6F6F4">.</span><span style="color: #97E1F1">task</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">loadData</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #7B7F8B">// &#x2705; Only fetch if you don&#39;t already have data</span></span>
<span class="line"><span style="color: #F6F6F4">.</span><span style="color: #97E1F1">task</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">guard</span><span style="color: #F6F6F4"> items.</span><span style="color: #BF9EEE">isEmpty</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">else</span><span style="color: #F6F6F4"> { </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> }</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">loadData</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<p>Or with a loading state:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>enum LoadingState {
    case idle, loading, loaded, failed(Error)
}

@State private var state: LoadingState = .idle

.task {
    guard state == .idle else { return }
    state = .loading
    do {
        items = try await api.fetchItems()
        state = .loaded
    } catch {
        state = .failed(error)
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">enum</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">LoadingState</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> idle, loading, loaded, failed(</span><span style="color: #97E1F1; font-style: italic">Error</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">@State</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">private</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> state: LoadingState </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> .idle</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">.</span><span style="color: #97E1F1">task</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">guard</span><span style="color: #F6F6F4"> state </span><span style="color: #F286C4">==</span><span style="color: #F6F6F4"> .idle </span><span style="color: #F286C4">else</span><span style="color: #F6F6F4"> { </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> }</span></span>
<span class="line"><span style="color: #F6F6F4">    state </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> .loading</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">do</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">        items </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">try</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> api.</span><span style="color: #97E1F1">fetchItems</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        state </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> .loaded</span></span>
<span class="line"><span style="color: #F6F6F4">    } </span><span style="color: #F286C4">catch</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">        state </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> .</span><span style="color: #97E1F1">failed</span><span style="color: #F6F6F4">(error)</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<p>With this safeguard in place, when the user switchs tabs, the system doesn&#8217;t re-fetch. The task fires, hits the guard, and exits.</p>



<div style="height:0px" aria-hidden="true" class="wp-block-spacer wp-container-content-c0740251"></div>



<h3 class="wp-block-heading">Distinguish &#8220;first load&#8221; from &#8220;came back&#8221;</h3>



<p>Sometimes you want to do something every time the view appears (refresh a timestamp, check permissions) but only fetch data once:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>@State private var hasLoaded = false

.task {
    if !hasLoaded {
        await loadData()
        hasLoaded = true
    }

    // Runs every appearance
    await refreshTimestamp()
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">@State</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">private</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> hasLoaded </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">false</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">.</span><span style="color: #97E1F1">task</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">if</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">!</span><span style="color: #F6F6F4">hasLoaded {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">loadData</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        hasLoaded </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">true</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// Runs every appearance</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">refreshTimestamp</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<p><code>hasLoaded</code> is <code>@State</code> — tied to the node&#8217;s lifetime. It survives tab switches (visibility events) but resets if the node is destroyed (e.g., popped from a <code>NavigationStack</code>).</p>



<div style="height:0px" aria-hidden="true" class="wp-block-spacer wp-container-content-16d1eb73"></div>



<h3 class="wp-block-heading">Test with print statements before shipping</h3>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>.task {
    print("&#91;(Self.self)&#93; task fired")
    await loadData()
}
.onAppear { print("&#91;(Self.self)&#93; onAppear") }
.onDisappear { print("&#91;(Self.self)&#93; onDisappear") }</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F6F6F4">.</span><span style="color: #97E1F1">task</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">print</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">&#91;(Self.self)&#93; task fired</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">loadData</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span>
<span class="line"><span style="color: #F6F6F4">.</span><span style="color: #97E1F1">onAppear</span><span style="color: #F6F6F4"> { </span><span style="color: #97E1F1">print</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">&#91;(Self.self)&#93; onAppear</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) }</span></span>
<span class="line"><span style="color: #F6F6F4">.</span><span style="color: #97E1F1">onDisappear</span><span style="color: #F6F6F4"> { </span><span style="color: #97E1F1">print</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">&#91;(Self.self)&#93; onDisappear</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) }</span></span></code></pre></div>



<p>Run your app. Switch tabs. Push and pop. Scroll. Watch the console. You&#8217;ll catch double-fetches, missing cancellations, and unexpected re-fires before your users do.</p>



<div style="height:0px" aria-hidden="true" class="wp-block-spacer wp-container-content-c0740251"></div>
</div>



<div class="wp-block-group is-vertical is-content-justification-stretch is-layout-flex wp-container-core-group-is-layout-353c4f5a wp-block-group-is-layout-flex">
<h2 class="wp-block-heading">Container Cheat Sheet</h2>



<p>Because every container has different rules:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Container</th><th>Node created when&#8230;</th><th>Node destroyed when&#8230;</th><th><code>onAppear</code>/<code>onDisappear</code> fires on&#8230;</th></tr></thead><tbody><tr><td><code>if/else</code></td><td>Condition becomes true</td><td>Condition becomes false</td><td>Same as lifetime</td></tr><tr><td><code>TabView</code></td><td>Tab is first visited (lazy since iOS 18)</td><td>Rarely (app teardown)</td><td>Tab switch (visibility)</td></tr><tr><td><code>NavigationStack</code></td><td>Push</td><td>Pop</td><td>Push/pop (visibility)</td></tr><tr><td><code>List</code> / <code>LazyVStack</code></td><td>Row scrolls into view</td><td>Row scrolls far enough away (SwiftUI decides)</td><td>Scroll (visibility)</td></tr><tr><td><code>sheet</code> / <code>fullScreenCover</code></td><td>Presented</td><td>Dismissed</td><td>Same as lifetime</td></tr></tbody></table></figure>



<div style="height:0px" aria-hidden="true" class="wp-block-spacer wp-container-content-c0740251"></div>



<h2 class="wp-block-heading">The Mental Model</h2>



<p>Stop thinking about SwiftUI lifecycle as a single linear sequence. Think about it as <strong>two independent tracks</strong>:</p>



<pre class="wp-block-code"><code>Node Lifetime: ─── created ───────────────────── destroyed 
                                       │                                                                                       │
                              @State initialized                                                            @State torn down                       


Visibility:           ───  visible ─── hidden ─── visible ───────── hidden                       
                                       │                   │                     │                                         │                       
                                   onAppear      onDisappear     onAppear                           onDisappear</code></pre>



<p>In the <code>if/else</code> case, these tracks are the same length — one appear, one disappear, one lifetime. In <code>TabView</code>, the lifetime track is long and the visibility track bounces up and down within it.</p>



<p>Once you see these as separate tracks, you stop being surprised when <code>onDisappear</code> fires but your state survives, or when <code>onAppear</code> fires but your <code>@State</code> isn&#8217;t fresh.</p>



<p></p>
</div>
<p>The post <a rel="nofollow" href="https://www.swiftyplace.com/blog/swiftui-view-lifecycle-onappear">SwiftUI View Lifecycle: When onAppear Actually fires</a> appeared first on <a rel="nofollow" href="https://www.swiftyplace.com">swiftyplace</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.swiftyplace.com/blog/swiftui-view-lifecycle-onappear/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>The AttributeGraph &#8211; The Engine Behind Every SwiftUI View</title>
		<link>https://www.swiftyplace.com/blog/the-attributegraph-the-engine-behind-every-swiftui-view?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=the-attributegraph-the-engine-behind-every-swiftui-view</link>
					<comments>https://www.swiftyplace.com/blog/the-attributegraph-the-engine-behind-every-swiftui-view#respond</comments>
		
		<dc:creator><![CDATA[Karin Prater]]></dc:creator>
		<pubDate>Tue, 24 Mar 2026 14:19:05 +0000</pubDate>
				<category><![CDATA[iOS app development]]></category>
		<category><![CDATA[SwiftUI]]></category>
		<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://www.swiftyplace.com/?p=1005648</guid>

					<description><![CDATA[<p>SwiftUI has been around since 2019. Apple has given us dozens of WWDC talks about it. And yet most developers — even experienced ones — still get tripped up by the same things: These aren&#8217;t ... <a title="The AttributeGraph &#8211; The Engine Behind Every SwiftUI View" class="read-more" href="https://www.swiftyplace.com/blog/the-attributegraph-the-engine-behind-every-swiftui-view" aria-label="More on The AttributeGraph &#8211; The Engine Behind Every SwiftUI View">Read more</a></p>
<p>The post <a rel="nofollow" href="https://www.swiftyplace.com/blog/the-attributegraph-the-engine-behind-every-swiftui-view">The AttributeGraph &#8211; The Engine Behind Every SwiftUI View</a> appeared first on <a rel="nofollow" href="https://www.swiftyplace.com">swiftyplace</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<div class="wp-block-group is-vertical is-content-justification-stretch is-layout-flex wp-container-core-group-is-layout-353c4f5a wp-block-group-is-layout-flex">
<p>SwiftUI has been around since 2019. Apple has given us dozens of WWDC talks about it. And yet most developers — even experienced ones — still get tripped up by the same things:</p>



<ul class="wp-block-list">
<li>Why does my @State initializer get ignored?</li>



<li>Why does my view&#8217;s state reset when I didn&#8217;t expect it to?</li>



<li>Where is deinit? When does my data actually get cleaned up?</li>



<li>Why did my entire list re-render when I changed one item?</li>
</ul>



<p>These aren&#8217;t edge cases. They&#8217;re fundamental behaviors. And they all trace back to one thing that Apple barely talks about: the <strong>AttributeGraph</strong>.</p>



<p>The AttributeGraph is SwiftUI&#8217;s runtime engine. It&#8217;s the thing that decides when your views update, what data persists, and what gets thrown away. Every SwiftUI behavior that confuses you is the graph doing exactly what it was designed to do.</p>



<p>You don&#8217;t need to know its internal implementation. But you need to understand the <strong>model</strong> — how it thinks — because once you do, SwiftUI stops feeling like magic and starts feeling predictable.</p>


<div class="gb-container gb-container-d95462f4">

<figure class="gb-block-image gb-block-image-4605378c"><img loading="lazy" decoding="async" width="766" height="481" class="gb-image gb-image-4605378c" src="https://www.swiftyplace.com/wp-content/uploads/2026/03/data-flow-tree-6.webp" alt="" title="data-flow-tree-6" srcset="https://www.swiftyplace.com/wp-content/uploads/2026/03/data-flow-tree-6.webp 766w, https://www.swiftyplace.com/wp-content/uploads/2026/03/data-flow-tree-6-300x188.webp 300w" sizes="auto, (max-width: 766px) 100vw, 766px" />
<figcaption class="gb-headline gb-headline-09b1863c gb-headline-text">source: <a href="https://www.google.com/url?sa=t&amp;source=web&amp;rct=j&amp;opi=89978449&amp;url=https://developer.apple.com/videos/play/wwdc2021/10022/&amp;ved=2ahUKEwiPpvWV3LiTAxXLR_EDHRKxDsMQwqsBegQIFRAB&amp;usg=AOvVaw11oMNT6BVU6LcnejX9Pkpj" data-type="link" data-id="https://www.google.com/url?sa=t&amp;source=web&amp;rct=j&amp;opi=89978449&amp;url=https://developer.apple.com/videos/play/wwdc2021/10022/&amp;ved=2ahUKEwiPpvWV3LiTAxXLR_EDHRKxDsMQwqsBegQIFRAB&amp;usg=AOvVaw11oMNT6BVU6LcnejX9Pkpj" target="_blank" rel="noopener">WWDC21 Demystify SwiftUI</a></figcaption>
</figure>

</div>


<div style="height:0px" aria-hidden="true" class="wp-block-spacer wp-container-content-81dc8709"></div>
</div>



<div class="wp-block-group has-f-6-f-6-f-4-color has-text-color is-vertical is-content-justification-left is-layout-flex wp-container-core-group-is-layout-dd225191 wp-block-group-is-layout-flex">
<h2 class="gb-headline gb-headline-d5ed2bd9 gb-headline-text">What Is the AttributeGraph?</h2>



<p>Think of a spreadsheet.</p>



<ul class="wp-block-list">
<li>Cell <strong>A1</strong> = <code>5</code></li>



<li>Cell <strong>A2</strong> = <code>10</code></li>



<li>Cell <strong>A3</strong> = <code>= A1 + A2</code></li>
</ul>



<p>You change A1 to <code>7</code>. Excel doesn&#8217;t recalculate every cell in the sheet. It knows A3 depends on A1, so it recalculates <strong>only A3</strong>. A2 is untouched.</p>



<p>The AttributeGraph works the same way. It&#8217;s a directed graph made up of:</p>



<ul class="wp-block-list">
<li><strong>Nodes</strong> (called <strong>attributes</strong>) — each one stores a specific piece of information. A <code>@State</code> value. A custom view. SwiftUI views like VStack, Text and Button. </li>



<li><strong>Edges</strong> (called <strong>dependencies</strong>) — connections between nodes that say &#8220;this node reads from that node.&#8221;</li>
</ul>



<p>When a state node&#8217;s value changes, the graph walks <strong>down</strong> through the edges and marks every dependent node as <strong>dirty</strong> — meaning it needs to be recalculated. Nodes that don&#8217;t depend on the changed value are never touched.</p>



<p>That&#8217;s the entire update engine.</p>



<div style="height:0px" aria-hidden="true" class="wp-block-spacer wp-container-content-c0740251"></div>
</div>



<div class="wp-block-group has-f-6-f-6-f-4-color has-text-color is-vertical is-content-justification-stretch is-layout-flex wp-container-core-group-is-layout-353c4f5a wp-block-group-is-layout-flex">
<h2 class="gb-headline gb-headline-1e1cf9e1 gb-headline-text">Your View Struct Is Not the UI</h2>



<p>This is the most important mental shift coming from UIKit.</p>



<p>In UIKit, your <code>UIViewController</code> <strong>is</strong> the thing. It holds the data. It holds the views. It has a lifecycle. You create it, you configure it, it lives, it dies.</p>



<p>In SwiftUI, your view struct is a <strong>description</strong>. A blueprint. SwiftUI reads it, extracts the information, feeds it into the AttributeGraph, and throws the struct away.</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>struct CounterView: View {
    @State var count = 0

    var body: some View {
        VStack {
            Text("(count)")
            Button("+1") { count += 1 }
        }
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">CounterView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@State</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> count </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">0</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">VStack</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">Text</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">(count)</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">Button</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">+1</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) { count </span><span style="color: #F286C4">+=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">1</span><span style="color: #F6F6F4"> }</span></span>
<span class="line"><span style="color: #F6F6F4">        }</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<p>When SwiftUI processes this, the graph ends up looking roughly like this:</p>


<div class="gb-container gb-container-a7db327a">

<figure class="gb-block-image gb-block-image-17bcf9ab"><img loading="lazy" decoding="async" width="736" height="648" class="gb-image gb-image-17bcf9ab" src="https://www.swiftyplace.com/wp-content/uploads/2026/03/data-flow-tree-1.webp" alt="" title="data-flow-tree-1" srcset="https://www.swiftyplace.com/wp-content/uploads/2026/03/data-flow-tree-1.webp 736w, https://www.swiftyplace.com/wp-content/uploads/2026/03/data-flow-tree-1-300x264.webp 300w" sizes="auto, (max-width: 736px) 100vw, 736px" /></figure>

</div>


<p>Each box is an attribute. Each arrow is a dependency. When <code>count</code> changes:</p>



<ol class="wp-block-list">
<li>The <code>count</code> attribute is marked dirty</li>



<li>All nodes that depend on count are marked as dirty (an edge/dependency from the state to): <code>ContentView</code> is marked dirty</li>



<li><code>CounterView.body</code> is called and compared (diff) against the old value</li>



<li><code>Text</code> depends on <code>body</code>&#8216;s output → <strong>the system notes the change</strong></li>



<li><code>Button</code> — did its inputs change? No → skipped</li>



<li>During the commit phase the acutual changes are passed to the underlying UIKit components: here a UILabel get a new text. </li>
</ol>



<p>The view is used to create and update the underlying UI. </p>



<div style="height:0px" aria-hidden="true" class="wp-block-spacer wp-container-content-29cb0303"></div>
</div>



<div class="wp-block-group has-f-6-f-6-f-4-color has-text-color is-vertical is-content-justification-stretch is-layout-flex wp-container-core-group-is-layout-353c4f5a wp-block-group-is-layout-flex">
<h2 class="gb-headline gb-headline-2c3b3666 gb-headline-text">Where @State Actually Lives</h2>



<p>When you write:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>@State private var name = ""</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">@State</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">private</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> name </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;&quot;</span></span></code></pre></div>



<p><code>@State</code> is a property wrapper. Inside it, there&#8217;s a reference — Apple calls it a <code>_location</code> — that points to an attribute in the graph. The actual value lives <strong>there</strong>. I highlighted the state attribute as blue in the above infographic.</p>



<p>Here&#8217;s what happens step by step when SwiftUI encounters your view for the first time:</p>



<pre class="wp-block-code"><code>1. Parent's body runs and mentions YourView(...)

2. SwiftUI calls YourView.init()
   → A temporary struct is created on the stack
   → @State property contains State&lt;String&gt;(initialValue: "")
   → This is just a DESCRIPTION — no storage yet

3. SwiftUI checks the view's position in the tree (Structural Identity)
   → "Have I seen a view at this position before?"

4. First time → NO
   → Allocate a new attribute in the graph
   → Store the initial value ""
   → Connect the @State handle to this attribute

5. SwiftUI calls body on the struct
   → body READS name
   → Graph records a dependency: "body depends on name"

6. SwiftUI throws away the struct
   → The struct is gone
   → The attribute in the graph remains</code></pre>



<p>The struct is disposable. The state attribute in the graph is the source of truth.</p>



<div style="height:0px" aria-hidden="true" class="wp-block-spacer wp-container-content-c0740251"></div>
</div>



<div class="wp-block-group has-f-6-f-6-f-4-color has-text-color is-vertical is-content-justification-stretch is-layout-flex wp-container-core-group-is-layout-353c4f5a wp-block-group-is-layout-flex">
<h4 class="gb-headline gb-headline-51b632d5 gb-headline-text">Single Source of Truth: A Graph Topology</h4>


<div class="gb-container gb-container-">

<p>You&#8217;ve heard this phrase a hundred times. Here&#8217;s what it means in terms of the graph.</p>

</div>


<p><strong>The Problem:</strong> Two @State = Two Attributes</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);--cbp-line-highlight-color:rgba(251, 251, 239, 0.2);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>struct ParentView: View {
    @State var count = 0

    var body: some View {
        VStack {
            Button("+1") { count += 1 }
            ChildView(name: "Updated: (count)")
        }
    }
}

struct ChildView: View {
    @State var name: String

    var body: some View {
        Text(name) // Always shows "Updated: 0"
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ParentView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line cbp-line-highlight"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@State</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> count </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">0</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">VStack</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">Button</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">+1</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) { count </span><span style="color: #F286C4">+=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">1</span><span style="color: #F6F6F4"> }</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">ChildView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">name</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Updated: (count)</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">        }</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ChildView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line cbp-line-highlight"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@State</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> name: </span><span style="color: #97E1F1; font-style: italic">String</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">Text</span><span style="color: #F6F6F4">(name) </span><span style="color: #7B7F8B">// Always shows &quot;Updated: 0&quot;</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<p>This creates <strong>two attributes</strong> in the graph, which i colored in blue:</p>


<div class="gb-container gb-container-ccb95118">

<figure class="gb-block-image gb-block-image-ac6af156"><img loading="lazy" decoding="async" width="1125" height="364" class="gb-image gb-image-ac6af156" src="http://www.swiftyplace.com/wp-content/uploads/2026/03/data-flow-tree-tree.webp" alt="SwiftUI data flow with the attribute graph and state property wrapper" title="data-flow-tree-tree" srcset="https://www.swiftyplace.com/wp-content/uploads/2026/03/data-flow-tree-tree.webp 1125w, https://www.swiftyplace.com/wp-content/uploads/2026/03/data-flow-tree-tree-300x97.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2026/03/data-flow-tree-tree-1024x331.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2026/03/data-flow-tree-tree-768x248.webp 768w" sizes="auto, (max-width: 1125px) 100vw, 1125px" /></figure>

</div>


<p>The user taps the button. Only <code>ParentView.count</code> updates. <code>ChildView.name</code> is still <code>"Update 0"</code>. They have drifted apart. There is no edge connecting them for ongoing sync — the value was copied once at init and that&#8217;s it.</p>



<div style="height:0px" aria-hidden="true" class="wp-block-spacer wp-container-content-b0b89ccb"></div>



<p><strong>The Fix: </strong>One @State + @Binding = One Attribute</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);--cbp-line-highlight-color:rgba(251, 251, 239, 0.2);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>struct ParentView: View {
    @State var count = 0

    var body: some View {
        VStack {
            Button("+1") { count += 1 }
            ChildView(name: "Updated: (count)")
        }
    }
}

struct ChildView: View {
    let name: String

    var body: some View {
        Text(name) 
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ParentView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line cbp-line-highlight"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@State</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> count </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">0</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">VStack</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">Button</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">+1</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) { count </span><span style="color: #F286C4">+=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">1</span><span style="color: #F6F6F4"> }</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">ChildView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">name</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Updated: (count)</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">        }</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ChildView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line cbp-line-highlight"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> name: </span><span style="color: #97E1F1; font-style: italic">String</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">Text</span><span style="color: #F6F6F4">(name) </span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>


<div class="gb-container gb-container-043252ec">

<figure class="gb-block-image gb-block-image-7a91f25b"><img loading="lazy" decoding="async" width="1125" height="364" class="gb-image gb-image-7a91f25b" src="https://www.swiftyplace.com/wp-content/uploads/2026/03/data-flow-tree-3.webp" alt="" title="data-flow-tree-3" srcset="https://www.swiftyplace.com/wp-content/uploads/2026/03/data-flow-tree-3.webp 1125w, https://www.swiftyplace.com/wp-content/uploads/2026/03/data-flow-tree-3-300x97.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2026/03/data-flow-tree-3-1024x331.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2026/03/data-flow-tree-3-768x248.webp 768w" sizes="auto, (max-width: 1125px) 100vw, 1125px" /></figure>

</div>


<p>There is only one <code>@State</code> declaration which SwiftUI interprets and creates one attribute for in the AttriibuteGraph. This one single source of truth that both <code>ParentView</code> and <code>ChildView</code> will reflect. </p>



<div style="height:0px" aria-hidden="true" class="wp-block-spacer wp-container-content-c0740251"></div>
</div>



<div class="wp-block-group is-vertical is-content-justification-stretch is-layout-flex wp-container-core-group-is-layout-353c4f5a wp-block-group-is-layout-flex">
<h2 class="gb-headline gb-headline-ddce5427 gb-headline-text">Dependencies: How the Graph Knows What to Update</h2>



<p class="has-f-6-f-6-f-4-color has-text-color">When your view<code> </code>is first time added to the view hierarchy, the system needs to add the edges/dependencies. The are differnt ways/styles that determine the rules. </p>



<p class="has-f-6-f-6-f-4-color has-text-color">First lets look at value types with<code> @State</code> and <code>@Binding</code>:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>struct MyView: View {
    @State var name = ""       // @state create a node and the edge/dependency
    @Binding var name: String  // no edge, the updates are discovered during view tree walking 
    let age: Int               // no edge, the updates are discovered during view tree walking    

    var body: some View {
        Text(name) // body reads `name` but NOT `age`
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">MyView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@State</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> name </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;&quot;</span><span style="color: #F6F6F4">       </span><span style="color: #7B7F8B">// @state create a node and the edge/dependency</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@Binding</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> name: </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4">  </span><span style="color: #7B7F8B">// no edge, the updates are discovered during view tree walking </span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> age: </span><span style="color: #97E1F1; font-style: italic">Int</span><span style="color: #F6F6F4">               </span><span style="color: #7B7F8B">// no edge, the updates are discovered during view tree walking    </span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">Text</span><span style="color: #F6F6F4">(name) </span><span style="color: #7B7F8B">// body reads `name` but NOT `age`</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<p class="has-f-6-f-6-f-4-color has-text-color">Similarly for ObservableObject:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>struct ParentView: View {
    @StateObject var myObject = MyObject()           // @StateObject create a node and the edge/dependency
    @StateObject var otherObject = OtherObject()     // @StateObject create a node and the edge/dependency
    var body: some View {
        MyView(myObject: myObject, otherObject: otherObject)
    }
}

struct MyView: View {
    @ObservedObject var myObject: MyObject          // edge to existing node 
    var otherObject: OtherObject                    // no edge, changes to this object will not be tracked

    var body: some View {
        Text(myObject.name) 
        Text(otherObject.count)
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ParentView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@StateObject</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> myObject </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">MyObject</span><span style="color: #F6F6F4">()           </span><span style="color: #7B7F8B">// @StateObject create a node and the edge/dependency</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@StateObject</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> otherObject </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">OtherObject</span><span style="color: #F6F6F4">()     </span><span style="color: #7B7F8B">// @StateObject create a node and the edge/dependency</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">MyView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">myObject</span><span style="color: #F6F6F4">: myObject, </span><span style="color: #97E1F1">otherObject</span><span style="color: #F6F6F4">: otherObject)</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">MyView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@ObservedObject</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> myObject: MyObject          </span><span style="color: #7B7F8B">// edge to existing node </span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> otherObject: OtherObject                    </span><span style="color: #7B7F8B">// no edge, changes to this object will not be tracked</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">Text</span><span style="color: #F6F6F4">(myObject.name) </span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">Text</span><span style="color: #F6F6F4">(otherObject.</span><span style="color: #BF9EEE">count</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<p>@StateObject tells the graph: <strong>&#8220;Allocate a persistent attribute for this object and keep it alive as long as this view&#8217;s identity exists.&#8221;</strong> Same rules as <code>@State</code> — created once, survives struct recreation, destroyed when identity is removed.</p>



<p>SwiftUI sees the property wrappers and creates the edges in the AttributeGraph, which basically says &#8220;when any property in the ObservableObject changes, check these views for updates&#8221;. Note that without these property wrappers the graph has no pointer/edge and does not know to update this view. </p>



<p>For Observable feature the rules are different. The edges are created towards the single properties in the Obervable and are set if they are accessed in the view:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>struct ParentView: View {
    @State var myObject = MyObject()    // @StateObject create a node 
    var body: some View {
        MyView(myObject: myObject)     // no edge for dependency tracking,
                                       // because the body does not access any property
    }
}

struct MyView: View {
    var myObject: MyObject

    var body: some View {
        Text(myObject.name)    // read access -> edge to node "myObject.name" 
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ParentView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@State</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> myObject </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">MyObject</span><span style="color: #F6F6F4">()    </span><span style="color: #7B7F8B">// @StateObject create a node </span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">MyView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">myObject</span><span style="color: #F6F6F4">: myObject)     </span><span style="color: #7B7F8B">// no edge for dependency tracking,</span></span>
<span class="line"><span style="color: #F6F6F4">                                       </span><span style="color: #7B7F8B">// because the body does not access any property</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">MyView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> myObject: MyObject</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">Text</span><span style="color: #F6F6F4">(myObject.name)    </span><span style="color: #7B7F8B">// read access -&gt; edge to node &quot;myObject.name&quot; </span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<p>When using the new SwiftUI instrument feature, you will see the dependencies from the AttributeGraph. I gives you the `transactions` a list of state changes that is processed in batch per update frame. Then in blue the @State attributes and the edge/dependencies that are used from the AttributedGraph. It gives a lot of details like what views are further evaluatd and what SwiftUI views are updated e.g. Text views content. It basically shows you a part of the AttributeGraph and what state caused the upate and what views depend on it:</p>


<div class="gb-container gb-container-f2d6ddc6">

<figure class="gb-block-image gb-block-image-9d713ee4"><img loading="lazy" decoding="async" width="1064" height="462" class="gb-image gb-image-9d713ee4" src="http://www.swiftyplace.com/wp-content/uploads/2026/03/data-flow-tree-4.webp" alt="SwiftUI cause and effects graph shows data dependencies" title="data-flow-tree-4" srcset="https://www.swiftyplace.com/wp-content/uploads/2026/03/data-flow-tree-4.webp 1064w, https://www.swiftyplace.com/wp-content/uploads/2026/03/data-flow-tree-4-300x130.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2026/03/data-flow-tree-4-1024x445.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2026/03/data-flow-tree-4-768x333.webp 768w" sizes="auto, (max-width: 1064px) 100vw, 1064px" /></figure>

</div>


<p>In this example, I am not only updating the view that directly depends on the state which is SubView, but also other views that recieve data from there parent views. SwiftUI is bacially &#8220;walking the tree&#8221; from top to botttom, which is also why the data flow is top to bottom from parent to child. But that is a different blog post on its own (when body is called, diff, and inits).</p>



<div style="height:0px" aria-hidden="true" class="wp-block-spacer wp-container-content-c0740251"></div>
</div>



<div class="wp-block-group is-vertical is-content-justification-stretch is-layout-flex wp-container-core-group-is-layout-353c4f5a wp-block-group-is-layout-flex">
<h2 class="wp-block-heading has-f-6-f-6-f-4-color has-text-color">When Does State Die?</h2>



<p class="has-f-6-f-6-f-4-color has-text-color">In UIKit, you know exactly when data dies — <code>deinit</code>. You see it. You control it.</p>



<p class="has-f-6-f-6-f-4-color has-text-color">In SwiftUI there&#8217;s no <code>deinit</code> on your view struct (it&#8217;s a value type — it just pops off the stack). So when does the graph clean up?</p>



<p class="has-f-6-f-6-f-4-color has-text-color"><strong>When the view&#8217;s identity is removed from the tree.</strong></p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);--cbp-line-highlight-color:rgba(251, 251, 239, 0.2);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>struct ParentView: View {
    @State var showChild = true

    var body: some View {
        VStack {
            Toggle("Show", isOn: $showChild)
            if showChild {
                ChildView()
            }
        }
    }
}

struct ChildView: View {
    @State var count = 0

    var body: some View {
        Button("Count: (count)") { count += 1 }
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ParentView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@State</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> showChild </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">true</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">VStack</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">Toggle</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Show</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">, </span><span style="color: #97E1F1">isOn</span><span style="color: #F6F6F4">: $showChild)</span></span>
<span class="line cbp-line-highlight"><span style="color: #F6F6F4">            </span><span style="color: #F286C4">if</span><span style="color: #F6F6F4"> showChild {</span></span>
<span class="line cbp-line-highlight"><span style="color: #F6F6F4">                </span><span style="color: #97E1F1">ChildView</span><span style="color: #F6F6F4">()</span></span>
<span class="line cbp-line-highlight"><span style="color: #F6F6F4">            }</span></span>
<span class="line"><span style="color: #F6F6F4">        }</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ChildView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@State</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> count </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">0</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">Button</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Count: (count)</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) { count </span><span style="color: #F286C4">+=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">1</span><span style="color: #F6F6F4"> }</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>


<div class="gb-container gb-container-7d1275fd">

<figure class="gb-block-image gb-block-image-9fae5fad"><img loading="lazy" decoding="async" width="1017" height="328" class="gb-image gb-image-9fae5fad" src="https://www.swiftyplace.com/wp-content/uploads/2026/03/data-flow-tree-5.webp" alt="" title="data-flow-tree-5" srcset="https://www.swiftyplace.com/wp-content/uploads/2026/03/data-flow-tree-5.webp 1017w, https://www.swiftyplace.com/wp-content/uploads/2026/03/data-flow-tree-5-300x97.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2026/03/data-flow-tree-5-768x248.webp 768w" sizes="auto, (max-width: 1017px) 100vw, 1017px" /></figure>

</div>


<ol class="wp-block-list has-f-6-f-6-f-4-color has-text-color">
<li><code>showChild</code> is <code>true</code>. <code>ChildView</code> appears. The graph creates an attribute for <code>count</code> with value <code>0</code>.</li>



<li>You tap the button a few times. <code>count</code> is now <code>5</code>.</li>



<li>Toggle <code>showChild</code> to <code>false</code>. The <code>if</code> branch removes <code>ChildView</code> from the tree.</li>



<li>SwiftUI sees that <code>ChildView</code>&#8216;s identity is gone → <strong>destroys all its attributes</strong>. <code>count</code> is gone.</li>



<li>Toggle <code>showChild</code> back to <code>true</code>. <code>ChildView</code> appears again. The graph creates a <strong>new</strong> attribute for <code>count</code> with value <code>0</code>.</li>
</ol>



<p class="has-f-6-f-6-f-4-color has-text-color">The old <code>count = 5</code> is gone forever. This is your <code>deinit</code> — not on the struct, but on the <strong>identity in the graph</strong>.</p>



<div style="height:0px" aria-hidden="true" class="wp-block-spacer wp-container-content-16d1eb73"></div>
</div>



<div class="wp-block-group has-f-6-f-6-f-4-color has-text-color is-vertical is-content-justification-stretch is-layout-flex wp-container-core-group-is-layout-353c4f5a wp-block-group-is-layout-flex">
<h2 class="gb-headline gb-headline-7e18e1fc gb-headline-text">Identity: How the Graph Knows &#8220;Which&#8221; View You Mean</h2>



<p>The graph needs a way to match your view struct to the right attribute. It does this through <strong>Identity</strong>.</p>



<p>There are two kinds:</p>



<h4 class="wp-block-heading">Structural Identity</h4>



<p>This is the default. SwiftUI uses your view&#8217;s <strong>position in the code</strong> to identify it.</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>var body: some View {
    VStack {
        Text("Hello")   // ← identity: VStack/child-0
        Text("World")   // ← identity: VStack/child-1
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">VStack</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">Text</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Hello</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)   </span><span style="color: #7B7F8B">// ← identity: VStack/child-0</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">Text</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">World</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)   </span><span style="color: #7B7F8B">// ← identity: VStack/child-1</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<p>Each <code>Text</code> has a different identity because it&#8217;s at a different position in the <code>VStack</code>. SwiftUI uses this position as the key to look up attributes in the graph.</p>



<p>This is also why <code>if/else</code> branches create different identities:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>var body: some View {
    if isLoggedIn {
        HomeView()      // ← identity: if-true-branch
    } else {
        LoginView()     // ← identity: if-false-branch
    }
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">if</span><span style="color: #F6F6F4"> isLoggedIn {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">HomeView</span><span style="color: #F6F6F4">()      </span><span style="color: #7B7F8B">// ← identity: if-true-branch</span></span>
<span class="line"><span style="color: #F6F6F4">    } </span><span style="color: #F286C4">else</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">LoginView</span><span style="color: #F6F6F4">()     </span><span style="color: #7B7F8B">// ← identity: if-false-branch</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<p><code>HomeView</code> and <code>LoginView</code> have completely different identities. They have completely different attributes in the graph. When <code>isLoggedIn</code> flips from <code>false</code> to <code>true</code>, the graph <strong>destroys</strong> all attributes for <code>LoginView</code> and <strong>creates</strong> new ones for <code>HomeView</code>.</p>



<div style="height:0px" aria-hidden="true" class="wp-block-spacer wp-container-content-16d1eb73"></div>



<h4 class="wp-block-heading">Explicit Identity</h4>



<p>You provide this with <code>.id()</code> or <code>ForEach</code>.</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>ForEach(items, id: \.id) { item in
    RowView(item: item)
}</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #97E1F1">ForEach</span><span style="color: #F6F6F4">(items, </span><span style="color: #97E1F1">id</span><span style="color: #F6F6F4">: \.id) { item </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">RowView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">item</span><span style="color: #F6F6F4">: item)</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<p>Each <code>RowView</code> is identified by <code>item.id</code>. If you remove an item from the array, the graph destroys the attributes for that identity. If you add a new item, new attributes are created.</p>



<p>And here&#8217;s the power move:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>ChildView()
    .id(someValue)</textarea></pre><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #97E1F1">ChildView</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">    .</span><span style="color: #97E1F1">id</span><span style="color: #F6F6F4">(someValue)</span></span></code></pre></div>



<p>When <code>someValue</code> changes, SwiftUI treats this as a <strong>completely new view</strong>. The old attributes are destroyed. New ones are created with fresh initial values. This is your manual &#8220;reset state&#8221; button.</p>



<div style="height:0px" aria-hidden="true" class="wp-block-spacer wp-container-content-16d1eb73"></div>
</div>



<h2 class="gb-headline gb-headline-bb406d12 gb-headline-text">The Update Cycle: Putting It All Together</h2>



<p class="has-f-6-f-6-f-4-color has-text-color">Here&#8217;s the full picture of what happens when you change a <code>@State</code> value:</p>



<pre class="wp-block-code has-f-6-f-6-f-4-color has-text-color"><code>1. You set count = 5
   → The @State handle writes to its attribute in the graph

2. The attribute marks itself as DIRTY

3. The graph walks DOWN through dependency edges
   → Every attribute that depends on `count` is marked dirty

4. SwiftUI schedules a re-evaluation (batched — not immediate)

5. On the next render pass, SwiftUI visits each dirty attribute
   → Calls body on views whose body attribute is dirty (starts in the highest node in the attribute graph, closest to @main)
   → body returns new view descriptions
   → diffing: compares old and new values to decide whats changed
   → markes custom views as invalide if there input changed
   → continuesly calls  body for invalidated subviews

6. Commit phase: changes are passed to the underlying rendering engine

7. Next frame: UI shows new display for updated state</code></pre>



<p class="has-f-6-f-6-f-4-color has-text-color">This is why SwiftUI is fast. It doesn&#8217;t re-evaluate your entire view tree. It follows the dependency edges from the changed node downward and only touches what&#8217;s necessary. When it calls a body, it means it checks if it needs updating and only when it finds changes these are actually implemented. The whole system is very complex and Apple engineers have been working constantly on improving the reliability and performance of the system. </p>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading has-f-6-f-6-f-4-color has-text-color">The UIKit <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2194.png" alt="↔" class="wp-smiley" style="height: 1em; max-height: 1em;" /> SwiftUI Translation Table</h2>



<figure class="wp-block-table"><table class="has-f-6-f-6-f-4-color has-text-color has-fixed-layout"><thead><tr><th class="has-text-align-left" data-align="left">Concept</th><th class="has-text-align-left" data-align="left">UIKit</th><th class="has-text-align-left" data-align="left">SwiftUI</th></tr></thead><tbody><tr><td class="has-text-align-left" data-align="left">Where data lives</td><td class="has-text-align-left" data-align="left">In your objects</td><td class="has-text-align-left" data-align="left">In the AttributeGraph</td></tr><tr><td class="has-text-align-left" data-align="left">Who creates UI</td><td class="has-text-align-left" data-align="left">You (<code>addSubview</code>, <code>NSLayoutConstraint</code>)</td><td class="has-text-align-left" data-align="left">The graph (from your <code>body</code> description)</td></tr><tr><td class="has-text-align-left" data-align="left">Who updates UI</td><td class="has-text-align-left" data-align="left">You (<code>label.text = ...</code>)</td><td class="has-text-align-left" data-align="left">The graph (dependency tracking)</td></tr><tr><td class="has-text-align-left" data-align="left">When data is created</td><td class="has-text-align-left" data-align="left"><code>init</code> / <code>viewDidLoad</code></td><td class="has-text-align-left" data-align="left">First time identity appears in tree</td></tr><tr><td class="has-text-align-left" data-align="left">When data dies</td><td class="has-text-align-left" data-align="left"><code>deinit</code></td><td class="has-text-align-left" data-align="left">Identity removed from tree</td></tr><tr><td class="has-text-align-left" data-align="left">How updates propagate</td><td class="has-text-align-left" data-align="left">You call methods manually</td><td class="has-text-align-left" data-align="left">Graph walks dependency edges automatically</td></tr><tr><td class="has-text-align-left" data-align="left">What your code is</td><td class="has-text-align-left" data-align="left">The engine</td><td class="has-text-align-left" data-align="left">A blueprint</td></tr></tbody></table></figure>



<div style="height:24px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading has-f-6-f-6-f-4-color has-text-color">Practical Implications</h2>



<p class="has-f-6-f-6-f-4-color has-text-color">Everything above leads to a set of rules that aren&#8217;t suggestions — they&#8217;re consequences of how the graph works:</p>



<ol class="wp-block-list has-f-6-f-6-f-4-color has-text-color">
<li><p><strong>Don&#8217;t use <code>@State</code> for data passed from a parent.</strong> The graph allocates the attribute once and ignores future init values. Use <code>@Binding</code> or just a <code>let</code> constant.</p></li>



<li><p><strong>Don&#8217;t sync two <code>@State</code> properties manually.</strong> Two <code>@State</code> = two attributes. Use one <code>@State</code> and pass <code>@Binding</code> references down.</p></li>



<li><p><strong>Put your source of truth as low as possible, but high enough so that all views can access it.</strong> When an attribute changes, every dependent below it might be re-evaluated. If your <code>@State</code> is at the root, a change can cause many updated down the entire tree.</p></li>



<li><p><strong><code>@StateObject</code> for objects you create. <code>@ObservedObject</code> for objects you receive.</strong> This maps directly to whether the graph allocates storage or just watches.</p></li>



<li><p><strong><code>init()</code> is not your setup point.</strong> It runs every time the parent re-evaluates. The graph decides whether to use your initial values or ignore them.</p></li>
</ol>



<p>Your view struct is the top layer. The graph is the middle layer. The screen is the output. You write the top layer. The graph does the rest.</p>



<p>When something doesn&#8217;t behave the way you expect, the answer is almost always in the middle layer: which attribute exists, what it&#8217;s connected to, and whether its identity is still in the graph.</p>



<div style="height:41px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading has-f-6-f-6-f-4-color has-text-color">Further Reading</h2>



<ul class="wp-block-list has-f-6-f-6-f-4-color has-text-color">
<li><a href="https://developer.apple.com/documentation/swiftui/managing-user-interface-state" target="_blank" rel="noopener">Managing User Interface State — Apple Documentation</a></li>



<li><a href="https://developer.apple.com/videos/play/wwdc2021/10022/" target="_blank" rel="noopener">Demystify SwiftUI — WWDC21</a></li>



<li><a href="https://developer.apple.com/videos/play/wwdc2020/10040/" target="_blank" rel="noopener">Data Essentials in SwiftUI — WWDC20</a></li>
</ul>
<p>The post <a rel="nofollow" href="https://www.swiftyplace.com/blog/the-attributegraph-the-engine-behind-every-swiftui-view">The AttributeGraph &#8211; The Engine Behind Every SwiftUI View</a> appeared first on <a rel="nofollow" href="https://www.swiftyplace.com">swiftyplace</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.swiftyplace.com/blog/the-attributegraph-the-engine-behind-every-swiftui-view/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Debugging Swift Concurrency: &#8220;Am I on the Main Actor?&#8221; (Not the Main Thread)</title>
		<link>https://www.swiftyplace.com/blog/debugging-swift-concurrency?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=debugging-swift-concurrency</link>
					<comments>https://www.swiftyplace.com/blog/debugging-swift-concurrency#respond</comments>
		
		<dc:creator><![CDATA[Karin Prater]]></dc:creator>
		<pubDate>Tue, 02 Sep 2025 07:47:05 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://www.swiftyplace.com/?p=1005631</guid>

					<description><![CDATA[<p>When working with async Swift code in&#160;Swift 6, you&#8217;ll eventually hit this classic question: “Is this code running on the main thread?” You might try the old line: &#8230;and get hit with this Swift 6 ... <a title="Debugging Swift Concurrency: &#8220;Am I on the Main Actor?&#8221; (Not the Main Thread)" class="read-more" href="https://www.swiftyplace.com/blog/debugging-swift-concurrency" aria-label="More on Debugging Swift Concurrency: &#8220;Am I on the Main Actor?&#8221; (Not the Main Thread)">Read more</a></p>
<p>The post <a rel="nofollow" href="https://www.swiftyplace.com/blog/debugging-swift-concurrency">Debugging Swift Concurrency: &#8220;Am I on the Main Actor?&#8221; (Not the Main Thread)</a> appeared first on <a rel="nofollow" href="https://www.swiftyplace.com">swiftyplace</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>When working with async Swift code in&nbsp;<strong>Swift 6</strong>, you&#8217;ll eventually hit this classic question:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><em>“Is this code running on the main thread?”</em></p>
</blockquote>



<p>You might try the old line:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="print(Thread.isMainThread ? &quot;is on Main Thread&quot; : &quot;not on Main Thread&quot;)" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #97E1F1">print</span><span style="color: #F6F6F4">(Thread.isMainThread </span><span style="color: #F286C4">?</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">is on Main Thread</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">not on Main Thread</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span></code></pre></div>



<div style="height:24px" aria-hidden="true" class="wp-block-spacer"></div>



<p>&#8230;and get hit with this Swift 6 compiler error:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p> <strong>&#8216;Thread.isMainThread&#8217; is unavailable from asynchronous contexts</strong><br><em>&#8220;Work intended for the main actor should be marked with @MainActor; this is an error in the Swift 6 language mode.&#8221;</em></p>
</blockquote>



<p>So what now?</p>



<div style="height:45px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="gb-headline gb-headline-1e1570dc gb-headline-text">Stop thinking in threads. Start thinking in actors.</h2>



<p>In Swift Concurrency, the real question isn&#8217;t “which thread?”, it&#8217;s:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><strong>“Which actor am I on?”</strong></p>
</blockquote>



<ul class="wp-block-list">
<li><strong>UI work</strong> must happen on the <code>MainActor</code></li>



<li><strong>Heavy work</strong> should run off the <code>MainActor</code></li>



<li><strong>Actor isolation</strong> is what protects your code — not threads.</li>
</ul>



<p>Actors manage&nbsp;<em>serial execution</em>&nbsp;and data safety. Threads are implementation details.</p>



<div style="height:100px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="gb-headline gb-headline-942abcfd gb-headline-text">Use MainActor.assertIsolated() to debug actor isolation</h2>



<p>During development, you can assert that you&#8217;re on the <code>MainActor</code> like this:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="func updateUI() {
    MainActor.assertIsolated(&quot;UI updates must happen on MainActor!&quot;)
    self.title = &quot;Loaded&quot;
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">updateUI</span><span style="color: #F6F6F4">() {</span></span>
<span class="line"><span style="color: #F6F6F4">    MainActor.</span><span style="color: #97E1F1">assertIsolated</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">UI updates must happen on MainActor!</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #BF9EEE; font-style: italic">self</span><span style="color: #F6F6F4">.title </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Loaded</span><span style="color: #DEE492">&quot;</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:32px" aria-hidden="true" class="wp-block-spacer"></div>



<p>If you ever call this function from the wrong actor, it stops you <em>immediately</em> during development.</p>



<p>What it does:</p>



<ul class="wp-block-list">
<li><strong>In Debug builds</strong> (Xcode default): Crashes immediately if you&#8217;re <em>not</em> on the <code>MainActor</code></li>



<li><strong>In Release builds</strong>: Does nothing (zero cost)</li>
</ul>



<p>Want a hard crash in all builds? Use <code>preconditionIsolated</code></p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="MainActor.preconditionIsolated(&quot;UI must be touched on MainActor&quot;)" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F6F6F4">MainActor.</span><span style="color: #97E1F1">preconditionIsolated</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">UI must be touched on MainActor</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span></code></pre></div>



<div style="height:32px" aria-hidden="true" class="wp-block-spacer"></div>



<p>This crashes <strong>in Debug and Release</strong>. Use this when calling from the wrong actor would be a <em>logic error</em> in production.</p>



<div style="height:100px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="gb-headline gb-headline-033edc76 gb-headline-text">What does the crash actually look like?</h2>



<p>When the assertion fails, your app will just trap:</p>



<figure class="gb-block-image gb-block-image-84990e17"><img loading="lazy" decoding="async" width="1373" height="552" class="gb-image gb-image-84990e17" src="https://www.swiftyplace.com/wp-content/uploads/2025/09/concurrency_debugging.webp" alt="" title="concurrency_debugging" srcset="https://www.swiftyplace.com/wp-content/uploads/2025/09/concurrency_debugging.webp 1373w, https://www.swiftyplace.com/wp-content/uploads/2025/09/concurrency_debugging-300x121.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2025/09/concurrency_debugging-1024x412.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2025/09/concurrency_debugging-768x309.webp 768w" sizes="auto, (max-width: 1373px) 100vw, 1373px" /></figure>



<div style="height:32px" aria-hidden="true" class="wp-block-spacer"></div>



<p>In the Xcode debugger navigator, you’ll see:</p>



<pre class="wp-block-code"><code>Task 1 Queue : com.apple.root.user-initiated-qos.cooperative (concurrent)

9 ContentView.fetchAndDecode(query:)
10 ContentView.load()
</code></pre>



<div style="height:32px" aria-hidden="true" class="wp-block-spacer"></div>



<p>This tells you:</p>



<ul class="wp-block-list">
<li> <code>Queue ... (concurrent)</code> You are <strong>not on the MainActor</strong></li>



<li>You are running on a <strong>background actor</strong> with <code>.userInitiated</code> QoS</li>



<li>This task is off the main thread and outside the UI actor</li>
</ul>



<p>If you&#8217;re <strong>on</strong> the MainActor:</p>



<pre class="wp-block-code"><code>Queue: com.apple.main-thread (serial)</code></pre>



<div style="height:32px" aria-hidden="true" class="wp-block-spacer"></div>



<p>That queue name is your <em>runtime hint</em> about actor context.</p>



<div style="height:15px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">What about&nbsp;<strong>custom global actors</strong>?</h2>



<p>You can create your own actor — and use the same assertion methods:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="@globalActor
actor ImageCacheActor {
    static let shared = ImageCacheActor()
}

@ImageCacheActor
func mutateCache() {
    ImageCacheActor.assertIsolated(&quot;must be on ImageCacheActor&quot;)
    // safe to touch cache here
}
" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">@globalActor</span></span>
<span class="line"><span style="color: #F286C4">actor</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ImageCacheActor</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">static</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> shared </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">ImageCacheActor</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">@ImageCacheActor</span></span>
<span class="line"><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">mutateCache</span><span style="color: #F6F6F4">() {</span></span>
<span class="line"><span style="color: #F6F6F4">    ImageCacheActor.</span><span style="color: #97E1F1">assertIsolated</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">must be on ImageCacheActor</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// safe to touch cache here</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span>
<span class="line"></span></code></pre></div>



<div style="height:28px" aria-hidden="true" class="wp-block-spacer"></div>



<p>It works just like <code>MainActor.assertIsolated</code>.</p>



<p>There’s no <code>assertNotMainActor</code>, so if you <em>want</em> to make sure you&#8217;re not on main, you can drop in a <code>MainActor.assertIsolated("should NOT be main")</code> as a temp check and see if it crashes — if it doesn&#8217;t, then you&#8217;re on main and now you know. You can also add a breakpoint and read the Task value from the debug navigator area.</p>



<div style="height:45px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="gb-headline gb-headline-b27601e4 gb-headline-text">Summary</h2>



<p>Swift 6 enforces proper actor usage. Take it seriously — it saves you from race conditions and UI bugs. Forget <code>Thread.isMainThread</code>. Ask instead:<strong>&#8220;Am I on the actor I expect to be on?&#8221;</strong></p>



<p>During development:</p>



<ul class="wp-block-list">
<li>Use <code>assertIsolated</code> to catch mistakes early</li>



<li>Use Xcode&#8217;s <strong>Queue</strong> info to understand runtime actor context</li>



<li>Use custom logs to visualize where hops are happening</li>
</ul>



<p>Stay actor-aware, and let the compiler + runtime do the heavy lifting. <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4aa.png" alt="💪" class="wp-smiley" style="height: 1em; max-height: 1em;" /><br>You&#8217;re not just managing threads anymore — you&#8217;re designing concurrency.<br></p>
<p>The post <a rel="nofollow" href="https://www.swiftyplace.com/blog/debugging-swift-concurrency">Debugging Swift Concurrency: &#8220;Am I on the Main Actor?&#8221; (Not the Main Thread)</a> appeared first on <a rel="nofollow" href="https://www.swiftyplace.com">swiftyplace</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.swiftyplace.com/blog/debugging-swift-concurrency/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Building an AI Chatbot in SwiftUI with Foundation Models Framework</title>
		<link>https://www.swiftyplace.com/blog/foundation-models-framework?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=foundation-models-framework</link>
					<comments>https://www.swiftyplace.com/blog/foundation-models-framework#respond</comments>
		
		<dc:creator><![CDATA[Karin Prater]]></dc:creator>
		<pubDate>Sat, 26 Jul 2025 15:41:34 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<category><![CDATA[foundation models framework]]></category>
		<guid isPermaLink="false">https://www.swiftyplace.com/?p=1005605</guid>

					<description><![CDATA[<p>Apple just changed the game at WWDC 2025 with the Foundation Models framework. For the first time, you can now run the exact same AI model that powers Apple Intelligence directly inside your own iOS ... <a title="Building an AI Chatbot in SwiftUI with Foundation Models Framework" class="read-more" href="https://www.swiftyplace.com/blog/foundation-models-framework" aria-label="More on Building an AI Chatbot in SwiftUI with Foundation Models Framework">Read more</a></p>
<p>The post <a rel="nofollow" href="https://www.swiftyplace.com/blog/foundation-models-framework">Building an AI Chatbot in SwiftUI with Foundation Models Framework</a> appeared first on <a rel="nofollow" href="https://www.swiftyplace.com">swiftyplace</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Apple just changed the game at WWDC 2025 with the Foundation Models framework. For the first time, you can now run the exact same AI model that powers Apple Intelligence directly inside your own iOS apps. No internet connection needed, no OpenAI API bills, just pure on-device artificial intelligence.</p>



<p>Because I really want to know how capable the local llm is, I build a more complex app. This isn&#8217;t just another demo project. We&#8217;re building a real-world AI chatbot that can handle complex conversations, remember context, and even enhance its knowledge with custom data when needed.</p>



<p><a href="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqbXhIOElMNXhydmlxN3VCdzdyNUJrRTI3ejNpUXxBQ3Jtc0tubWxHQ29UZjBQVi1oTFJqMVVRbzQ2cnNCUHlQNEt6ZzJiN3k3Ym1mV29OYUR4OU9UVlNOeFlsUGVuamVVNFhoYnJzVHFZS3JYNHNaV0N6a2tsUURQdTlhejlFY3Y1LWd1ZVQ0NTBPM290MldIemRUYw&amp;q=https%3A%2F%2Fschool.swiftyplace.com%2Ff%2Fproject-files-foundation-models-framework&amp;v=wl0vZrQ5J9Q" data-type="link" data-id="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqbXhIOElMNXhydmlxN3VCdzdyNUJrRTI3ejNpUXxBQ3Jtc0tubWxHQ29UZjBQVi1oTFJqMVVRbzQ2cnNCUHlQNEt6ZzJiN3k3Ym1mV29OYUR4OU9UVlNOeFlsUGVuamVVNFhoYnJzVHFZS3JYNHNaV0N6a2tsUURQdTlhejlFY3Y1LWd1ZVQ0NTBPM290MldIemRUYw&amp;q=https%3A%2F%2Fschool.swiftyplace.com%2Ff%2Fproject-files-foundation-models-framework&amp;v=wl0vZrQ5J9Q" target="_blank" rel="noopener"><strong><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2b07.png" alt="⬇" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Download project files</strong></a></p>



<figure class="wp-block-embed is-type-video is-provider-youtube wp-block-embed-youtube wp-embed-aspect-16-9 wp-has-aspect-ratio"><div class="wp-block-embed__wrapper">
<iframe loading="lazy" title="Build Your First AI Chatbot App with SwiftUI + Foundation Models Framework PART 1 | iOS26 | WWDC25" width="1400" height="788" src="https://www.youtube.com/embed/wl0vZrQ5J9Q?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
</div></figure>



<div style="height:38px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">Building a Real-World AI Chat App</h2>



<p>Our demo app is a &#8220;Dog Helper&#8221; that showcases practical AI implementation. Users can:</p>



<ol class="wp-block-list">
<li><strong>Ask preset questions</strong> like &#8220;Tell me about Border Collies&#8221;</li>



<li><strong>Have natural conversations</strong> &#8211; ask follow-ups like &#8220;Are they good with kids?&#8221;</li>



<li><strong>Get enhanced answers</strong> &#8211; when the local model lacks knowledge, we use tool calling to fetch additional data</li>



<li><strong>Handle unknown breeds</strong> &#8211; for rare breeds like &#8220;Caucasian Shepherd,&#8221; the AI gracefully uses our custom data</li>
</ol>



<p>The magic happens when you ask about allergy-friendly dogs. Instead of generic answers, our tool calling system searches through curated breed data and returns specific recommendations.</p>



<figure class="gb-block-image gb-block-image-38055002"><img loading="lazy" decoding="async" width="1402" height="930" class="gb-image gb-image-38055002" src="http://www.swiftyplace.com/wp-content/uploads/2025/07/fmf_chatbot_iosapp.webp" alt="building a ai chatbot app with foundation models framework" title="fmf_chatbot_iosapp" srcset="https://www.swiftyplace.com/wp-content/uploads/2025/07/fmf_chatbot_iosapp.webp 1402w, https://www.swiftyplace.com/wp-content/uploads/2025/07/fmf_chatbot_iosapp-300x199.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2025/07/fmf_chatbot_iosapp-1024x679.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2025/07/fmf_chatbot_iosapp-768x509.webp 768w" sizes="auto, (max-width: 1402px) 100vw, 1402px" /></figure>



<div style="height:41px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">What is Apple&#8217;s Foundation Models Framework?</h2>



<p>The Foundation Models framework gives you direct access to Apple&#8217;s local large language model (LLM) &#8211; the same AI brain behind Siri&#8217;s new capabilities and Apple Intelligence features. Think of it as having ChatGPT built right into your iPhone, but completely private and offline.</p>



<p>This is huge because:</p>



<ul class="wp-block-list">
<li><strong>Zero API costs</strong> &#8211; no more paying per request to OpenAI or Claude</li>



<li><strong>Complete privacy</strong> &#8211; all AI processing happens on-device</li>



<li><strong>Lightning fast</strong> &#8211; no network latency, instant responses</li>



<li><strong>Always available</strong> &#8211; works in airplane mode or poor connectivity</li>
</ul>



<div style="height:41px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Understanding Foundation Models vs Apple&#8217;s Foundation Framework &#8211; Common Naming Confusion</h3>



<p>Don&#8217;t get confused by the name! Apple&#8217;s <strong>Foundation Models framework</strong> is completely different from their older <strong>Foundation framework</strong>.</p>



<p>In AI terminology, a &#8220;foundation model&#8221; means a general-purpose base model that you can customize for specific tasks. It&#8217;s called &#8220;foundation&#8221; because it&#8217;s the foundation you build specialized AI features on top of.</p>



<div style="height:41px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Foundation Models Performance and Limitations</h3>



<p>Technical Specifications That Matter:</p>



<ul class="wp-block-list">
<li><strong>3 billion parameters</strong> (vs ChatGPT&#8217;s 100+ billion)</li>



<li><strong>3GB RAM usage</strong> (why newer devices are required)</li>



<li><strong>4,096 token context window</strong> (conversations have memory limits)</li>



<li><strong>Text-only input/output</strong> (no image processing)</li>



<li><strong>Knowledge cutoff: End of 2023</strong></li>



<li><strong>16 language support</strong> (check <code>model.supportedLanguages</code>)</li>



<li><strong>Adapter compatibility</strong> for fine-tuning specific use cases</li>
</ul>



<p>What This Model Excels At:</p>



<ul class="wp-block-list">
<li><strong>Text summarization</strong> &#8211; Great for condensing content</li>



<li><strong>Information extraction</strong> &#8211; Pull structured data from text</li>



<li><strong>Content classification</strong> &#8211; Categorize text by type/topic</li>



<li><strong>Simple content generation</strong> &#8211; Basic writing tasks</li>
</ul>



<p>What To Avoid Using It For:</p>



<ul class="wp-block-list">
<li><strong>Philosophy or complex reasoning</strong> &#8211; Too small for deep thinking</li>



<li><strong>Current events</strong> &#8211; Knowledge cutoff limitations</li>



<li><strong>Advanced math</strong> &#8211; Prone to calculation errors</li>



<li><strong>Creative writing</strong> &#8211; Limited compared to larger models</li>
</ul>



<div style="height:41px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Device Compatibility Requirements &#8211; The Hard Truth About Apple Intelligence Availability</h3>



<p>Here&#8217;s what you need before diving in &#8211; and it&#8217;s more limited than you might think:</p>



<h4 class="wp-block-heading">iPhone Compatibility for Foundation Models Framework</h4>



<ul class="wp-block-list">
<li><strong>iPhone 15 Pro and 15 Pro Max</strong> (A17 Pro chip)</li>



<li><strong>All iPhone 16 models</strong> (A18 chip)</li>



<li><strong>Older iPhones won&#8217;t work</strong> &#8211; sorry iPhone 13/14 users</li>
</ul>



<h4 class="wp-block-heading">iPad Support for On-Device AI</h4>



<ul class="wp-block-list">
<li><strong>iPad Mini 7th gen</strong> (A17 Pro)</li>



<li><strong>iPad Air M1/M2</strong> models</li>



<li><strong>iPad Pro M1/M2/M4</strong> models</li>



<li><strong>Older iPads are incompatible</strong> &#8211; even recent non-Pro models</li>
</ul>



<h4 class="wp-block-heading">Mac Requirements for Foundation Models</h4>



<ul class="wp-block-list">
<li><strong>Any Mac with M1, M2, M3, or M4 chips</strong></li>



<li><strong>Intel Macs are completely unsupported</strong></li>



<li><strong>macOS Sequoia 15.2+ required</strong></li>
</ul>



<p>Plus, Apple Intelligence must be downloaded and enabled (3GB download, 30-minute setup).</p>



<div style="height:26px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Market Reality &#8211; How Many Users Can Actually Use Your AI App?</h3>



<p>Let&#8217;s talk numbers. Based on a current device adoption roughly:</p>



<ul class="wp-block-list">
<li><strong>Only 15% of iPhones</strong> can run Foundation Models apps</li>



<li><strong>30% of iPads</strong> are compatible (thanks to M1 adoption)</li>



<li><strong>50% of Macs</strong> support it (M-series popularity growing)</li>
</ul>



<p>This means <strong>your app needs robust fallbacks</strong>. Don&#8217;t build AI-only features &#8211; always have non-AI alternatives ready.</p>



<div style="height:26px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">How to Check Device Compatibility</h2>



<p>Smart developers always check compatibility first. Here&#8217;s the complete implementation:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="import SwiftUI
import FoundationModels

struct ContentView: View {
    private var model = SystemModel.default

    var body: some View {
        switch model.availability {
        case .available:
            // Device supports AI - show full features
            ChatbotView()

        case .unavailable(.modelNotReady):
            // Compatible device, but AI model still downloading
            ModelDownloadingView()

        case .unavailable(.deviceNotEligible):
            // Old device - offer alternative features
            NonAIFallbackView()

        case .unavailable(.appleIntelligenceNotEnabled):
            // User needs to enable Apple Intelligence
            EnableIntelligenceView()
        }
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">SwiftUI</span></span>
<span class="line"><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">FoundationModels</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ContentView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">private</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> model </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> SystemModel.default</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">switch</span><span style="color: #F6F6F4"> model.availability {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .available</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #7B7F8B">// Device supports AI - show full features</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">ChatbotView</span><span style="color: #F6F6F4">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .</span><span style="color: #97E1F1">unavailable</span><span style="color: #F6F6F4">(.modelNotReady)</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #7B7F8B">// Compatible device, but AI model still downloading</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">ModelDownloadingView</span><span style="color: #F6F6F4">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .</span><span style="color: #97E1F1">unavailable</span><span style="color: #F6F6F4">(.deviceNotEligible)</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #7B7F8B">// Old device - offer alternative features</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">NonAIFallbackView</span><span style="color: #F6F6F4">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .</span><span style="color: #97E1F1">unavailable</span><span style="color: #F6F6F4">(.appleIntelligenceNotEnabled)</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #7B7F8B">// User needs to enable Apple Intelligence</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">EnableIntelligenceView</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        }</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:36px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Testing Device Compatibility States in Xcode Simulator</h3>



<p>Apple made testing easy with built-in simulator options. In your Xcode scheme settings:</p>



<ol class="wp-block-list">
<li>Go to <strong>Run → Arguments → Environment Variables</strong></li>



<li>Find <strong>&#8220;Simulating foundation model availability&#8221;</strong></li>



<li>Set values like:
<ul class="wp-block-list">
<li><code>deviceNotEligible</code> &#8211; Test old device flow</li>



<li><code>appleIntelligenceNotEnabled</code> &#8211; Test setup flow</li>



<li><code>modelNotReady</code> &#8211; Test download state</li>
</ul>
</li>
</ol>



<p>This lets you test all compatibility scenarios without owning multiple devices</p>



<figure class="gb-block-image gb-block-image-cf7b4017"><img loading="lazy" decoding="async" width="1616" height="920" class="gb-image gb-image-cf7b4017" src="http://www.swiftyplace.com/wp-content/uploads/2025/07/fmf_testing_options.webp" alt="xcode testing foundation models framework availability on device" title="fmf_testing_options" srcset="https://www.swiftyplace.com/wp-content/uploads/2025/07/fmf_testing_options.webp 1616w, https://www.swiftyplace.com/wp-content/uploads/2025/07/fmf_testing_options-300x171.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2025/07/fmf_testing_options-1024x583.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2025/07/fmf_testing_options-768x437.webp 768w, https://www.swiftyplace.com/wp-content/uploads/2025/07/fmf_testing_options-1536x874.webp 1536w" sizes="auto, (max-width: 1616px) 100vw, 1616px" /></figure>



<div style="height:55px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">Running Foundation Models in Playgrounds</h2>



<p>Let&#8217;s start simple with a playground to understand the basics:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="import FoundationModels
import Playgrounds

#Playground {
    
    // Create a language model session
    let session = LanguageModelSession()

    // Define your prompt
    let prompt = &quot;What is the meaning of life?&quot;

    // Get AI response (async operation)
    do {
        let response = try await session.respond(to: prompt)
        
        print(response.content) // This is your AI-generated text
    } catch {
        print(&quot;AI generation failed: (error)&quot;)
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">FoundationModels</span></span>
<span class="line"><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">Playgrounds</span></span>
<span class="line"></span>
<span class="line"><span style="color: #97E1F1">#Playground</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// Create a language model session</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> session </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">LanguageModelSession</span><span style="color: #F6F6F4">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// Define your prompt</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> prompt </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">What is the meaning of life?</span><span style="color: #DEE492">&quot;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// Get AI response (async operation)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">do</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> response </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">try</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> session.</span><span style="color: #97E1F1">respond</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">to</span><span style="color: #F6F6F4">: prompt)</span></span>
<span class="line"><span style="color: #F6F6F4">        </span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">print</span><span style="color: #F6F6F4">(response.content) </span><span style="color: #7B7F8B">// This is your AI-generated text</span></span>
<span class="line"><span style="color: #F6F6F4">    } </span><span style="color: #F286C4">catch</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">print</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">AI generation failed: (error)</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:29px" aria-hidden="true" class="wp-block-spacer"></div>



<p><strong>Performance reality check:</strong> This took 23 seconds on an M1 Mac for a philosophical question. The model runs at your device&#8217;s speed &#8211; no cloud acceleration here.</p>



<figure class="gb-block-image gb-block-image-4d2af7b0"><img loading="lazy" decoding="async" width="1582" height="962" class="gb-image gb-image-4d2af7b0" src="http://www.swiftyplace.com/wp-content/uploads/2025/07/fmf_testing_playground.webp" alt="test new foundation models framework with xcode 26 playground macro" title="fmf_testing_playground" srcset="https://www.swiftyplace.com/wp-content/uploads/2025/07/fmf_testing_playground.webp 1582w, https://www.swiftyplace.com/wp-content/uploads/2025/07/fmf_testing_playground-300x182.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2025/07/fmf_testing_playground-1024x623.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2025/07/fmf_testing_playground-768x467.webp 768w, https://www.swiftyplace.com/wp-content/uploads/2025/07/fmf_testing_playground-1536x934.webp 1536w" sizes="auto, (max-width: 1582px) 100vw, 1582px" /></figure>



<div style="height:38px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Handling AI Generation Errors and Guardrails</h3>



<p>The Foundation Models framework has strict safety measures:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="do {
    let response = try await session.response(to: prompt)
    return response.content
} catch let error as FoundationModels.GenerationError {
    switch error.type {
    case .exceedsContextWindowSize:
        // Conversation too long - start new session
        return &quot;Let's start a fresh conversation&quot;

    case .guardrailViolation:
        // Content flagged as unsafe
        return &quot;I can't help with that request&quot;

    case .unsupportedLanguage:
        // User used non-supported language
        return &quot;Please ask in English or another supported language&quot;

    case .rateLimited:
        // App backgrounded - system prioritizing foreground apps
        return &quot;Please try again in a moment&quot;

    case .concurrentRequests:
        // Multiple requests on same session
        return &quot;Please wait for current response to complete&quot;

    default:
        return &quot;Something went wrong with AI generation&quot;
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">do</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> response </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">try</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> session.</span><span style="color: #97E1F1">response</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">to</span><span style="color: #F6F6F4">: prompt)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> response.content</span></span>
<span class="line"><span style="color: #F6F6F4">} </span><span style="color: #F286C4">catch</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> error </span><span style="color: #F286C4">as</span><span style="color: #F6F6F4"> FoundationModels.</span><span style="color: #97E1F1">GenerationError</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">switch</span><span style="color: #F6F6F4"> error.type {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .exceedsContextWindowSize</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// Conversation too long - start new session</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Let&#39;s start a fresh conversation</span><span style="color: #DEE492">&quot;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .guardrailViolation</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// Content flagged as unsafe</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">I can&#39;t help with that request</span><span style="color: #DEE492">&quot;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .unsupportedLanguage</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// User used non-supported language</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Please ask in English or another supported language</span><span style="color: #DEE492">&quot;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .rateLimited</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// App backgrounded - system prioritizing foreground apps</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Please try again in a moment</span><span style="color: #DEE492">&quot;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .concurrentRequests</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// Multiple requests on same session</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Please wait for current response to complete</span><span style="color: #DEE492">&quot;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">default:</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Something went wrong with AI generation</span><span style="color: #DEE492">&quot;</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:38px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">Real-World Example &#8211; Building a Dog Breed Knowledge Assistant</h2>



<p>Let&#8217;s test the model&#8217;s knowledge boundaries with a practical example:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="// Test with well-known breed
let borderColliePrompt = &quot;Tell me about Border Collies for apartment living&quot;
// Result: Good general knowledge, reasonable advice

// Test with rare breed
let caucasianPrompt = &quot;Can I keep a Caucasian Shepherd in an apartment?&quot;
// Result: Hallucination! Says they're &quot;wonderful apartment companions&quot;" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #7B7F8B">// Test with well-known breed</span></span>
<span class="line"><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> borderColliePrompt </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Tell me about Border Collies for apartment living</span><span style="color: #DEE492">&quot;</span></span>
<span class="line"><span style="color: #7B7F8B">// Result: Good general knowledge, reasonable advice</span></span>
<span class="line"></span>
<span class="line"><span style="color: #7B7F8B">// Test with rare breed</span></span>
<span class="line"><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> caucasianPrompt </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Can I keep a Caucasian Shepherd in an apartment?</span><span style="color: #DEE492">&quot;</span></span>
<span class="line"><span style="color: #7B7F8B">// Result: Hallucination! Says they&#39;re &quot;wonderful apartment companions&quot;</span></span></code></pre></div>



<p><strong>The problem:</strong> The model confidently gives wrong advice about a 150-pound protective breed being apartment-friendly. This is why tool calling becomes essential.</p>



<div style="height:38px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Why Tool Calling Is Essential for Production AI Apps</h3>



<p>The local model has knowledge gaps. When it doesn&#8217;t know something, it often guesses wrong instead of admitting ignorance. Tool calling lets you:</p>



<ol class="wp-block-list">
<li><strong>Detect knowledge gaps</strong> &#8211; Recognize when the model lacks specific info</li>



<li><strong>Fetch accurate data</strong> &#8211; Pull from your curated database</li>



<li><strong>Enhance responses</strong> &#8211; Combine AI reasoning with factual data</li>



<li><strong>Maintain accuracy</strong> &#8211; Prevent harmful misinformation</li>
</ol>



<p>Think of tool calling as giving your AI access to Google, but with your own trusted data sources.</p>



<div style="height:38px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Working Around Text-Only Limitations with Vision Framework Integration</h3>



<p>The Foundation Models framework only handles text, but you can build powerful workflows:</p>



<pre class="wp-block-code"><code>// 1. User takes photo of a recipe
// 2. Use Vision framework to extract text from image
// 3. Pass extracted text to Foundation Models
// 4. AI formats and structures the recipe data</code></pre>



<p>This pattern works great for:</p>



<ul class="wp-block-list">
<li><strong>Document scanning</strong> &#8211; Extract and process text from images</li>



<li><strong>Recipe digitization</strong> &#8211; Photo to structured recipe data</li>



<li><strong>Text translation</strong> &#8211; OCR + AI translation</li>



<li><strong>Content organization</strong> &#8211; Extract and categorize information</li>
</ul>



<div style="height:38px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Choosing the Right Use Cases for 3-Billion Parameter Models</h3>



<p>The model size matters. Here&#8217;s when Foundation Models work well vs when you need alternatives:</p>



<h3 class="wp-block-heading">Perfect Use Cases</h3>



<ul class="wp-block-list">
<li><strong>App help chatbots</strong> &#8211; Answer questions about your app&#8217;s features</li>



<li><strong>Content summarization</strong> &#8211; Condense long text into key points</li>



<li><strong>Data extraction</strong> &#8211; Pull specific info from unstructured text</li>



<li><strong>Simple classification</strong> &#8211; Categorize content by type or sentiment</li>
</ul>



<h3 class="wp-block-heading">Consider Cloud APIs Instead</h3>



<ul class="wp-block-list">
<li><strong>Creative writing</strong> &#8211; Need larger models for quality output</li>



<li><strong>Complex reasoning</strong> &#8211; Mathematical or logical problem solving</li>



<li><strong>Current information</strong> &#8211; Anything requiring up-to-date knowledge</li>



<li><strong>Multi-language support</strong> &#8211; Beyond the 12 supported languages</li>
</ul>



<div style="height:58px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">Configuring Your Language Model Session for Production Apps</h2>



<p>The <code>LanguageModelSession</code> has several important parameters you can customize:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="let session = LanguageModelSession(
    model: .default, // Can use custom adapters here
    guardrails: .default, // Safety filters (strict by default)
    tools: [], // We'll add tool calling in part 2
    instructions: &quot;&quot;&quot;
        You are a dog specialist. Your job is to give helpful 
        advice to new dog owners.
        &quot;&quot;&quot;
)" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> session </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">LanguageModelSession</span><span style="color: #F6F6F4">(</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">model</span><span style="color: #F6F6F4">: .default, </span><span style="color: #7B7F8B">// Can use custom adapters here</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">guardrails</span><span style="color: #F6F6F4">: .default, </span><span style="color: #7B7F8B">// Safety filters (strict by default)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">tools</span><span style="color: #F6F6F4">: [], </span><span style="color: #7B7F8B">// We&#39;ll add tool calling in part 2</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">instructions</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;&quot;&quot;</span></span>
<span class="line"><span style="color: #E7EE98">        You are a dog specialist. Your job is to give helpful </span></span>
<span class="line"><span style="color: #E7EE98">        advice to new dog owners.</span></span>
<span class="line"><span style="color: #E7EE98">        </span><span style="color: #DEE492">&quot;&quot;&quot;</span></span>
<span class="line"><span style="color: #F6F6F4">)</span></span></code></pre></div>



<div style="height:29px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Setting Effective System Instructions</h3>



<p>Instructions are more powerful than regular prompts. They define your AI&#8217;s personality and boundaries:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="let instructions = &quot;&quot;&quot;
You are a dog specialist. Your job is to give helpful advice to new dog owners.
&quot;&quot;&quot;

// Test the boundaries
// User asks: &quot;What is 1 + 1?&quot;
// AI responds: &quot;I'm sorry, I cannot assist with that request.&quot;" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> instructions </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;&quot;&quot;</span></span>
<span class="line"><span style="color: #E7EE98">You are a dog specialist. Your job is to give helpful advice to new dog owners.</span></span>
<span class="line"><span style="color: #DEE492">&quot;&quot;&quot;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #7B7F8B">// Test the boundaries</span></span>
<span class="line"><span style="color: #7B7F8B">// User asks: &quot;What is 1 + 1?&quot;</span></span>
<span class="line"><span style="color: #7B7F8B">// AI responds: &quot;I&#39;m sorry, I cannot assist with that request.&quot;</span></span></code></pre></div>



<p>Instructions act as strong guardrails &#8211; even when users try to misuse your app, the AI stays in character.</p>



<div style="height:61px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Controlling AI Response Quality with Generation Options</h3>



<h4 class="wp-block-heading">Temperature Settings for Creativity Control</h4>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="let options = GenerationOptions(
    sampling: .default, // Balanced creativity
    temperature: 1.0,   // 0 = robotic, 2 = chaotic
    maxTokens: nil      // Let it finish thoughts naturally
)" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> options </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">GenerationOptions</span><span style="color: #F6F6F4">(</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">sampling</span><span style="color: #F6F6F4">: .default, </span><span style="color: #7B7F8B">// Balanced creativity</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">temperature</span><span style="color: #F6F6F4">: </span><span style="color: #BF9EEE">1.0</span><span style="color: #F6F6F4">,   </span><span style="color: #7B7F8B">// 0 = robotic, 2 = chaotic</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">maxTokens</span><span style="color: #F6F6F4">: </span><span style="color: #BF9EEE">nil</span><span style="color: #F6F6F4">      </span><span style="color: #7B7F8B">// Let it finish thoughts naturally</span></span>
<span class="line"><span style="color: #F6F6F4">)</span></span></code></pre></div>



<p><strong>Avoid these common mistakes:</strong></p>



<ul class="wp-block-list">
<li><code>temperature: 0</code> = Always identical, robotic responses</li>



<li><code>temperature: 2</code> = Too random and incoherent</li>



<li>Setting <code>maxTokens</code> too low = Cut off mid-sentence</li>
</ul>



<h4 class="wp-block-heading">Better Ways to Control Response Length</h4>



<p>Instead of limiting tokens (which cuts off responses), use natural language:</p>



<pre class="wp-block-code"><code>// &#x274c; Bad: maxTokens: 200 (cuts off mid-sentence)

// &#x2705; Good: Add to instructions
"Keep responses to 100-200 words"
"Answer in 2 paragraphs" 
"Give brief, concise answers"</code></pre>



<div style="height:46px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">Implementing Real-Time Streaming Responses </h2>



<p>Nobody wants to wait 23 seconds staring at a blank screen. Streaming shows text as it generates:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="class ChatViewModel: ObservableObject {
    @Published var partialGenerated: String.PartialGenerated?
    @Published var isResponding = false
    private var streamingTask: Task&lt;Void, Never&gt;?

    func sendMessage(_ userInput: String) {
        isResponding = true

        streamingTask = Task {
            do {
                let stream = try session.streamResponse(to: userInput)

                for try await partial in stream {
                    // Check if task was cancelled
                    guard !Task.isCancelled else { break }

                    // Update UI with each new token
                    await MainActor.run {
                        self.partialGenerated = partial
                    }
                }

                // Streaming complete
                await MainActor.run {
                    self.isResponding = false
                    self.saveResponse()
                }

            } catch {
                await MainActor.run {
                    self.handleError(error)
                }
            }
        }
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">class</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">ChatViewModel</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">ObservableObject </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@Published</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> partialGenerated: </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4">.PartialGenerated</span><span style="color: #F286C4">?</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@Published</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> isResponding </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">false</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">private</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> streamingTask: Task&lt;</span><span style="color: #97E1F1; font-style: italic">Void</span><span style="color: #F6F6F4">, </span><span style="color: #BF9EEE">Never</span><span style="color: #F6F6F4">&gt;</span><span style="color: #F286C4">?</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">sendMessage</span><span style="color: #F6F6F4">(</span><span style="color: #62E884">_</span><span style="color: #F6F6F4"> </span><span style="color: #FFB86C; font-style: italic">userInput</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4">) {</span></span>
<span class="line"><span style="color: #F6F6F4">        isResponding </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">true</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        streamingTask </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">Task</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #F286C4">do</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> stream </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">try</span><span style="color: #F6F6F4"> session.</span><span style="color: #97E1F1">streamResponse</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">to</span><span style="color: #F6F6F4">: userInput)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #F286C4">for</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">try</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> partial </span><span style="color: #F286C4">in</span><span style="color: #F6F6F4"> stream {</span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #7B7F8B">// Check if task was cancelled</span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #F286C4">guard</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">!</span><span style="color: #F6F6F4">Task.isCancelled </span><span style="color: #F286C4">else</span><span style="color: #F6F6F4"> { </span><span style="color: #F286C4">break</span><span style="color: #F6F6F4"> }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #7B7F8B">// Update UI with each new token</span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> MainActor.</span><span style="color: #97E1F1">run</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">                        </span><span style="color: #BF9EEE; font-style: italic">self</span><span style="color: #F6F6F4">.partialGenerated </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> partial</span></span>
<span class="line"><span style="color: #F6F6F4">                    }</span></span>
<span class="line"><span style="color: #F6F6F4">                }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #7B7F8B">// Streaming complete</span></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> MainActor.</span><span style="color: #97E1F1">run</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #BF9EEE; font-style: italic">self</span><span style="color: #F6F6F4">.isResponding </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">false</span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #BF9EEE; font-style: italic">self</span><span style="color: #F6F6F4">.</span><span style="color: #97E1F1">saveResponse</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">                }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">            } </span><span style="color: #F286C4">catch</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> MainActor.</span><span style="color: #97E1F1">run</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #BF9EEE; font-style: italic">self</span><span style="color: #F6F6F4">.</span><span style="color: #97E1F1">handleError</span><span style="color: #F6F6F4">(error)</span></span>
<span class="line"><span style="color: #F6F6F4">                }</span></span>
<span class="line"><span style="color: #F6F6F4">            }</span></span>
<span class="line"><span style="color: #F6F6F4">        }</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:32px" aria-hidden="true" class="wp-block-spacer"></div>



<figure class="gb-block-image gb-block-image-461db406"><img loading="lazy" decoding="async" width="1868" height="930" class="gb-image gb-image-461db406" src="http://www.swiftyplace.com/wp-content/uploads/2025/07/fmf_streaming.webp" alt="streaming response in swiftui app with foundatin models framework" title="fmf_streaming" srcset="https://www.swiftyplace.com/wp-content/uploads/2025/07/fmf_streaming.webp 1868w, https://www.swiftyplace.com/wp-content/uploads/2025/07/fmf_streaming-300x149.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2025/07/fmf_streaming-1024x510.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2025/07/fmf_streaming-768x382.webp 768w, https://www.swiftyplace.com/wp-content/uploads/2025/07/fmf_streaming-1536x765.webp 1536w" sizes="auto, (max-width: 1868px) 100vw, 1868px" /></figure>



<div style="height:42px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Creating Smooth Streaming Animations in SwiftUI</h3>



<p>Make your streaming responses feel polished with proper animations:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="struct StreamingResponseView: View {
    let partialResponse: String.PartialGenerated

    var body: some View {
        Text(partialResponse.content, format: .markdown)
            .padding()
            .background(.gray.opacity(0.1), in: RoundedRectangle(cornerRadius: 12))
            .contentTransition(.opacity)
            .animation(.bouncy, value: partialResponse)
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">StreamingResponseView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> partialResponse: </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4">.PartialGenerated</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">Text</span><span style="color: #F6F6F4">(partialResponse.content, </span><span style="color: #97E1F1">format</span><span style="color: #F6F6F4">: .markdown)</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">padding</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">background</span><span style="color: #F6F6F4">(.gray.</span><span style="color: #97E1F1">opacity</span><span style="color: #F6F6F4">(</span><span style="color: #BF9EEE">0.1</span><span style="color: #F6F6F4">), </span><span style="color: #97E1F1">in</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1">RoundedRectangle</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">cornerRadius</span><span style="color: #F6F6F4">: </span><span style="color: #BF9EEE">12</span><span style="color: #F6F6F4">))</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">contentTransition</span><span style="color: #F6F6F4">(.opacity)</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">animation</span><span style="color: #F6F6F4">(.bouncy, </span><span style="color: #97E1F1">value</span><span style="color: #F6F6F4">: partialResponse)</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<p><strong>Key animation principles:</strong></p>



<ul class="wp-block-list">
<li>Use <code>.animation(.bouncy, value: partialResponse)</code> for smooth text updates</li>



<li>Add <code>.contentTransition(.opacity)</code> to avoid choppy text changes</li>



<li>Keep transition duration short (0.2-0.5 seconds max)</li>
</ul>



<div style="height:34px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Fixing View Identity Problems</h3>



<p>When streaming text replaces with final messages, SwiftUI can get confused about view identity:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="// &#x274c; Causes UI jumps
ForEach(messages) { message in
    MessageView(message: message)
}
if let partial = partialGenerated {
    StreamingView(partial: partial)
}

// &#x2705; Fixed with consistent IDs
ForEach(messages) { message in
    MessageView(message: message)
        .id(message.id)
}
if let partial = partialGenerated {
    StreamingView(partial: partial)
        .id(partialId ?? UUID()) // Same ID used for final message
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #7B7F8B">// &#x274c; Causes UI jumps</span></span>
<span class="line"><span style="color: #97E1F1">ForEach</span><span style="color: #F6F6F4">(messages) { message </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">MessageView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">message</span><span style="color: #F6F6F4">: message)</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span>
<span class="line"><span style="color: #F286C4">if</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> partial </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> partialGenerated {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">StreamingView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">partial</span><span style="color: #F6F6F4">: partial)</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #7B7F8B">// &#x2705; Fixed with consistent IDs</span></span>
<span class="line"><span style="color: #97E1F1">ForEach</span><span style="color: #F6F6F4">(messages) { message </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">MessageView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">message</span><span style="color: #F6F6F4">: message)</span></span>
<span class="line"><span style="color: #F6F6F4">        .</span><span style="color: #97E1F1">id</span><span style="color: #F6F6F4">(message.id)</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span>
<span class="line"><span style="color: #F286C4">if</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> partial </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> partialGenerated {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">StreamingView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">partial</span><span style="color: #F6F6F4">: partial)</span></span>
<span class="line"><span style="color: #F6F6F4">        .</span><span style="color: #97E1F1">id</span><span style="color: #F6F6F4">(partialId </span><span style="color: #F286C4">??</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">UUID</span><span style="color: #F6F6F4">()) </span><span style="color: #7B7F8B">// Same ID used for final message</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<h2 class="wp-block-heading">Managing Chat Sessions and Message History</h2>



<p>Handle conversation flow properly with session management:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="class ChatViewModel: ObservableObject {
    @Published var messages: [ChatMessage] = []
    @Published var userInput = &quot;&quot;
    private var session: LanguageModelSession
    private let instructions = &quot;You are a dog specialist...&quot;

    func resetSession() {
        // Cancel any ongoing streaming
        streamingTask?.cancel()

        // Clear UI state
        messages.removeAll()
        partialGenerated = nil
        isResponding = false

        // Create fresh session (important!)
        session = LanguageModelSession(instructions: instructions)
    }

    private func saveResponse() {
        guard let partial = partialGenerated else { return }

        // Add AI response to chat history
        messages.append(ChatMessage(
            id: partialId ?? UUID(),
            role: .assistant,
            content: partial.content
        ))

        // Clear streaming state
        partialGenerated = nil
        partialId = nil
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">class</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">ChatViewModel</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">ObservableObject </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@Published</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> messages: [ChatMessage] </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> []</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@Published</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> userInput </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;&quot;</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">private</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> session: LanguageModelSession</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">private</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> instructions </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">You are a dog specialist...</span><span style="color: #DEE492">&quot;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">resetSession</span><span style="color: #F6F6F4">() {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// Cancel any ongoing streaming</span></span>
<span class="line"><span style="color: #F6F6F4">        streamingTask</span><span style="color: #F286C4">?</span><span style="color: #F6F6F4">.</span><span style="color: #97E1F1">cancel</span><span style="color: #F6F6F4">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// Clear UI state</span></span>
<span class="line"><span style="color: #F6F6F4">        messages.</span><span style="color: #97E1F1">removeAll</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        partialGenerated </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">nil</span></span>
<span class="line"><span style="color: #F6F6F4">        isResponding </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">false</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// Create fresh session (important!)</span></span>
<span class="line"><span style="color: #F6F6F4">        session </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">LanguageModelSession</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">instructions</span><span style="color: #F6F6F4">: instructions)</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">private</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">saveResponse</span><span style="color: #F6F6F4">() {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">guard</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> partial </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> partialGenerated </span><span style="color: #F286C4">else</span><span style="color: #F6F6F4"> { </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// Add AI response to chat history</span></span>
<span class="line"><span style="color: #F6F6F4">        messages.</span><span style="color: #97E1F1">append</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">ChatMessage</span><span style="color: #F6F6F4">(</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">id</span><span style="color: #F6F6F4">: partialId </span><span style="color: #F286C4">??</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">UUID</span><span style="color: #F6F6F4">(),</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">role</span><span style="color: #F6F6F4">: .assistant,</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">content</span><span style="color: #F6F6F4">: partial.content</span></span>
<span class="line"><span style="color: #F6F6F4">        ))</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// Clear streaming state</span></span>
<span class="line"><span style="color: #F6F6F4">        partialGenerated </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">nil</span></span>
<span class="line"><span style="color: #F6F6F4">        partialId </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">nil</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:43px" aria-hidden="true" class="wp-block-spacer"></div>
</div></div>



<h3 class="wp-block-heading">Proper Progress Indication</h3>



<p>Show loading states only when appropriate:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="if isResponding &amp;&amp; partialGenerated == nil {
    // Show spinner only before streaming starts
    ProgressView()
} else if let partial = partialGenerated {
    // Show streaming content
    StreamingResponseView(partial: partial)
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">if</span><span style="color: #F6F6F4"> isResponding </span><span style="color: #F286C4">&amp;&amp;</span><span style="color: #F6F6F4"> partialGenerated </span><span style="color: #F286C4">==</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">nil</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// Show spinner only before streaming starts</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">ProgressView</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">} </span><span style="color: #F286C4">else</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">if</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> partial </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> partialGenerated {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// Show streaming content</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">StreamingResponseView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">partial</span><span style="color: #F6F6F4">: partial)</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:32px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Handling Concurrent Requests and Session Limits</h3>



<p>Each <code>LanguageModelSession</code> can only handle one request at a time:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="func sendMessage(_ input: String) {
    // Prevent multiple concurrent requests
    guard !session.isResponding else { return }

    // Or create multiple sessions for parallel processing
    let newSession = LanguageModelSession(instructions: instructions)
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">sendMessage</span><span style="color: #F6F6F4">(</span><span style="color: #62E884">_</span><span style="color: #F6F6F4"> </span><span style="color: #FFB86C; font-style: italic">input</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4">) {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// Prevent multiple concurrent requests</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">guard</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">!</span><span style="color: #F6F6F4">session.isResponding </span><span style="color: #F286C4">else</span><span style="color: #F6F6F4"> { </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// Or create multiple sessions for parallel processing</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> newSession </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">LanguageModelSession</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">instructions</span><span style="color: #F6F6F4">: instructions)</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<p><strong>Session management strategies:</strong></p>



<ul class="wp-block-list">
<li><strong>Single session:</strong> Simple conversations with memory</li>



<li><strong>Multiple sessions:</strong> Parallel processing different topics</li>



<li><strong>Session pools:</strong> Handle high-volume concurrent requests</li>
</ul>



<div style="height:37px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Real-World Error Handling and User Feedback</h3>



<p>The Foundation Models framework can fail in various ways. Handle them gracefully:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="private func handleStreamingError(_ error: Error) {
    if let genError = error as? FoundationModels.GenerationError {
        switch genError.type {
        case .guardrailViolation:
            showMessage(&quot;I can't help with that type of request&quot;)
        case .exceedsContextWindowSize:
            showMessage(&quot;This conversation is getting too long. Let's start fresh!&quot;)
            resetSession()
        case .rateLimited:
            showMessage(&quot;I'm busy with other tasks. Please try again in a moment&quot;)
        default:
            showMessage(&quot;Something went wrong. Please try again&quot;)
        }
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">private</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">handleStreamingError</span><span style="color: #F6F6F4">(</span><span style="color: #62E884">_</span><span style="color: #F6F6F4"> </span><span style="color: #FFB86C; font-style: italic">error</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">Error</span><span style="color: #F6F6F4">) {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">if</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> genError </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> error </span><span style="color: #F286C4">as?</span><span style="color: #F6F6F4"> FoundationModels.GenerationError {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">switch</span><span style="color: #F6F6F4"> genError.type {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .guardrailViolation</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">showMessage</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">I can&#39;t help with that type of request</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .exceedsContextWindowSize</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">showMessage</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">This conversation is getting too long. Let&#39;s start fresh!</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">resetSession</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .rateLimited</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">showMessage</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">I&#39;m busy with other tasks. Please try again in a moment</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">default:</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">showMessage</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Something went wrong. Please try again</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">        }</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:72px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">Performance Optimization Tips for On-Device AI</h2>



<h3 class="wp-block-heading">Memory Management</h3>



<ul class="wp-block-list">
<li>Monitor memory usage &#8211; the 3B model uses ~3GB RAM</li>



<li>Consider releasing sessions when not needed</li>



<li>Test on real devices, not just simulators</li>
</ul>



<h3 class="wp-block-heading">Battery Impact</h3>



<ul class="wp-block-list">
<li>Long AI generations drain battery quickly</li>



<li>Consider limiting response length for mobile use</li>



<li>Show battery usage warnings for intensive tasks</li>
</ul>



<h3 class="wp-block-heading">Background Handling</h3>



<ul class="wp-block-list">
<li>AI requests get rate-limited when app goes to background</li>



<li>Save conversation state before backgrounding</li>



<li>Resume gracefully when returning to foreground</li>
</ul>



<div style="height:44px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">What&#8217;s Next: Tool Calling and Knowledge Enhancement</h2>



<p>This basic chatbot is just the foundation. The real power comes with tool calling &#8211; letting your AI fetch current data, search databases, and enhance its knowledge beyond the 2023 training cutoff.</p>



<p>In the next tutorial, we&#8217;ll add:</p>



<ul class="wp-block-list">
<li><strong>Custom tool functions</strong> to fetch dog breed data</li>



<li><strong>Intent detection</strong> to decide when to use tools</li>



<li><strong>Knowledge enhancement</strong> for accurate, up-to-date responses</li>



<li><strong>Structured data integration</strong> with your app&#8217;s backend</li>
</ul>



<p>The Foundation Models framework gives you a solid base for on-device AI, but tool calling makes it production-ready for real-world applications.</p>



<p><a href="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqbXhIOElMNXhydmlxN3VCdzdyNUJrRTI3ejNpUXxBQ3Jtc0tubWxHQ29UZjBQVi1oTFJqMVVRbzQ2cnNCUHlQNEt6ZzJiN3k3Ym1mV29OYUR4OU9UVlNOeFlsUGVuamVVNFhoYnJzVHFZS3JYNHNaV0N6a2tsUURQdTlhejlFY3Y1LWd1ZVQ0NTBPM290MldIemRUYw&amp;q=https%3A%2F%2Fschool.swiftyplace.com%2Ff%2Fproject-files-foundation-models-framework&amp;v=wl0vZrQ5J9Q" data-type="link" data-id="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqbXhIOElMNXhydmlxN3VCdzdyNUJrRTI3ejNpUXxBQ3Jtc0tubWxHQ29UZjBQVi1oTFJqMVVRbzQ2cnNCUHlQNEt6ZzJiN3k3Ym1mV29OYUR4OU9UVlNOeFlsUGVuamVVNFhoYnJzVHFZS3JYNHNaV0N6a2tsUURQdTlhejlFY3Y1LWd1ZVQ0NTBPM290MldIemRUYw&amp;q=https%3A%2F%2Fschool.swiftyplace.com%2Ff%2Fproject-files-foundation-models-framework&amp;v=wl0vZrQ5J9Q" target="_blank" rel="noopener"><strong><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2b07.png" alt="⬇" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Download project files</strong></a></p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p><em>Ready to enhance your chatbot with tool calling? Check out part 2 of this series where we add custom data sources and make our AI truly intelligent.</em></p>



<p></p>
<p>The post <a rel="nofollow" href="https://www.swiftyplace.com/blog/foundation-models-framework">Building an AI Chatbot in SwiftUI with Foundation Models Framework</a> appeared first on <a rel="nofollow" href="https://www.swiftyplace.com">swiftyplace</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.swiftyplace.com/blog/foundation-models-framework/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Introduction to XCTest: How to Write Unit Tests for iOS apps</title>
		<link>https://www.swiftyplace.com/blog/unit-testing-xctest-swiftui?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=unit-testing-xctest-swiftui</link>
					<comments>https://www.swiftyplace.com/blog/unit-testing-xctest-swiftui#respond</comments>
		
		<dc:creator><![CDATA[Karin Prater]]></dc:creator>
		<pubDate>Mon, 19 May 2025 15:05:03 +0000</pubDate>
				<category><![CDATA[Testing in iOS development]]></category>
		<guid isPermaLink="false">https://www.swiftyplace.com/?p=1005568</guid>

					<description><![CDATA[<p>Writing tests sounds boring until your app breaks and you have no idea why. In this post, we’ll walk through how to write simple, focused unit tests for a SwiftUI app using Apple’s built-in framework: ... <a title="Introduction to XCTest: How to Write Unit Tests for iOS apps" class="read-more" href="https://www.swiftyplace.com/blog/unit-testing-xctest-swiftui" aria-label="More on Introduction to XCTest: How to Write Unit Tests for iOS apps">Read more</a></p>
<p>The post <a rel="nofollow" href="https://www.swiftyplace.com/blog/unit-testing-xctest-swiftui">Introduction to XCTest: How to Write Unit Tests for iOS apps</a> appeared first on <a rel="nofollow" href="https://www.swiftyplace.com">swiftyplace</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Writing tests sounds boring until your app breaks and you have no idea why. In this post, we’ll walk through how to write simple, focused <strong>unit tests</strong> for a SwiftUI app using Apple’s built-in framework: <strong>XCTest</strong>.<br>We’ll use a small SwiftUI demo app as a sandbox. You’ll learn how to:</p>



<ul class="wp-block-list">
<li>What XCTest is and how it works</li>



<li>How to write and run tests in Xcode</li>



<li>Test basic logic in a ViewModel</li>



<li>Avoid common testing mistakes</li>



<li>Understand what parts of your app are worth testing</li>
</ul>



<p>This guide is aimed at intermediate iOS developers—those who know Swift, have written apps, maybe even shipped a few—but haven’t yet made testing a regular habit. You’ll learn how to test logic without wasting time or overengineering your codebase.</p>



<div style="height:50px" aria-hidden="true" class="wp-block-spacer"></div>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<h2 class="wp-block-heading">What Is XCTest and How Does It Work?</h2>



<p><strong>XCTest</strong> is the default testing framework in Xcode. It runs your tests automatically and shows you exactly what passes, fails, or breaks. It supports:</p>



<ul class="wp-block-list">
<li><strong>Unit tests</strong> – testing logic like ViewModels, data formatting, math, etc.</li>



<li><strong>UI tests</strong> – simulating taps, swipes, etc.</li>



<li><strong>Performance tests</strong> – measuring runtime of specific code</li>
</ul>



<p>For now, we’re focusing on <strong>unit testing</strong>.</p>



<p>Note that WWDC24 introduced a newer framework <a href="https://www.swiftyplace.com/blog/unit-testing-ios-apps">Swift Testing that can replace XCTest for unit testing.</a></p>
</div></div>



<h2 class="wp-block-heading">How to Add Unit Tests to Your Xcode Project</h2>



<h4 class="wp-block-heading">Step 1: Create a Test Target</h4>



<p>If your project doesn’t already have one:</p>



<ol class="wp-block-list">
<li>In Xcode, go to <strong>File > New > Target</strong></li>



<li>Choose <strong>XCTest for Unit and UI Test</strong></li>



<li>Call it <code>ItemAppTests</code></li>
</ol>



<figure class="gb-block-image gb-block-image-92479e12"><img loading="lazy" decoding="async" width="1464" height="1064" class="gb-image gb-image-92479e12" src="http://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_1.webp" alt="add swift testing wih xctest when creating a new project in Xcode" title="unit_testing_1" srcset="https://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_1.webp 1464w, https://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_1-300x218.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_1-1024x744.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_1-768x558.webp 768w" sizes="auto, (max-width: 1464px) 100vw, 1464px" /></figure>



<h4 class="wp-block-heading">Step 2: Add Your First Test Case</h4>



<p>By default, your app code is not visible in your test target unless you mark it as <code>public</code>. But constantly marking internal stuff as <code>public</code> just for testing is dumb.</p>



<p>Instead, use this in your test files:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="@testable import YourApp" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">@testable</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">YourApp</span></span></code></pre></div>



<div style="height:30px" aria-hidden="true" class="wp-block-spacer"></div>



<p>This allows your test code to access <code>internal</code> stuff—functions, classes, etc.—without changing your access levels just for testing.</p>



<p>You write unit tests by subclassing XCTestCase. This is a simple test function:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="import XCTest
@testable import ItemApp

final class ItemAppTests: XCTestCase {
    func test_example() {
        XCTAssertTrue(true)
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">XCTest</span></span>
<span class="line"><span style="color: #F286C4">@testable</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ItemApp</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">final</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">class</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">ItemAppTests</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">XCTestCase </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">test_example</span><span style="color: #F6F6F4">() {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">XCTAssertTrue</span><span style="color: #F6F6F4">(</span><span style="color: #BF9EEE">true</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:32px" aria-hidden="true" class="wp-block-spacer"></div>



<p>Let’s break it down:</p>



<ul class="wp-block-list">
<li>import XCTest gives you the test tools.</li>



<li>@testable import ItemApp makes your app’s internal classes accessible to your test target.</li>



<li>The class must inherit from XCTestCase.</li>



<li>Every test method starts with test – otherwise it won’t run.</li>



<li>XCTAssertTrue(true) is just a placeholder—it&#8217;s a way to make the test pass.</li>
</ul>



<div style="height:32px" aria-hidden="true" class="wp-block-spacer"></div>



<h4 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/25b6.png" alt="▶" class="wp-smiley" style="height: 1em; max-height: 1em;" /> How to Run Your Tests</h4>



<p>You can run your tests from <strong>anywhere in Xcode</strong>. There are three ways:</p>



<ol class="wp-block-list">
<li><p><strong>The diamond icon</strong> <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/25b6.png" alt="▶" class="wp-smiley" style="height: 1em; max-height: 1em;" /><br>Look to the left of any test method or class in the gutter.<br>Click it to run that specific test.</p></li>



<li><p><strong>Keyboard shortcut:</strong> <code>⌘ + U</code><br>This runs <strong>all tests in your project</strong>.</p></li>



<li><p><strong>Product > Test</strong> from the top menu.</p></li>
</ol>



<figure class="gb-block-image gb-block-image-e2796300"><img loading="lazy" decoding="async" width="2208" height="1247" class="gb-image gb-image-e2796300" src="https://www.swiftyplace.com/wp-content/uploads/2025/05/xctest_example_run.webp" alt="" title="xctest_example_run" srcset="https://www.swiftyplace.com/wp-content/uploads/2025/05/xctest_example_run.webp 2208w, https://www.swiftyplace.com/wp-content/uploads/2025/05/xctest_example_run-300x169.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2025/05/xctest_example_run-1024x578.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2025/05/xctest_example_run-768x434.webp 768w, https://www.swiftyplace.com/wp-content/uploads/2025/05/xctest_example_run-1536x867.webp 1536w, https://www.swiftyplace.com/wp-content/uploads/2025/05/xctest_example_run-2048x1157.webp 2048w" sizes="auto, (max-width: 2208px) 100vw, 2208px" /></figure>



<p>If a test fails, Xcode will highlight the failed line and show you the actual vs expected values in the <strong>Test navigator</strong> on the left sidebar.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="547" src="https://www.swiftyplace.com/wp-content/uploads/2025/05/xctest_example_result-1024x547.webp" alt="" class="wp-image-1005589" srcset="https://www.swiftyplace.com/wp-content/uploads/2025/05/xctest_example_result-1024x547.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2025/05/xctest_example_result-300x160.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2025/05/xctest_example_result-768x411.webp 768w, https://www.swiftyplace.com/wp-content/uploads/2025/05/xctest_example_result-1536x821.webp 1536w, https://www.swiftyplace.com/wp-content/uploads/2025/05/xctest_example_result-2048x1095.webp 2048w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<div style="height:65px" aria-hidden="true" class="wp-block-spacer"></div>



<h4 class="wp-block-heading">What Is XCTAssert?</h4>



<p>XCTest provides a bunch of <code>XCTAssert*</code> functions to check if your code does what you expect.</p>



<p>Here are the most useful ones:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Function</th><th>Use Case</th></tr></thead><tbody><tr><td><code>XCTAssertTrue</code></td><td>Something should be <code>true</code></td></tr><tr><td><code>XCTAssertFalse</code></td><td>Something should be <code>false</code></td></tr><tr><td><code>XCTAssertEqual</code></td><td>Two values should be equal</td></tr><tr><td><code>XCTAssertNotEqual</code></td><td>Two values should not be equal</td></tr><tr><td><code>XCTAssertNil</code></td><td>Value should be <code>nil</code></td></tr><tr><td><code>XCTAssertNotNil</code></td><td>Value should not be <code>nil</code></td></tr></tbody></table></figure>



<p><strong>Example:</strong></p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="let count = 3
XCTAssertEqual(count, 3)
XCTAssertTrue(count &gt; 0)
XCTAssertNotNil(viewModel.items.last)" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> count </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">3</span></span>
<span class="line"><span style="color: #97E1F1">XCTAssertEqual</span><span style="color: #F6F6F4">(count, </span><span style="color: #BF9EEE">3</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #97E1F1">XCTAssertTrue</span><span style="color: #F6F6F4">(count </span><span style="color: #F286C4">&gt;</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">0</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #97E1F1">XCTAssertNotNil</span><span style="color: #F6F6F4">(viewModel.items.</span><span style="color: #BF9EEE">last</span><span style="color: #F6F6F4">)</span></span></code></pre></div>



<p>If any of these assertions fail, the test fails, and you get clear feedback inside Xcode.</p>



<p>To help you understand what is going on when soemthing breaks later, you can add failure messages:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="XCTAssertEqual(count, 3, &quot;Expect that the item count is 3&quot;)
XCTAssertTrue(count &gt; 0, &quot;We should have at least have 1 item&quot;)" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #97E1F1">XCTAssertEqual</span><span style="color: #F6F6F4">(count, </span><span style="color: #BF9EEE">3</span><span style="color: #F6F6F4">, </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Expect that the item count is 3</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #97E1F1">XCTAssertTrue</span><span style="color: #F6F6F4">(count </span><span style="color: #F286C4">&gt;</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">0</span><span style="color: #F6F6F4">, </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">We should have at least have 1 item</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span></code></pre></div>



<div style="height:49px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">Testing a SwiftUI app with MVVM design pattern</h2>



<p>We have a small SwiftUI app where users can:</p>



<ul class="wp-block-list">
<li>Add new <code>Item</code>s by name.</li>



<li>Remove the last item in the list.</li>



<li>See all items displayed in a scroll view.</li>
</ul>



<figure class="gb-block-image gb-block-image-609dc911"><img loading="lazy" decoding="async" width="2543" height="1247" class="gb-image gb-image-609dc911" src="http://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_demo_add_item.webp" alt="" title="ui_testing_demo_add_item" srcset="https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_demo_add_item.webp 2543w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_demo_add_item-300x147.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_demo_add_item-1024x502.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_demo_add_item-768x377.webp 768w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_demo_add_item-1536x753.webp 1536w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_demo_add_item-2048x1004.webp 2048w" sizes="auto, (max-width: 2543px) 100vw, 2543px" /></figure>



<div style="height:41px" aria-hidden="true" class="wp-block-spacer"></div>



<p>All state is handled by an <code>ItemViewModel</code>, which looks like this:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="final class ItemViewModel: ObservableObject {

    @Published var items: [Item] = Item.examples
    @Published var deleteDisabled = false

    func deleteLastItem() {
        guard items.isEmpty == false else { return }
        items.removeLast()
        deleteDisabled = items.isEmpty
    }

    func addItem(name: String) {
        items.append(Item(name: name))
        deleteDisabled = items.isEmpty
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">final</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">class</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">ItemViewModel</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">ObservableObject </span><span style="color: #F6F6F4">{</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@Published</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> items: [Item] </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> Item.examples</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@Published</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> deleteDisabled </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">false</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">deleteLastItem</span><span style="color: #F6F6F4">() {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">guard</span><span style="color: #F6F6F4"> items.</span><span style="color: #BF9EEE">isEmpty</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">==</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">false</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">else</span><span style="color: #F6F6F4"> { </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> }</span></span>
<span class="line"><span style="color: #F6F6F4">        items.</span><span style="color: #97E1F1">removeLast</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        deleteDisabled </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> items.</span><span style="color: #BF9EEE">isEmpty</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">addItem</span><span style="color: #F6F6F4">(</span><span style="color: #62E884; font-style: italic">name</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4">) {</span></span>
<span class="line"><span style="color: #F6F6F4">        items.</span><span style="color: #97E1F1">append</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">Item</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">name</span><span style="color: #F6F6F4">: name))</span></span>
<span class="line"><span style="color: #F6F6F4">        deleteDisabled </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> items.</span><span style="color: #BF9EEE">isEmpty</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:22px" aria-hidden="true" class="wp-block-spacer"></div>



<p>It also exposes a flag <code>deleteDisabled</code> which reflects whether deletion should be possible.</p>



<p>These methods contain simple logic, but that logic can <strong>break silently</strong> when refactoring or adding new features. That’s why we write unit tests—to <strong>lock down behavior</strong> and catch breakages immediately.</p>



<p>This is a good starting point because:</p>



<ul class="wp-block-list">
<li>It has simple logic we can test.</li>



<li>It changes state in ways we can verify.</li>



<li>It doesn’t depend on UI or system APIs.</li>
</ul>



<div style="height:46px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Test 1: Adding an Item Should Increase the Count</h3>



<p>We want to test that calling <code>addItem(name:)</code> correctly updates the <code>items</code> array.</p>



<p>We’re going to follow a clean test structure following “Given-When-Then”:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="func test_add_new_item() {
    // --- GIVEN ---
    //  the initial state

    // --- WHEN ---
    // the action being tested    

    // --- THEN ---
    // the expected result
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">test_add_new_item</span><span style="color: #F6F6F4">() {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- GIVEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">//  the initial state</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- WHEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// the action being tested    </span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- THEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// the expected result</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:27px" aria-hidden="true" class="wp-block-spacer"></div>



<h4 class="wp-block-heading">What are we testing?</h4>



<ul class="wp-block-list">
<li>That the item count increases by 1</li>



<li>That the last item in the list matches the name we added</li>
</ul>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="func test_add_new_item() {
    // --- GIVEN ---
    let vm = ItemViewModel()
    let initialCount = vm.items.count
    let expectedName = &quot;Test Item&quot;

    // --- WHEN ---
    vm.addItem(name: expectedName)

    // --- THEN ---
    XCTAssertEqual(vm.items.count, initialCount + 1, 
                   &quot;Item cound should be increased by onw&quot;)
    XCTAssertEqual(vm.items.last?.name, expectedName, 
                   &quot;Expected that the last items title is the new items title&quot;)
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">test_add_new_item</span><span style="color: #F6F6F4">() {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- GIVEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> vm </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">ItemViewModel</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> initialCount </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> vm.items.</span><span style="color: #BF9EEE">count</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> expectedName </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Test Item</span><span style="color: #DEE492">&quot;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- WHEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    vm.</span><span style="color: #97E1F1">addItem</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">name</span><span style="color: #F6F6F4">: expectedName)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- THEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">XCTAssertEqual</span><span style="color: #F6F6F4">(vm.items.</span><span style="color: #BF9EEE">count</span><span style="color: #F6F6F4">, initialCount </span><span style="color: #F286C4">+</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">1</span><span style="color: #F6F6F4">, </span></span>
<span class="line"><span style="color: #F6F6F4">                   </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Item cound should be increased by onw</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">XCTAssertEqual</span><span style="color: #F6F6F4">(vm.items.</span><span style="color: #BF9EEE">last</span><span style="color: #F286C4">?</span><span style="color: #F6F6F4">.name, expectedName, </span></span>
<span class="line"><span style="color: #F6F6F4">                   </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Expected that the last items title is the new items title</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:23px" aria-hidden="true" class="wp-block-spacer"></div>



<p>This catches mistakes like forgetting to append the item or accidentally overwriting the array. It&#8217;s your first line of defense when logic changes.</p>



<div style="height:23px" aria-hidden="true" class="wp-block-spacer"></div>



<h4 class="wp-block-heading">Making Tests more Resilient for Future Refactoring</h4>



<p>When writing tests, you should try to write them in a flexibile way so that they will pass in the future. To explain this look at this test implementation:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="func test_add_new_item() {
    // --- GIVEN ---
    let vm = ItemViewModel()

    // --- WHEN ---
    vm.addItem(name: &quot;Test Item&quot;)

    // --- THEN ---
    XCTAssertEqual(vm.items.count, 4, 
                   &quot;Item cound should be increased by one&quot;)
    XCTAssertEqual(vm.items.last?.name, &quot;Test Item&quot;, 
                   &quot;Expected that the last items title is the new items title&quot;)
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">test_add_new_item</span><span style="color: #F6F6F4">() {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- GIVEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> vm </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">ItemViewModel</span><span style="color: #F6F6F4">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- WHEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    vm.</span><span style="color: #97E1F1">addItem</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">name</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Test Item</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- THEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">XCTAssertEqual</span><span style="color: #F6F6F4">(vm.items.</span><span style="color: #BF9EEE">count</span><span style="color: #F6F6F4">, </span><span style="color: #BF9EEE">4</span><span style="color: #F6F6F4">, </span></span>
<span class="line"><span style="color: #F6F6F4">                   </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Item cound should be increased by one</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">XCTAssertEqual</span><span style="color: #F6F6F4">(vm.items.</span><span style="color: #BF9EEE">last</span><span style="color: #F286C4">?</span><span style="color: #F6F6F4">.name, </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Test Item</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">, </span></span>
<span class="line"><span style="color: #F6F6F4">                   </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Expected that the last items title is the new items title</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<p>I set the new item count to 4. Which works as long as i start with exactly 3 items. But what happens when in the future I change the starting point and have a larger intitial items array? &#8211; You guessed it. The hardcode value of 4 will fail the test. This would be a false alarm, if the actuall behavior is still correct. In order to avoid false alarms, try to avoid hard coded assertions like we did in the above example and use more flexible assertions like:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="    let vm = ItemViewModel()
    let expectedCount = vm.iems.count - 1" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> vm </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">ItemViewModel</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> expectedCount </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> vm.iems.</span><span style="color: #BF9EEE">count</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">-</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">1</span></span></code></pre></div>



<div style="height:46px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Test 2: Deleting the Last Item Should Remove It</h3>



<p>We want to ensure that <code>deleteLastItem()</code> removes exactly one item and that the correct one is removed.</p>



<p>What are we testing?</p>



<ul class="wp-block-list">
<li>That the count is reduced by 1</li>



<li>That the item previously at the end is no longer in the list</li>
</ul>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="func test_delete_last_item() {
    // --- GIVEN ---
    let vm = ItemViewModel()
    let initialItems = vm.items
    let lastItem = initialItems.last
    let expectedCount = initialItems.count - 1

    // --- WHEN ---
    vm.deleteLastItem()

    // --- THEN ---
    XCTAssertEqual(vm.items.count, expectedCount, 
                  &quot;Expected that the item count is lowered by one&quot;)
    XCTAssertFalse(vm.items.contains(where: { $0.id == lastItem?.id }), 
                  &quot;Should have deleted last item&quot;)
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">test_delete_last_item</span><span style="color: #F6F6F4">() {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- GIVEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> vm </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">ItemViewModel</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> initialItems </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> vm.items</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> lastItem </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> initialItems.</span><span style="color: #BF9EEE">last</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> expectedCount </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> initialItems.</span><span style="color: #BF9EEE">count</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">-</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">1</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- WHEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    vm.</span><span style="color: #97E1F1">deleteLastItem</span><span style="color: #F6F6F4">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- THEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">XCTAssertEqual</span><span style="color: #F6F6F4">(vm.items.</span><span style="color: #BF9EEE">count</span><span style="color: #F6F6F4">, expectedCount, </span></span>
<span class="line"><span style="color: #F6F6F4">                  </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Expected that the item count is lowered by one</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">XCTAssertFalse</span><span style="color: #F6F6F4">(vm.items.</span><span style="color: #97E1F1">contains</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">where</span><span style="color: #F6F6F4">: { </span><span style="color: #BF9EEE; font-style: italic">$0</span><span style="color: #F6F6F4">.id </span><span style="color: #F286C4">==</span><span style="color: #F6F6F4"> lastItem</span><span style="color: #F286C4">?</span><span style="color: #F6F6F4">.id }), </span></span>
<span class="line"><span style="color: #F6F6F4">                  </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Should have deleted last item</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:17px" aria-hidden="true" class="wp-block-spacer"></div>



<p>If you change how deletion is implemented—say, filtering instead of removing the last—you might introduce subtle bugs. This test guards against that.</p>



<div style="height:39px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Test 3: Deleting All Items Should Disable the Delete Button</h3>



<p>This one checks the <code>deleteDisabled</code> flag. When the list becomes empty, the delete button in the UI should be disabled. This flag is driven by internal logic, not UI state.</p>



<p>What are we testing?</p>



<ul class="wp-block-list">
<li>That <code>deleteDisabled</code> becomes <code>true</code> when the list is empty</li>
</ul>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="func test_when_all_items_deleted_then_delete_is_disabled() {
    // --- GIVEN ---
    let vm = ItemViewModel()

    // --- WHEN ---
    while !vm.items.isEmpty {
        vm.deleteLastItem()
    }

    // --- THEN ---
    XCTAssertTrue(vm.deleteDisabled, &quot;Delete should be disabled for empty items array&quot;)
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">test_when_all_items_deleted_then_delete_is_disabled</span><span style="color: #F6F6F4">() {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- GIVEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> vm </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">ItemViewModel</span><span style="color: #F6F6F4">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- WHEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">while</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">!</span><span style="color: #F6F6F4">vm.items.</span><span style="color: #BF9EEE">isEmpty</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">        vm.</span><span style="color: #97E1F1">deleteLastItem</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- THEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">XCTAssertTrue</span><span style="color: #F6F6F4">(vm.deleteDisabled, </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Delete should be disabled for empty items array</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:28px" aria-hidden="true" class="wp-block-spacer"></div>



<p>This test is protecting <strong>user experience</strong>. If <code>deleteDisabled</code> isn’t set correctly, the delete button could remain active and trigger unexpected behavior in the UI.</p>



<div style="height:35px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Test 4: Adding an Empty Name Is Allowed (for Now)</h3>



<p>This test checks a behavioral detail: should the ViewModel accept empty strings as item names? Currently, yes. We want to write a test that defines and locks in this behavior.</p>



<p>What are we testing?</p>



<ul class="wp-block-list">
<li>That items with empty names are accepted and added to the list</li>
</ul>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="func test_add_empty_name_item_is_allowed() {
    // --- GIVEN ---
    let vm = ItemViewModel()

    // --- WHEN ---
    vm.addItem(name: &quot;&quot;)

    // --- THEN ---
    XCTAssertEqual(vm.items.last?.name, &quot;&quot;, 
                   &quot;Should be allowed to add an item with emppty name field&quot;)
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">test_add_empty_name_item_is_allowed</span><span style="color: #F6F6F4">() {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- GIVEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> vm </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">ItemViewModel</span><span style="color: #F6F6F4">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- WHEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    vm.</span><span style="color: #97E1F1">addItem</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">name</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- THEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">XCTAssertEqual</span><span style="color: #F6F6F4">(vm.items.</span><span style="color: #BF9EEE">last</span><span style="color: #F286C4">?</span><span style="color: #F6F6F4">.name, </span><span style="color: #DEE492">&quot;&quot;</span><span style="color: #F6F6F4">, </span></span>
<span class="line"><span style="color: #F6F6F4">                   </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Should be allowed to add an item with emppty name field</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:24px" aria-hidden="true" class="wp-block-spacer"></div>



<p>Even if this seems trivial, it locks in expected behavior. Later, if you add input validation, this test will fail—and that’s exactly what you want: clear visibility into changed assumptions.</p>



<p>That’s a solid principle—and yes, Vladimir Khorikov’s approach (from his <em>Unit Testing</em> book) is pragmatic and aligns well with long-term maintainability: <strong>don’t test everything—test what matters</strong>.</p>



<p>Let’s revise and expand the <strong>“What to test”</strong> section using that mindset:</p>



<div style="height:61px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">What to Test: Focus on Logic That Matters</h2>



<p>You don’t need to test everything. You need to test what can <strong>hurt you</strong>.</p>



<p>A good rule of thumb (inspired by <em>Vladimir Khorikov’s Unit Testing</em> book):</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Test code that is high in complexity and/or high in business value.</strong><br><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Skip code that is simple and stable or low-value.</p>
</blockquote>



<div class="wp-block-group has-background" style="background-color:#e1e4eb"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<h4 class="wp-block-heading">High Business Value</h4>



<p>This is logic that the <strong>user or product owner deeply cares about</strong>. If it breaks, the app loses trust.</p>



<p><strong>Examples in your app:</strong></p>



<ul class="wp-block-list">
<li>Adding and removing items from the list</li>



<li>Disabling the delete button at the right time</li>



<li>Any decision that changes user-visible state</li>
</ul>



<p>These are worth testing even if they look simple—because the <em>behavior</em> is valuable.</p>
</div></div>



<div style="height:16px" aria-hidden="true" class="wp-block-spacer"></div>



<div class="wp-block-group has-background" style="background-color:#e1e4eb"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<h4 class="wp-block-heading">High Complexity</h4>



<p>This is code that is tricky to get right or easy to misunderstand. Even if it’s not mission-critical, complexity makes it fragile.</p>



<p><strong>Examples to test:</strong></p>



<ul class="wp-block-list">
<li>Conditions like <code>guard items.isEmpty == false else { return }</code></li>



<li>Derived state like <code>deleteDisabled = items.isEmpty</code></li>



<li>Loops, multiple branches, or custom algorithms</li>
</ul>
</div></div>



<div style="height:22px" aria-hidden="true" class="wp-block-spacer"></div>



<h4 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Don’t Waste Time On&#8230;</h4>



<ul class="wp-block-list">
<li>Getters/setters or trivial code</li>



<li>Things you don’t control (like Swift’s standard library)</li>
</ul>



<p>In short: test <strong>where bugs hide</strong> and <strong>where logic changes break behavior</strong>.<br>You’ll write fewer tests—but they’ll do more work.</p>



<div style="height:53px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">The Real Problems You’ll Face in Bigger Apps</h2>



<p>Let’s be honest: the app we tested here is very simple. &#8211; In real apps, testing gets a lot harder. And that’s probably why many teams skip it or give up after writing a few tests.</p>



<h4 class="wp-block-heading">Why?</h4>



<p>Because <strong>real code touches more than just arrays in memory</strong>. It saves to disk, talks to APIs, handles background tasks, shows alerts, and reacts to user actions. That’s where the pain starts.</p>



<p>Here are the real-world problems you&#8217;ll hit:</p>



<div class="wp-block-group has-background" style="background-color:#e1e4eb"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<h4 class="wp-block-heading">Shared State Between Tests</h4>



<p><strong>What it is:</strong><br>When one test changes something that affects another test. For example, if your ViewModel saved items to disk and you didn&#8217;t clean up after the test, the next test would see that data—even though it shouldn&#8217;t.</p>



<p><strong>Why it&#8217;s a problem:</strong><br>Tests should run in any order and still pass. If they don’t, they’re called <strong>flaky tests</strong>—they pass sometimes, fail other times. You can’t trust them.</p>
</div></div>



<div style="height:17px" aria-hidden="true" class="wp-block-spacer"></div>



<div class="wp-block-group has-background" style="background-color:#e1e4eb"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<h4 class="wp-block-heading">Tests Are Too Fake</h4>



<p><strong>What it is:</strong><br>Many tutorials suggest replacing real things (like network calls) with fake ones. This is called <strong>mocking</strong>. But if your fake doesn’t behave like the real thing, you’re not testing your real app—you’re testing a lie.</p>



<p><strong>Example:</strong><br>You mock a network call to return a clean JSON, but the real API sometimes fails, or sends unexpected data. Your tests still pass, but your app crashes. That’s worse than no test.</p>
</div></div>



<div style="height:17px" aria-hidden="true" class="wp-block-spacer"></div>



<div class="wp-block-group has-background" style="background-color:#e1e4eb"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<h4 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4f1.png" alt="📱" class="wp-smiley" style="height: 1em; max-height: 1em;" /> UI Logic Isn’t Tested</h4>



<p><strong>What it is:</strong><br>Most bugs happen in the UI layer—wrong button state, missing sheet, alert doesn’t show, tap does nothing. But many iOS devs avoid testing views because it&#8217;s “hard” or “not pure enough.”</p>



<p><strong>Reality:</strong><br>Skipping UI logic tests means skipping the bugs your users actually see.</p>
</div></div>



<div style="height:45px" aria-hidden="true" class="wp-block-spacer"></div>



<h4 class="wp-block-heading">You Can Still Use Real Code in Tests</h4>



<p><strong>You don’t need to mock everything.</strong> Sometimes using the real system is the better choice—especially for things <strong>you control</strong>.</p>



<p><strong>For example:</strong><br>Saving items to disk?<br>Use <code>FileManager.temporaryDirectory</code> in your tests.<br>That way you catch real issues (permissions, file not found, codable, etc.)<br>And you don’t affect other tests—as long as you clean up.</p>



<div style="height:45px" aria-hidden="true" class="wp-block-spacer"></div>



<h4 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4da.png" alt="📚" class="wp-smiley" style="height: 1em; max-height: 1em;" /> What You Can Learn Next</h4>



<p>Once you&#8217;re past basic unit tests, here are good next steps—each could be its own follow-up post:</p>



<ol class="wp-block-list">
<li><p><strong>Testing file saving using temporary files</strong><br>– Real file system, isolated and safe<strong>Using Core Data in tests with an in-memory store</strong><br>– No need to mock your model layer</p></li>



<li><p><strong>Testing async code (networking, debouncing)</strong><br>– Use <code>XCTestExpectation</code> or <code>async/await</code></p></li>



<li><p><strong>Testing SwiftUI view logic</strong><br>– Don’t test layout, but do test state and actions</p></li>



<li><p><strong>What shared state is and how to avoid flaky tests</strong><br>– Deep dive into test isolation</p></li>
</ol>



<p>Testing is easy when everything lives in memory.<br>It gets valuable—and tricky—when real-world stuff comes in.</p>



<div style="height:32px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">Summary</h2>



<p>In this post, you learned how to:</p>



<ul class="wp-block-list">
<li>Use <strong>XCTest</strong> to write unit tests for a SwiftUI ViewModel</li>



<li>Structure your tests using the <strong>GIVEN / WHEN / THEN</strong> format</li>



<li>Focus on <strong>business logic</strong> and <strong>state changes</strong>, not just code coverage</li>



<li>Handle <strong>edge cases</strong> and verify behavior that matters to users</li>



<li>Understand the basics of <strong>what to test</strong>, <strong>what not to test</strong>, and <strong>why testing gets harder in real apps</strong></li>
</ul>



<p>Don’t aim for perfect tests. &#8211; Aim for <strong>useful tests</strong> that protect you from regressions and crashes.</p>



<p>Most bugs live in the UI. Now that your ViewModel is covered, it’s time to test what users actually see and tap.<br><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f449.png" alt="👉" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Up next: <strong><a href="XCUITest: How to Write UI Tests for SwiftUI Apps">XCUITest: How to Write UI Tests for SwiftUI Apps</a></strong></p>
<p>The post <a rel="nofollow" href="https://www.swiftyplace.com/blog/unit-testing-xctest-swiftui">Introduction to XCTest: How to Write Unit Tests for iOS apps</a> appeared first on <a rel="nofollow" href="https://www.swiftyplace.com">swiftyplace</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.swiftyplace.com/blog/unit-testing-xctest-swiftui/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>XCUITest: How to Write UI Tests for SwiftUI Apps</title>
		<link>https://www.swiftyplace.com/blog/xcuitest-ui-testing-swiftui?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=xcuitest-ui-testing-swiftui</link>
					<comments>https://www.swiftyplace.com/blog/xcuitest-ui-testing-swiftui#respond</comments>
		
		<dc:creator><![CDATA[Karin Prater]]></dc:creator>
		<pubDate>Mon, 19 May 2025 14:10:55 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://www.swiftyplace.com/?p=1005567</guid>

					<description><![CDATA[<p>If you’ve ever shipped a bug where a button did nothing, a sheet didn’t show, or the wrong item appeared on screen—this post is for you. Most iOS app bugs happen in the UI layer. ... <a title="XCUITest: How to Write UI Tests for SwiftUI Apps" class="read-more" href="https://www.swiftyplace.com/blog/xcuitest-ui-testing-swiftui" aria-label="More on XCUITest: How to Write UI Tests for SwiftUI Apps">Read more</a></p>
<p>The post <a rel="nofollow" href="https://www.swiftyplace.com/blog/xcuitest-ui-testing-swiftui">XCUITest: How to Write UI Tests for SwiftUI Apps</a> appeared first on <a rel="nofollow" href="https://www.swiftyplace.com">swiftyplace</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>If you’ve ever shipped a bug where a button did nothing, a sheet didn’t show, or the wrong item appeared on screen—this post is for you.</p>



<p>Most iOS app bugs happen in the UI layer. Unit tests won’t catch them, and manual testing is slow and unreliable. That’s where <strong>UI tests</strong> come in.</p>



<p>In this post, you’ll learn how to:</p>



<ul class="wp-block-list">
<li>Write your first <strong>UI test</strong> for a SwiftUI app using <strong>XCUITest</strong></li>



<li>Use <strong>accessibility identifiers</strong> to reliably find and interact with views</li>



<li>Assert that your app behaves correctly—just like a real user would</li>
</ul>



<p>You’ll use a simple SwiftUI app as an example, but everything here also applies to <strong>UIKit</strong> apps. XCUITest works across both.</p>



<p>These are <strong>real iOS tests</strong>—they launch the full app in a simulator and simulate actual user interaction.</p>



<p>Let’s dive in.</p>



<p><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2b07.png" alt="⬇" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Project files: <a href="https://github.com/gahntpo/iOS-testing" target="_blank" rel="noopener">https://github.com/gahntpo/iOS-testing</a></p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<h2 class="wp-block-heading">Why UI Tests Matter</h2>



<p>Even if you have 100% unit test coverage, your app can still break:</p>



<ul class="wp-block-list">
<li>Buttons wired to the wrong action</li>



<li>Views not showing because of missing state</li>



<li>Sheets or alerts not appearing</li>



<li>Broken navigation</li>
</ul>



<p>UI tests catch these problems because they run the <strong>real app</strong>, from launch to user interaction.</p>



<p>They’re especially powerful for:</p>



<ul class="wp-block-list">
<li>Testing happy paths from the user’s point of view</li>



<li>Preventing regressions in tap/gesture flow</li>



<li>Validating dynamic UI behaviour (like showing/hiding buttons or disabling inputs)</li>
</ul>



<p>You don’t need to test every pixel—just the logic that connects user interaction to state.</p>
</div></div>



<h2 class="wp-block-heading">Setting Up UI Testing in Xcode</h2>



<p>You can start with a new Xcode project, or use your existing one.</p>



<h3 class="wp-block-heading">Option 1: Create a New Project with Tests Enabled</h3>



<p>When creating a new iOS project, for the  Testing System sections choose either  Swift Testing or XCTest. Note that UI tests only work with XCTest.</p>



<p>This creates:</p>



<ul class="wp-block-list">
<li>A <strong>unit test target</strong></li>



<li>A <strong>UI test target</strong></li>
</ul>



<figure class="gb-block-image gb-block-image-52bc75e3"><img loading="lazy" decoding="async" width="1464" height="1064" class="gb-image gb-image-52bc75e3" src="http://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_1.webp" alt="add swift testing wih xctest when creating a new project in Xcode" title="unit_testing_1" srcset="https://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_1.webp 1464w, https://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_1-300x218.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_1-1024x744.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_1-768x558.webp 768w" sizes="auto, (max-width: 1464px) 100vw, 1464px" /></figure>



<div style="height:52px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Option 2: Add a UI Test Target to an Existing Project</h3>



<p>If you already have a working SwiftUI app (like the <code>ItemApp</code> from earlier), you can add a UI test target manually:</p>



<ol class="wp-block-list">
<li>In Xcode, go to <strong>File &gt; New &gt; Target</strong></li>



<li>Choose <strong>iOS &gt; UI Testing Bundle</strong></li>



<li>Name it something like <code>ItemAppUITests</code></li>



<li>Make sure it targets the right app</li>
</ol>



<p>Now you’ll see a new folder with a file like this:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="final class ItemAppUITests: XCTestCase {
    func testExample() {
        // your test will go here
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">final</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">class</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">ItemAppUITests</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">XCTestCase </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">testExample</span><span style="color: #F6F6F4">() {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// your test will go here</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:33px" aria-hidden="true" class="wp-block-spacer"></div>



<p>Great—you’re ready to start testing.</p>



<figure class="gb-block-image gb-block-image-dd83f971"><img loading="lazy" decoding="async" width="1899" height="996" class="gb-image gb-image-dd83f971" src="https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_xcode.webp" alt="" title="ui_testing_xcode" srcset="https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_xcode.webp 1899w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_xcode-300x157.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_xcode-1024x537.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_xcode-768x403.webp 768w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_xcode-1536x806.webp 1536w" sizes="auto, (max-width: 1899px) 100vw, 1899px" /></figure>



<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="gb-headline gb-headline-f4a44476 gb-headline-text">SwiftUI App Under Test</h2>



<p>This is a demo app, where you can see a list of items, add and delete them. </p>



<figure class="gb-block-image gb-block-image-64656445"><img loading="lazy" decoding="async" width="2543" height="1247" class="gb-image gb-image-64656445" src="http://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_demo_add_item.webp" alt="" title="ui_testing_demo_add_item" srcset="https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_demo_add_item.webp 2543w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_demo_add_item-300x147.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_demo_add_item-1024x502.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_demo_add_item-768x377.webp 768w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_demo_add_item-1536x753.webp 1536w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_demo_add_item-2048x1004.webp 2048w" sizes="auto, (max-width: 2543px) 100vw, 2543px" /></figure>



<div style="height:26px" aria-hidden="true" class="wp-block-spacer"></div>



<p>In a previous post, I used the same app example to write unit tests with swift testing. You can read about it <a href="https://www.swiftyplace.com/blog/unit-testing-ios-apps">here</a>.</p>



<div style="height:49px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">Adding Accessibility Identifiers</h2>



<p>To make views accessible to your UI tests, you need to add <strong>accessibility identifiers</strong> to them. These are string tags that XCUITest uses to find buttons, text fields, and other UI elements.<br>In the example app, you can see SwiftUI views like so:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="Button(&quot;Remove Last&quot;) {
    viewModel.deleteLastItem()
}
.buttonStyle(.borderedProminent)
.disabled(viewModel.deleteDisabled)" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #97E1F1">Button</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Remove Last</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) {</span></span>
<span class="line"><span style="color: #F6F6F4">    viewModel.</span><span style="color: #97E1F1">deleteLastItem</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span>
<span class="line"><span style="color: #F6F6F4">.</span><span style="color: #97E1F1">buttonStyle</span><span style="color: #F6F6F4">(.borderedProminent)</span></span>
<span class="line"><span style="color: #F6F6F4">.</span><span style="color: #97E1F1">disabled</span><span style="color: #F6F6F4">(viewModel.deleteDisabled)</span></span></code></pre></div>



<div style="height:21px" aria-hidden="true" class="wp-block-spacer"></div>



<p>This works, but XCUITest can’t find this button unless you give it a name. So we add:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code=".accessibilityIdentifier(&quot;button.delete&quot;)" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F6F6F4">.</span><span style="color: #97E1F1">accessibilityIdentifier</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">button.delete</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span></code></pre></div>



<div style="height:21px" aria-hidden="true" class="wp-block-spacer"></div>



<p>Repeat this for all important UI elements.</p>



<div style="height:35px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Recommended Naming: Unique and Clear</h3>



<p>Use a consistent naming scheme:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="&lt;screen&gt;.&lt;type&gt;.&lt;purpose&gt;" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">&lt;</span><span style="color: #F6F6F4">screen</span><span style="color: #F286C4">&gt;.&lt;</span><span style="color: #F6F6F4">type</span><span style="color: #F286C4">&gt;.&lt;</span><span style="color: #F6F6F4">purpose</span><span style="color: #F286C4">&gt;</span></span></code></pre></div>



<div style="height:35px" aria-hidden="true" class="wp-block-spacer"></div>



<p>Why?</p>



<ul class="wp-block-list">
<li>It prevents conflicts between views (e.g. two buttons named <code>"add"</code>)</li>



<li>It makes it clear <strong>where</strong> the element lives</li>



<li>It scales well as your app grows</li>
</ul>



<div style="height:35px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Examples</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Identifier</th><th>Meaning</th></tr></thead><tbody><tr><td><code>"AddNewItemScreen.button.confirm”</code></td><td>Add button on the item creation screen</td></tr><tr><td><code>"AddNewItemScreen.textfield.newItem"</code></td><td>Text input field for new item name</td></tr><tr><td><code>“<code>ItemListScreen</code>.button.add”</code></td><td>Add button on item list screen</td></tr><tr><td><code>“<code>ItemListScreen</code>.button.delete”</code></td><td>Delete last item button</td></tr><tr><td><code>"ItemListScreen.view.itemList"</code></td><td>Main scroll view showing all items</td></tr><tr><td><code>"ItemListScreen.item.&lt;itemName&gt;"</code></td><td>A visible item in the list (dynamic)</td></tr></tbody></table></figure>



<div style="height:45px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Pro Tip: Use a Central enum to Store Them</h3>



<p>Hardcoding strings across your app and test files is a recipe for broken tests.</p>



<p>Instead, define identifiers once in a shared file:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="enum UIIdentifiers {
    enum AddNewItemScreen {
        static let addButton = &quot;AddNewItemScreen.button.add&quot;
        static let itemNameTextField = &quot;AddNewItemScreen.textfield.newItem&quot;
        static let confirmButton = &quot;AddNewItemScreen.button.confirm&quot;
    }

    enum ItemListScreen {
        static let itemListView = &quot;ItemListScreen.view.itemList&quot;
        static let deleteButton = &quot;ItemListScreen.button.delete&quot;
        static let addButton = &quot;ItemListScreen.button.add&quot;
        static func item(_ name: String) -&gt; String {
            &quot;ItemListScreen.item.(name)&quot;
        }
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">enum</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">UIIdentifiers</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">enum</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">AddNewItemScreen</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">static</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> addButton </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">AddNewItemScreen.button.add</span><span style="color: #DEE492">&quot;</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">static</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> itemNameTextField </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">AddNewItemScreen.textfield.newItem</span><span style="color: #DEE492">&quot;</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">static</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> confirmButton </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">AddNewItemScreen.button.confirm</span><span style="color: #DEE492">&quot;</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">enum</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ItemListScreen</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">static</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> itemListView </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">ItemListScreen.view.itemList</span><span style="color: #DEE492">&quot;</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">static</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> deleteButton </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">ItemListScreen.button.delete</span><span style="color: #DEE492">&quot;</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">static</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> addButton </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">ItemListScreen.button.add</span><span style="color: #DEE492">&quot;</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">static</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">item</span><span style="color: #F6F6F4">(</span><span style="color: #62E884">_</span><span style="color: #F6F6F4"> </span><span style="color: #FFB86C; font-style: italic">name</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4">) </span><span style="color: #F286C4">-&gt;</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">ItemListScreen.item.(name)</span><span style="color: #DEE492">&quot;</span></span>
<span class="line"><span style="color: #F6F6F4">        }</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:31px" aria-hidden="true" class="wp-block-spacer"></div>



<p>This makes your UI tests:</p>



<ul class="wp-block-list">
<li>Easier to write</li>



<li>Easier to refactor</li>



<li>Less likely to break from typo bugs</li>
</ul>



<p>Make sure to add this file to both your main app and ui testing target, so you can access it for you tests.</p>



<figure class="gb-block-image gb-block-image-094b41fe"><img loading="lazy" decoding="async" width="1863" height="951" class="gb-image gb-image-094b41fe" src="http://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_identifiers.webp" alt="adding accessibiliy identifiers to xcode for xcuitests" title="ui_testing_identifiers" srcset="https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_identifiers.webp 1863w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_identifiers-300x153.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_identifiers-1024x523.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_identifiers-768x392.webp 768w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_identifiers-1536x784.webp 1536w" sizes="auto, (max-width: 1863px) 100vw, 1863px" /></figure>



<p>Next, I’ll show you how to update your actual SwiftUI views to use these identifiers—and then we’ll write your first UI test.</p>



<div style="height:45px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">Updating the SwiftUI Views with Accessibility Identifiers</h2>



<p>Add identifiers to ContentView for the scrollview items, delete and add buttons:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);--cbp-line-highlight-color:rgba(251, 251, 239, 0.2);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="import SwiftUI

struct ContentView: View {

    @ObservedObject var viewModel: ItemViewModel
    @State private var showingAddSheet = false

    var body: some View {
        VStack {
            ScrollView {
                ForEach(viewModel.items) { item in
                    Text(item.name)
                        .padding()
                        .frame(maxWidth: .infinity)
                        .background(Capsule().fill(Color.yellow))
                        .accessibilityIdentifier(UIIdentifiers.ItemListScreen.item(item.name))
                }
            }
            .accessibilityIdentifier(UIIdentifiers.ItemListScreen.itemListView)

            Divider()

            HStack {
                Button(&quot;Remove Last&quot;) {
                    viewModel.deleteLastItem()
                }
                .buttonStyle(.borderedProminent)
                .disabled(viewModel.deleteDisabled)
                .accessibilityIdentifier(UIIdentifiers.ItemListScreen.deleteButton)

                Button {
                    showingAddSheet = true
                } label: {
                    Label(&quot;Add Item&quot;, systemImage: &quot;plus&quot;)
                }
                .buttonStyle(.bordered)
                .accessibilityIdentifier(UIIdentifiers.AddNewItemScreen.addButton)
                .sheet(isPresented: $showingAddSheet) {
                    NewItemView(viewModel: viewModel)
                }
            }
        }
        .padding()
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">SwiftUI</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ContentView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@ObservedObject</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> viewModel: ItemViewModel</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@State</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">private</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> showingAddSheet </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">false</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">VStack</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">ScrollView</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #97E1F1">ForEach</span><span style="color: #F6F6F4">(viewModel.items) { item </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #97E1F1">Text</span><span style="color: #F6F6F4">(item.name)</span></span>
<span class="line"><span style="color: #F6F6F4">                        .</span><span style="color: #97E1F1">padding</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">                        .</span><span style="color: #97E1F1">frame</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">maxWidth</span><span style="color: #F6F6F4">: .</span><span style="color: #BF9EEE">infinity</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">                        .</span><span style="color: #97E1F1">background</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">Capsule</span><span style="color: #F6F6F4">().</span><span style="color: #97E1F1">fill</span><span style="color: #F6F6F4">(Color.yellow))</span></span>
<span class="line cbp-line-highlight"><span style="color: #F6F6F4">                        .</span><span style="color: #97E1F1">accessibilityIdentifier</span><span style="color: #F6F6F4">(UIIdentifiers.ItemListScreen.</span><span style="color: #97E1F1">item</span><span style="color: #F6F6F4">(item.name))</span></span>
<span class="line"><span style="color: #F6F6F4">                }</span></span>
<span class="line"><span style="color: #F6F6F4">            }</span></span>
<span class="line cbp-line-highlight"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">accessibilityIdentifier</span><span style="color: #F6F6F4">(UIIdentifiers.ItemListScreen.itemListView)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">Divider</span><span style="color: #F6F6F4">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">HStack</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #97E1F1">Button</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Remove Last</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) {</span></span>
<span class="line"><span style="color: #F6F6F4">                    viewModel.</span><span style="color: #97E1F1">deleteLastItem</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">                }</span></span>
<span class="line"><span style="color: #F6F6F4">                .</span><span style="color: #97E1F1">buttonStyle</span><span style="color: #F6F6F4">(.borderedProminent)</span></span>
<span class="line"><span style="color: #F6F6F4">                .</span><span style="color: #97E1F1">disabled</span><span style="color: #F6F6F4">(viewModel.deleteDisabled)</span></span>
<span class="line cbp-line-highlight"><span style="color: #F6F6F4">                .</span><span style="color: #97E1F1">accessibilityIdentifier</span><span style="color: #F6F6F4">(UIIdentifiers.ItemListScreen.deleteButton)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #97E1F1">Button</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">                    showingAddSheet </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">true</span></span>
<span class="line"><span style="color: #F6F6F4">                } </span><span style="color: #97E1F1">label</span><span style="color: #F6F6F4">: {</span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #97E1F1">Label</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Add Item</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">, </span><span style="color: #97E1F1">systemImage</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">plus</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">                }</span></span>
<span class="line"><span style="color: #F6F6F4">                .</span><span style="color: #97E1F1">buttonStyle</span><span style="color: #F6F6F4">(.bordered)</span></span>
<span class="line cbp-line-highlight"><span style="color: #F6F6F4">                .</span><span style="color: #97E1F1">accessibilityIdentifier</span><span style="color: #F6F6F4">(UIIdentifiers.AddNewItemScreen.addButton)</span></span>
<span class="line"><span style="color: #F6F6F4">                .</span><span style="color: #97E1F1">sheet</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">isPresented</span><span style="color: #F6F6F4">: $showingAddSheet) {</span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #97E1F1">NewItemView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">viewModel</span><span style="color: #F6F6F4">: viewModel)</span></span>
<span class="line"><span style="color: #F6F6F4">                }</span></span>
<span class="line"><span style="color: #F6F6F4">            }</span></span>
<span class="line"><span style="color: #F6F6F4">        }</span></span>
<span class="line"><span style="color: #F6F6F4">        .</span><span style="color: #97E1F1">padding</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:26px" aria-hidden="true" class="wp-block-spacer"></div>



<p>For NewItemView add identifiers to the textfield and button like so:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);--cbp-line-highlight-color:rgba(251, 251, 239, 0.2);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="struct NewItemView: View {

    @ObservedObject var viewModel: ItemViewModel
    @State private var newItemName = &quot;&quot;
    @Environment(.dismiss) var dismiss

    var body: some View {
        VStack(spacing: 20) {
            TextField(&quot;Item name&quot;, text: $newItemName)
                .padding()
                .textFieldStyle(.roundedBorder)
                .accessibilityIdentifier(UIIdentifiers.AddNewItemScreen.itemNameTextField)

            Button(&quot;Add Item&quot;) {
                if !newItemName.isEmpty {
                    viewModel.addItem(name: newItemName)
                    newItemName = &quot;&quot;
                    dismiss()
                }
            }
            .buttonStyle(.borderedProminent)
            .disabled(newItemName.isEmpty)
            .accessibilityIdentifier(UIIdentifiers.AddNewItemScreen.confirmButton)
        }
        .padding()
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">NewItemView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@ObservedObject</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> viewModel: ItemViewModel</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@State</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">private</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> newItemName </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;&quot;</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@Environment</span><span style="color: #F6F6F4">(.dismiss) </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> dismiss</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">VStack</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">spacing</span><span style="color: #F6F6F4">: </span><span style="color: #BF9EEE">20</span><span style="color: #F6F6F4">) {</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">TextField</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Item name</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">, </span><span style="color: #97E1F1">text</span><span style="color: #F6F6F4">: $newItemName)</span></span>
<span class="line"><span style="color: #F6F6F4">                .</span><span style="color: #97E1F1">padding</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">                .</span><span style="color: #97E1F1">textFieldStyle</span><span style="color: #F6F6F4">(.roundedBorder)</span></span>
<span class="line cbp-line-highlight"><span style="color: #F6F6F4">                .</span><span style="color: #97E1F1">accessibilityIdentifier</span><span style="color: #F6F6F4">(UIIdentifiers.AddNewItemScreen.itemNameTextField)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">Button</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Add Item</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) {</span></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #F286C4">if</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">!</span><span style="color: #F6F6F4">newItemName.</span><span style="color: #BF9EEE">isEmpty</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">                    viewModel.</span><span style="color: #97E1F1">addItem</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">name</span><span style="color: #F6F6F4">: newItemName)</span></span>
<span class="line"><span style="color: #F6F6F4">                    newItemName </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;&quot;</span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #97E1F1">dismiss</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">                }</span></span>
<span class="line"><span style="color: #F6F6F4">            }</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">buttonStyle</span><span style="color: #F6F6F4">(.borderedProminent)</span></span>
<span class="line cbp-line-highlight"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">disabled</span><span style="color: #F6F6F4">(newItemName.</span><span style="color: #BF9EEE">isEmpty</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">accessibilityIdentifier</span><span style="color: #F6F6F4">(UIIdentifiers.AddNewItemScreen.confirmButton)</span></span>
<span class="line"><span style="color: #F6F6F4">        }</span></span>
<span class="line"><span style="color: #F6F6F4">        .</span><span style="color: #97E1F1">padding</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<p>Now your app is testable: every important UI element is uniquely and clearly tagged. You’re ready to write your first <strong>UI test</strong>.</p>



<div style="height:67px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">Writing Your First SwiftUI UI Test with XCUITest</h2>



<p>We want to check:</p>



<ul class="wp-block-list">
<li>That the screen loads correctly</li>



<li>That we can locate the container (e.g., scroll view or list) of items</li>



<li>That the expected number of items is visible</li>
</ul>



<p>This test checks the happy path—the UI is working as expected and showing 4 predefined items.</p>



<div style="height:18px" aria-hidden="true" class="wp-block-spacer"></div>



<p>I will start by writing the test function where we test the shown items like so:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="func test_items_shown() {
    let app = XCUIApplication()
    app.launch()
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">test_items_shown</span><span style="color: #F6F6F4">() {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> app </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">XCUIApplication</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">    app.</span><span style="color: #97E1F1">launch</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:17px" aria-hidden="true" class="wp-block-spacer"></div>



<p>We create a new <code>XCUIApplication</code> and launch it. Every UI test starts from a clean app launch—no shared state, no leftovers from previous tests.</p>



<div style="height:32px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Run the Test</h3>



<p>You can now run it:</p>



<ul class="wp-block-list">
<li>Click the diamond <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/25b6.png" alt="▶" class="wp-smiley" style="height: 1em; max-height: 1em;" /> next to the test method</li>



<li>In the Test Navigator on the left, click the diamond for the corresponding test </li>



<li>Or press <code>⌘ + U</code> to run all tests</li>



<li>Use <code>⌘ + ⇧ + U</code> to build all tests without running</li>
</ul>



<figure class="gb-block-image gb-block-image-d0c80ac7"><img loading="lazy" decoding="async" width="1722" height="933" class="gb-image gb-image-d0c80ac7" src="https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_run.webp" alt="" title="ui_testing_run" srcset="https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_run.webp 1722w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_run-300x163.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_run-1024x555.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_run-768x416.webp 768w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_run-1536x832.webp 1536w" sizes="auto, (max-width: 1722px) 100vw, 1722px" /></figure>



<div style="height:54px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Locating the UI Elements</h3>



<p>You can ask the app instance for UI elements. For example, I can access views by their type and accessibility id. The following finds the scrollview by id:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="func test_items_shown() {
    let app = XCUIApplication()
    app.launch()
    let collection = app.scrollViews[&quot;items.collection&quot;]
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">test_items_shown</span><span style="color: #F6F6F4">() {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> app </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">XCUIApplication</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">    app.</span><span style="color: #97E1F1">launch</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> collection </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> app.scrollViews[</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">items.collection</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">]</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:32px" aria-hidden="true" class="wp-block-spacer"></div>



<p>But if you want to change your implementation in the future and change from a scroll view to e.g. a list, the test will fail. This would mean the test is <strong>brittle</strong> (breaks with future refactoring). A better more flexible approach is to look for elements that match the identifier like so:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="let id = UIIdentifiers.ItemListScreen.itemListView
let collection = app.descendants(matching: .any)[id]" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> id </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> UIIdentifiers.ItemListScreen.itemListView</span></span>
<span class="line"><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> collection </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> app.</span><span style="color: #97E1F1">descendants</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">matching</span><span style="color: #F6F6F4">: .any)[id]</span></span></code></pre></div>



<div style="height:22px" aria-hidden="true" class="wp-block-spacer"></div>



<p>Using <code>.descendants(matching: .any)[id]</code> to find any type of view by identifier. It’s <strong>more stable against refactors</strong>.</p>



<p>Another problem with XCUITest is that they can be <strong>flaky</strong> that means they randomly fail. One reason may be that the UI is not refreshing fast enough and the UI elemente have not appeared yet when you check the assertions. To solve this and prevent flaky tests you can use <code>.waitForExistence(timeout:)</code>. It makes the test more stable—it gives the UI a second to appear before failing.</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="func test_items_shown() {
    let app = XCUIApplication()
    app.launch()
    
    let id = UIIdentifiers.ItemListScreen.itemListView
    let collection = app.descendants(matching: .any)[id]
    XCTAssertTrue(collection.waitForExistence(timeout: 1), 
                 &quot;The items should be visible&quot;)
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">test_items_shown</span><span style="color: #F6F6F4">() {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> app </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">XCUIApplication</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">    app.</span><span style="color: #97E1F1">launch</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">    </span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> id </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> UIIdentifiers.ItemListScreen.itemListView</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> collection </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> app.</span><span style="color: #97E1F1">descendants</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">matching</span><span style="color: #F6F6F4">: .any)[id]</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">XCTAssertTrue</span><span style="color: #F6F6F4">(collection.</span><span style="color: #97E1F1">waitForExistence</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">timeout</span><span style="color: #F6F6F4">: </span><span style="color: #BF9EEE">1</span><span style="color: #F6F6F4">), </span></span>
<span class="line"><span style="color: #F6F6F4">                 </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">The items should be visible</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:22px" aria-hidden="true" class="wp-block-spacer"></div>



<p>Next I want to find the rows in the scrollview. I can use another search and define a predicate:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="    let itemId = UIIdentifiers.ItemListScreen.item(&quot;&quot;)
    let predicate = NSPredicate(format: &quot;identifier CONTAINS '\(itemId)'&quot;)
    let items = collection.descendants(matching: .any).matching(predicate)" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> itemId </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> UIIdentifiers.ItemListScreen.</span><span style="color: #97E1F1">item</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> predicate </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">NSPredicate</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">format</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">identifier CONTAINS &#39;</span><span style="color: #F286C4">\(</span><span style="color: #E7EE98">itemId</span><span style="color: #F286C4">)</span><span style="color: #E7EE98">&#39;</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> items </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> collection.</span><span style="color: #97E1F1">descendants</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">matching</span><span style="color: #F6F6F4">: .any).</span><span style="color: #97E1F1">matching</span><span style="color: #F6F6F4">(predicate)</span></span></code></pre></div>



<div style="height:22px" aria-hidden="true" class="wp-block-spacer"></div>



<p>Since each item has an identifier like <code>"ItemListScreen.item.first"</code> or <code>"ItemListScreen.item.second"</code>, we use a <strong>predicate</strong> to match anything that starts with <code>"ItemListScreen.item."</code>. This way, the test <strong>doesn’t depend on the exact names</strong>, just the count.</p>



<p>Thus the full test looks like:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="import XCTest

final class ItemAppUITests: XCTestCase {
    func test_items_shown() {
        let app = XCUIApplication()
        app.launch()

        let collection = app.descendants(matching: .any)[UIIdentifiers.ItemListScreen.itemListView]
        XCTAssertTrue(collection.waitForExistence(timeout: 1), 
                      &quot;The items should be visible&quot;)

        let itemId = UIIdentifiers.ItemListScreen.item(&quot;&quot;)
        let predicate = NSPredicate(format: &quot;identifier CONTAINS '(itemId)'&quot;)            
        let items = collection.descendants(matching: .any).matching(predicate)

        XCTAssertTrue(items.count &gt; 0, 
                      &quot;There should be at least one item on the screen&quot;)
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">XCTest</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">final</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">class</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">ItemAppUITests</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">XCTestCase </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">test_items_shown</span><span style="color: #F6F6F4">() {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> app </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">XCUIApplication</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        app.</span><span style="color: #97E1F1">launch</span><span style="color: #F6F6F4">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> collection </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> app.</span><span style="color: #97E1F1">descendants</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">matching</span><span style="color: #F6F6F4">: .any)[UIIdentifiers.ItemListScreen.itemListView]</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">XCTAssertTrue</span><span style="color: #F6F6F4">(collection.</span><span style="color: #97E1F1">waitForExistence</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">timeout</span><span style="color: #F6F6F4">: </span><span style="color: #BF9EEE">1</span><span style="color: #F6F6F4">), </span></span>
<span class="line"><span style="color: #F6F6F4">                      </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">The items should be visible</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> itemId </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> UIIdentifiers.ItemListScreen.</span><span style="color: #97E1F1">item</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> predicate </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">NSPredicate</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">format</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">identifier CONTAINS &#39;(itemId)&#39;</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)            </span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> items </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> collection.</span><span style="color: #97E1F1">descendants</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">matching</span><span style="color: #F6F6F4">: .any).</span><span style="color: #97E1F1">matching</span><span style="color: #F6F6F4">(predicate)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">XCTAssertTrue</span><span style="color: #F6F6F4">(items.</span><span style="color: #BF9EEE">count</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">&gt;</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">0</span><span style="color: #F6F6F4">, </span></span>
<span class="line"><span style="color: #F6F6F4">                      </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">There should be at least one item on the screen</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:29px" aria-hidden="true" class="wp-block-spacer"></div>



<p>This is a <strong>good assertion</strong> because:</p>



<ul class="wp-block-list">
<li>It&#8217;s flexible to future refactoring: we don&#8217;t expect exactly x items which might change in the future</li>



<li>It&#8217;s visible: if it fails, it tells you why</li>



<li>It avoids testing layout or view types—just the behaviour</li>
</ul>



<div style="height:61px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Attach a Screenshot (Bonus)</h3>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="    let screenshot = app.screenshot()
    let attachment = XCTAttachment(screenshot: screenshot)
    attachment.lifetime = .keepAlways
    add(attachment)" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> screenshot </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> app.</span><span style="color: #97E1F1">screenshot</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> attachment </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">XCTAttachment</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">screenshot</span><span style="color: #F6F6F4">: screenshot)</span></span>
<span class="line"><span style="color: #F6F6F4">    attachment.lifetime </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> .keepAlways</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">add</span><span style="color: #F6F6F4">(attachment)</span></span></code></pre></div>



<div style="height:23px" aria-hidden="true" class="wp-block-spacer"></div>



<p>Attaching a screenshot is helpful for debugging later—especially in CI or when a test fails unexpectedly.</p>



<p>Defaults to ~<a href="doc://com.apple.documentation/documentation/xctest/xctattachment/lifetime-swift.enum/deleteonsuccess" target="_blank" rel="noopener">XCTAttachment.Lifetime.deleteOnSuccess</a>~, indicating that the attachment should be discarded when its test passes successfully, to save on storage space. Set this property to ~<a href="doc://com.apple.documentation/documentation/xctest/xctattachment/lifetime-swift.enum/keepalways" target="_blank" rel="noopener">XCTAttachment.Lifetime.keepAlways</a>~ to persist an attachment even when its test passes.</p>



<p>To find the screenshots go to the <code>Test Navigator</code> and chose the test:</p>



<figure class="gb-block-image gb-block-image-91096511"><img loading="lazy" decoding="async" width="1884" height="965" class="gb-image gb-image-91096511" src="https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_results_1.webp" alt="" title="ui_testing_results_1" srcset="https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_results_1.webp 1884w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_results_1-300x154.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_results_1-1024x525.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_results_1-768x393.webp 768w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_results_1-1536x787.webp 1536w" sizes="auto, (max-width: 1884px) 100vw, 1884px" /></figure>



<p>You can also see how long the test runs. In the above example, the test takes 5sec. <br></p>



<figure class="gb-block-image gb-block-image-90fd26eb"><img loading="lazy" decoding="async" width="1799" height="1131" class="gb-image gb-image-90fd26eb" src="https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_results_2_screeshot.webp" alt="" title="ui_testing_results_2_screeshot" srcset="https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_results_2_screeshot.webp 1799w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_results_2_screeshot-300x189.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_results_2_screeshot-1024x644.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_results_2_screeshot-768x483.webp 768w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_results_2_screeshot-1536x966.webp 1536w" sizes="auto, (max-width: 1799px) 100vw, 1799px" /></figure>



<p>You can view the screenshot for later debugging. You can also export it. This can be useful if you want to automatically generate screenshots for app store connect.</p>



<div style="height:42px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">Testing Adding New Items</h2>



<p>In the next test I want to verify that:</p>



<ol class="wp-block-list">
<li>The &#8220;Add&#8221; button opens the new item sheet</li>



<li>The user can enter a name</li>



<li>Tapping &#8220;Add Item&#8221; closes the sheet</li>



<li>The new item appears in the list</li>
</ol>



<figure class="gb-block-image gb-block-image-1f391992"><img loading="lazy" decoding="async" width="2543" height="1247" class="gb-image gb-image-1f391992" src="https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_demo_add_item.webp" alt="" title="ui_testing_demo_add_item" srcset="https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_demo_add_item.webp 2543w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_demo_add_item-300x147.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_demo_add_item-1024x502.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_demo_add_item-768x377.webp 768w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_demo_add_item-1536x753.webp 1536w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_demo_add_item-2048x1004.webp 2048w" sizes="auto, (max-width: 2543px) 100vw, 2543px" /></figure>



<div style="height:29px" aria-hidden="true" class="wp-block-spacer"></div>



<p>This is a full <strong>user interaction flow</strong> test. It checks that the UI is wired up correctly and behaves as expected end-to-end.</p>



<p>Here’s your working test:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="import XCTest

final class ItemAppUITests: XCTestCase {

    func test_addItem_showsItemInList() {
        // --- GIVEN ---
        let app = XCUIApplication()
        app.launch()

        // --- WHEN ---
        app.buttons[UIIdentifiers.AddNewItemScreen.addButton].tap()

        let textField = app.textFields[UIIdentifiers.AddNewItemScreen.itemNameTextField]
        XCTAssertTrue(textField.waitForExistence(timeout: 1))
        textField.tap()
        textField.typeText(&quot;New Item&quot;)

        app.buttons[UIIdentifiers.AddNewItemScreen.confirmButton].tap()

        // --- THEN ---
        let newItem = app.staticTexts[UIIdentifiers.ItemListScreen.item(&quot;New Item&quot;)]
        XCTAssertTrue(newItem.waitForExistence(timeout: 2))
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">XCTest</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">final</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">class</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">ItemAppUITests</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">XCTestCase </span><span style="color: #F6F6F4">{</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">test_addItem_showsItemInList</span><span style="color: #F6F6F4">() {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// --- GIVEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> app </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">XCUIApplication</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        app.</span><span style="color: #97E1F1">launch</span><span style="color: #F6F6F4">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// --- WHEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">        app.buttons[UIIdentifiers.AddNewItemScreen.addButton].</span><span style="color: #97E1F1">tap</span><span style="color: #F6F6F4">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> textField </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> app.textFields[UIIdentifiers.AddNewItemScreen.itemNameTextField]</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">XCTAssertTrue</span><span style="color: #F6F6F4">(textField.</span><span style="color: #97E1F1">waitForExistence</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">timeout</span><span style="color: #F6F6F4">: </span><span style="color: #BF9EEE">1</span><span style="color: #F6F6F4">))</span></span>
<span class="line"><span style="color: #F6F6F4">        textField.</span><span style="color: #97E1F1">tap</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        textField.</span><span style="color: #97E1F1">typeText</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">New Item</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        app.buttons[UIIdentifiers.AddNewItemScreen.confirmButton].</span><span style="color: #97E1F1">tap</span><span style="color: #F6F6F4">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// --- THEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> newItem </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> app.staticTexts[UIIdentifiers.ItemListScreen.</span><span style="color: #97E1F1">item</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">New Item</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)]</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">XCTAssertTrue</span><span style="color: #F6F6F4">(newItem.</span><span style="color: #97E1F1">waitForExistence</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">timeout</span><span style="color: #F6F6F4">: </span><span style="color: #BF9EEE">2</span><span style="color: #F6F6F4">))</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:26px" aria-hidden="true" class="wp-block-spacer"></div>



<p>What This Test Does</p>



<ul class="wp-block-list">
<li><code>XCUIApplication().launch()</code> boots your app just like a user would</li>



<li>It taps the <strong>Add</strong> button (using the identifier) to open the sheet</li>



<li>In the sheet we locate the <code>"New Item TextField"</code></li>



<li>It enters <code>"New Item"</code> into the text field</li>



<li>It confirms the addition by taping the <code>"confirmButton"</code></li>



<li>It asserts that the item now exists in the list</li>
</ul>



<p>This is the most important part: verifying that the <strong>item actually appears on screen</strong>. Not just that the code ran—but that the user sees the result.<br>Using .waitForExistence(timeout:) here gives the UI a moment to update and avoids false negatives on slower devices or CI.</p>



<div style="height:42px" aria-hidden="true" class="wp-block-spacer"></div>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f6d1.png" alt="🛑" class="wp-smiley" style="height: 1em; max-height: 1em;" /> If the Test Fails</h3>



<p>If something breaks—wrong identifier, sheet not showing—you’ll see exactly where and why.<br>For example:</p>



<ul class="wp-block-list">
<li>Forgot to add <code>.accessibilityIdentifier</code>?</li>



<li>Sheet not presenting correctly?</li>



<li>ViewModel logic didn’t run?</li>
</ul>



<p>That’s exactly what UI tests are for—they catch <strong>real bugs</strong> that slip past unit tests.</p>



<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>
</div></div>



<h2 class="wp-block-heading">Test: Deleting All Items Disables the Delete Button</h2>



<p>In this test, we’ll simulate repeatedly tapping the <strong>&#8220;Remove Last&#8221;</strong> button until no items remain. Then we’ll assert that:</p>



<ul class="wp-block-list">
<li>The <strong>Delete</strong> button removes items from the list one by one</li>



<li>All items are eventually removed</li>



<li>The &#8220;Remove Last&#8221; button becomes <strong>disabled</strong> when the list is empty</li>



<li>This test verifies the entire &#8220;delete flow&#8221; and UI state update logic.</li>
</ul>



<figure class="gb-block-image gb-block-image-c3220a83"><img loading="lazy" decoding="async" width="1899" height="1247" class="gb-image gb-image-c3220a83" src="https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_demo_remove.webp" alt="" title="ui_testing_demo_remove" srcset="https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_demo_remove.webp 1899w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_demo_remove-300x197.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_demo_remove-1024x672.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_demo_remove-768x504.webp 768w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_demo_remove-1536x1009.webp 1536w" sizes="auto, (max-width: 1899px) 100vw, 1899px" /></figure>



<div style="height:38px" aria-hidden="true" class="wp-block-spacer"></div>



<p>I will create a new test function like so:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="func test_delete_button_removes_all_items_and_disables_button() {
    // --- GIVEN ---
    let app = XCUIApplication()
    app.launch()

    let deleteButton = app.buttons[UIIdentifiers.ItemListScreen.deleteButton]
    let collection = app.descendants(matching: .any)[UIIdentifiers.ItemListScreen.itemListView]

    XCTAssertTrue(collection.waitForExistence(timeout: 2), 
                  &quot;Items should be visible&quot;)
    XCTAssertTrue(deleteButton.waitForExistence(timeout: 2), 
                   &quot;Delete button should exist&quot;)
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">test_delete_button_removes_all_items_and_disables_button</span><span style="color: #F6F6F4">() {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- GIVEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> app </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">XCUIApplication</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">    app.</span><span style="color: #97E1F1">launch</span><span style="color: #F6F6F4">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> deleteButton </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> app.buttons[UIIdentifiers.ItemListScreen.deleteButton]</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> collection </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> app.</span><span style="color: #97E1F1">descendants</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">matching</span><span style="color: #F6F6F4">: .any)[UIIdentifiers.ItemListScreen.itemListView]</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">XCTAssertTrue</span><span style="color: #F6F6F4">(collection.</span><span style="color: #97E1F1">waitForExistence</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">timeout</span><span style="color: #F6F6F4">: </span><span style="color: #BF9EEE">2</span><span style="color: #F6F6F4">), </span></span>
<span class="line"><span style="color: #F6F6F4">                  </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Items should be visible</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">XCTAssertTrue</span><span style="color: #F6F6F4">(deleteButton.</span><span style="color: #97E1F1">waitForExistence</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">timeout</span><span style="color: #F6F6F4">: </span><span style="color: #BF9EEE">2</span><span style="color: #F6F6F4">), </span></span>
<span class="line"><span style="color: #F6F6F4">                   </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Delete button should exist</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:28px" aria-hidden="true" class="wp-block-spacer"></div>



<p>We locate both the <strong>button</strong> and the <strong>container (scroll view/list)</strong> and make sure they’re present before continuing. Always assert the UI is ready before interacting.</p>



<p>Next, I will count how many items are currently visibile:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="    let predicate = NSPredicate(format: &quot;identifier CONTAINS 'item.'&quot;)
    let itemElements = collection.descendants(matching: .any).matching(predicate)
    let initialCount = itemElements.count

    XCTAssertGreaterThan(initialCount, 0, &quot;There should be items to delete&quot;)
    XCTAssertTrue(deleteButton.isEnabled, &quot;Delete button should be enabled initially&quot;)" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> predicate </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">NSPredicate</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">format</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">identifier CONTAINS &#39;item.&#39;</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> itemElements </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> collection.</span><span style="color: #97E1F1">descendants</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">matching</span><span style="color: #F6F6F4">: .any).</span><span style="color: #97E1F1">matching</span><span style="color: #F6F6F4">(predicate)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> initialCount </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> itemElements.</span><span style="color: #BF9EEE">count</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">XCTAssertGreaterThan</span><span style="color: #F6F6F4">(initialCount, </span><span style="color: #BF9EEE">0</span><span style="color: #F6F6F4">, </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">There should be items to delete</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">XCTAssertTrue</span><span style="color: #F6F6F4">(deleteButton.isEnabled, </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Delete button should be enabled initially</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span></code></pre></div>



<div style="height:23px" aria-hidden="true" class="wp-block-spacer"></div>



<p>Instead of hardcoding the number of delete taps, we:</p>



<ul class="wp-block-list">
<li>Count how many items are currently in the list</li>



<li>Verify that there’s <strong>at least one</strong> item</li>



<li>Confirm the Delete button is enabled</li>
</ul>



<p><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Why this matters:</strong> This makes the test <strong>resilient</strong>—it still works if the initial item list changes.</p>



<div style="height:48px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Tap Delete Until All Items Are Gone</h3>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="    // --- WHEN ---
    for _ in 0..&lt;initialCount {
        deleteButton.tap()
    }" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- WHEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">for</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">_</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">in</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">0</span><span style="color: #F286C4">..&lt;</span><span style="color: #F6F6F4">initialCount {</span></span>
<span class="line"><span style="color: #F6F6F4">        deleteButton.</span><span style="color: #97E1F1">tap</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span></code></pre></div>



<div style="height:31px" aria-hidden="true" class="wp-block-spacer"></div>



<p>We dynamically tap the Delete button for every item that exists. This simulates how a user might clear out the list manually.</p>



<h3 class="wp-block-heading">Then: Verify UI Is Updated</h3>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="    // --- THEN ---
    XCTAssertTrue(collection.waitForExistence(timeout: 2), 
                      &quot;Item List should be visible&quot;) 
    let remainingItems = collection.descendants(matching: .any).matching(predicate)
    XCTAssertEqual(remainingItems.count, 0, 
                  &quot;All items should be removed from the screen&quot;)
    XCTAssertFalse(deleteButton.isEnabled, 
                   &quot;Delete button should be disabled after all items are removed&quot;)" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- THEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">XCTAssertTrue</span><span style="color: #F6F6F4">(collection.</span><span style="color: #97E1F1">waitForExistence</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">timeout</span><span style="color: #F6F6F4">: </span><span style="color: #BF9EEE">2</span><span style="color: #F6F6F4">), </span></span>
<span class="line"><span style="color: #F6F6F4">                      </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Item List should be visible</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) </span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> remainingItems </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> collection.</span><span style="color: #97E1F1">descendants</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">matching</span><span style="color: #F6F6F4">: .any).</span><span style="color: #97E1F1">matching</span><span style="color: #F6F6F4">(predicate)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">XCTAssertEqual</span><span style="color: #F6F6F4">(remainingItems.</span><span style="color: #BF9EEE">count</span><span style="color: #F6F6F4">, </span><span style="color: #BF9EEE">0</span><span style="color: #F6F6F4">, </span></span>
<span class="line"><span style="color: #F6F6F4">                  </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">All items should be removed from the screen</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">XCTAssertFalse</span><span style="color: #F6F6F4">(deleteButton.isEnabled, </span></span>
<span class="line"><span style="color: #F6F6F4">                   </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Delete button should be disabled after all items are removed</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span></code></pre></div>



<div style="height:27px" aria-hidden="true" class="wp-block-spacer"></div>



<p>We assert that:</p>



<ul class="wp-block-list">
<li><strong>No items remain in the UI</strong></li>



<li>The <strong>Delete button is now disabled</strong>, as expected</li>
</ul>



<p>This checks that the <strong>ViewModel → View state binding</strong> works and that the UI reacts correctly.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<h3 class="wp-block-heading">Why This Test Works Well</h3>



<ul class="wp-block-list">
<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> No hardcoded tap counts</li>



<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Verifies both <strong>visual state</strong> and <strong>interactivity</strong></li>



<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Easy to maintain as the app evolves</li>



<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Clear failure messages help pinpoint what broke</li>
</ul>



<p>This test covers not just business logic, but the <strong>end result that the user actually sees and interacts with</strong>.</p>
</div></div>



<div style="height:37px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">Advanced: Controlling App Behavior in Tests</h2>



<p>UI tests always launch your app from scratch—which means you can control how it launches.</p>



<p>For example, you can pass <strong>launch arguments</strong> or <strong>environment variables</strong> to:</p>



<ul class="wp-block-list">
<li>Load mock data</li>



<li>Use a temporary file for test storage</li>



<li>Clear <code>UserDefaults</code> or caches</li>



<li>Inject staging URLs or fake config</li>
</ul>



<p>This keeps your tests clean, repeatable, and isolated.</p>



<p><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f449.png" alt="👉" class="wp-smiley" style="height: 1em; max-height: 1em;" /> I’ll cover this in a follow-up post:<br><strong>“How to Inject Data and Configuration into Your iOS App for UI Tests”</strong></p>



<p>It includes examples using <code>CommandLine.arguments</code>, <code>ProcessInfo.environment</code>, and temp file paths.</p>



<div style="height:44px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">Pros and Cons of UI Testing in iOS with XCUITest</h2>



<p>UI tests let you automate real user flows—but they’re not magic. They work by <strong>launching the entire app from scratch</strong> in a simulator and interacting with the UI like a human would. This gives you full visibility into what users experience—but it also comes with downsides.</p>



<div style="height:35px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Pros: Why UI Tests Are Worth It</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Benefit</th><th>Why It Matters</th></tr></thead><tbody><tr><td><strong>Test the real app</strong></td><td>Validates actual navigation, buttons, and views</td></tr><tr><td><strong>Great for full flows</strong></td><td>Ideal for testing &#8220;add item → confirm → see item&#8221;</td></tr><tr><td><strong>High confidence</strong></td><td>Catches bugs unit tests can’t (e.g. button not wired)</td></tr><tr><td><strong>Runs in CI</strong></td><td>Can catch regressions on every commit</td></tr></tbody></table></figure>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Cons: The Real Trade-offs</h3>



<p>What i saw so far from the iOS developer community, the most common sentiment about XCUITest are that they are flaky and slow. </p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Limitation</th><th>Why It Hurts</th></tr></thead><tbody><tr><td><strong>Slow</strong></td><td>Each test launches the full app from scratch</td></tr><tr><td><strong>No access to app internals</strong></td><td>Can&#8217;t read variables or inspect state directly</td></tr><tr><td><strong>Flaky</strong></td><td>Timing issues (e.g. animations, async loads) cause random failures</td></tr><tr><td><strong>Harder to debug</strong></td><td>Failures often give vague errors</td></tr><tr><td><strong>Harder to isolate</strong></td><td>Shared state must be reset between runs</td></tr></tbody></table></figure>



<p>Probably the biggest downside to XCUITest is the test speed. The above 3 tests take together 27 seconds. They run one after the other in the simulator. You can imagine that this becomes quickly too slow to run frequently, if you want to use these test during Test-Driven-Develelpment TDD. The longer tests take the less likely you will be to actually run them. That is way most apps only add a few test flows, the most important long happy path.</p>



<p>In the following you can see the test results for both ui and unit tests:</p>



<figure class="gb-block-image gb-block-image-02e17cb2"><img loading="lazy" decoding="async" width="1614" height="1131" class="gb-image gb-image-02e17cb2" src="https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_results_speed.webp" alt="" title="ui_testing_results_speed" srcset="https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_results_speed.webp 1614w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_results_speed-300x210.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_results_speed-1024x718.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_results_speed-768x538.webp 768w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ui_testing_results_speed-1536x1076.webp 1536w" sizes="auto, (max-width: 1614px) 100vw, 1614px" /></figure>



<p>The slowest UI test takes 12 sec vs the slowest unit test with 0.0023 sec. Because of this massive difference, most iOS devs will avoid UI tests and concentrate mostly on writing unit tests. You can have hundreds of unit tests runing &lt;1sec that you run frequently during your development process withouth blocking you.</p>



<p>This is unfortunate since most bugs in iOS development are UI related. Avoiding UI testing makes this worse. There have been 3rd party libraries that try to give you fast unit tests for SwiftUI UI related code. You can have a look at these:</p>



<ul class="wp-block-list">
<li>ViewInspector</li>



<li>Snapshot testing</li>



<li>Preference testing</li>
</ul>



<p>The XCTest team is aware of the missing test tool. I hope that in the not so far future we will see some much needed UI testing tools that fit in the developer workflow.</p>



<div style="height:42px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">Summary</h2>



<p>In this post, you learned how to:</p>



<ul class="wp-block-list">
<li>Set up <strong>XCUITest</strong> to write UI tests for a SwiftUI iOS app</li>



<li>Use <strong>accessibility identifiers</strong> to reliably find UI elements</li>



<li>Validate real user flows like adding and deleting items</li>



<li>Understand the <strong>trade-offs</strong> of UI testing: high value, but slower and more fragile than unit tests</li>
</ul>



<p>UI tests won’t replace your unit tests—but they fill a gap unit tests can’t: verifying that the app behaves correctly from the user’s point of view.</p>



<p><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2b07.png" alt="⬇" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Project files: <a href="https://github.com/gahntpo/iOS-testing" target="_blank" rel="noopener">https://github.com/gahntpo/iOS-testing</a></p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f680.png" alt="🚀" class="wp-smiley" style="height: 1em; max-height: 1em;" /> What’s Next?</h3>



<ul class="wp-block-list">
<li>Testing SwiftUI navigation and sheet logic</li>



<li>Handling async waits and animations without flakiness</li>



<li>Using <code>ViewInspector</code> for fast, logic-level UI tests</li>



<li>Snapshot testing for visual regressions</li>
</ul>
</div></div>
<p>The post <a rel="nofollow" href="https://www.swiftyplace.com/blog/xcuitest-ui-testing-swiftui">XCUITest: How to Write UI Tests for SwiftUI Apps</a> appeared first on <a rel="nofollow" href="https://www.swiftyplace.com">swiftyplace</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.swiftyplace.com/blog/xcuitest-ui-testing-swiftui/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Getting Started with Unit Testing for iOS Development</title>
		<link>https://www.swiftyplace.com/blog/unit-testing-ios-apps?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=unit-testing-ios-apps</link>
					<comments>https://www.swiftyplace.com/blog/unit-testing-ios-apps#respond</comments>
		
		<dc:creator><![CDATA[Karin Prater]]></dc:creator>
		<pubDate>Thu, 15 May 2025 13:25:47 +0000</pubDate>
				<category><![CDATA[Testing in iOS development]]></category>
		<guid isPermaLink="false">https://www.swiftyplace.com/?p=1005536</guid>

					<description><![CDATA[<p>Manual QA is slow, clumsy and error-prone. When you click through every flow in your app by hand, you lose precious time—and you still might wake up at 3 AM wondering if you forgot to ... <a title="Getting Started with Unit Testing for iOS Development" class="read-more" href="https://www.swiftyplace.com/blog/unit-testing-ios-apps" aria-label="More on Getting Started with Unit Testing for iOS Development">Read more</a></p>
<p>The post <a rel="nofollow" href="https://www.swiftyplace.com/blog/unit-testing-ios-apps">Getting Started with Unit Testing for iOS Development</a> appeared first on <a rel="nofollow" href="https://www.swiftyplace.com">swiftyplace</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Manual QA is slow, clumsy and error-prone. When you click through every flow in your app by hand, you lose precious time—and you still might wake up at 3 AM wondering if you forgot to test that one edge case. Automated unit testing solve this by verifying small “units” of behavior in isolation, in milliseconds, over and over again. Let’s walk through how to get real value from unit tests without overengineering.</p>



<p>We&#8217;ll walk through writing your first test using the new Swift Testing Framework  (Swift 6+), which is a big improvement to <a href="https://www.swiftyplace.com/blog/unit-testing-xctest-swiftui">XCTest</a>.</p>



<p><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2b07.png" alt="⬇" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Project files: <a href="https://github.com/gahntpo/iOS-testing" target="_blank" rel="noopener">https://github.com/gahntpo/iOS-testing</a></p>



<div style="height:17px" aria-hidden="true" class="wp-block-spacer"></div>



			
			
										
			
			


<div style="height:17px" aria-hidden="true" class="wp-block-spacer"></div>



<p>A unit test is an automated test that</p>



<ul class="wp-block-list">
<li><em>Verifies a single unit of behavior,</em></li>



<li><em>Does it quickly,</em></li>



<li><em>And in isolation from other tests.</em></li>
</ul>



<p><em>Vladimir Khorikov, Unit Testing: Principles, Practices, and Patterns</em></p>



<div style="height:81px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="gb-headline gb-headline-32de0e1f gb-headline-text">Setting Up Unit Tests in Xcode</h2>



<p><strong><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Creating a New Project With Tests</strong></p>



<p>When starting a new project in Xcode:</p>



<ul class="wp-block-list">
<li>Select <strong>App</strong> template.</li>



<li>Make sure you check <strong>“Include Tests”</strong> when configuring the project.</li>



<li>This will add a test target automatically (e.g., YourAppTests).</li>
</ul>



<figure class="gb-block-image gb-block-image-0249a434"><img loading="lazy" decoding="async" width="1464" height="1064" class="gb-image gb-image-0249a434" src="http://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_1.webp" alt="add swift testing wih xctest when creating a new project in Xcode" title="unit_testing_1" srcset="https://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_1.webp 1464w, https://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_1-300x218.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_1-1024x744.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_1-768x558.webp 768w" sizes="auto, (max-width: 1464px) 100vw, 1464px" /></figure>



<div style="height:38px" aria-hidden="true" class="wp-block-spacer"></div>



<p><strong><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2795.png" alt="➕" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Adding Tests to an Existing Project</strong></p>



<p>If you already have a SwiftUI project:</p>



<ol class="wp-block-list">
<li>Open <strong>File &gt; New &gt; Target…</strong></li>



<li>Select <strong>“Unit Testing Bundle”</strong></li>



<li>Choose your app under “Target to be tested”</li>



<li>In your new test files, add:</li>
</ol>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="@testable import YourAppModule" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">@testable</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">YourAppModule</span></span></code></pre></div>



<div style="height:51px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="gb-headline gb-headline-ad3570b0 gb-headline-text">The Code We’re Testing</h2>



<p>Let’s start with a simple ItemViewModel that powers a SwiftUI view. It displays a list of items and removes the last one when a button is tapped.</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="struct Item: Identifiable {
    var name: String
    let id: UUID
    init(name: String, id: UUID = UUID()) {
        self.name = name
        self.id = id
    }

    static var examples: [Item] {
        [Item(name: &quot;first&quot;), Item(name: &quot;second&quot;), Item(name: &quot;third&quot;)]
    }
}

final class ItemViewModel: ObservableObject {
    @Published var items: [Item] = Item.examples
    @Published var deleteDisabled = false

    func removeLast() {
        guard items.isEmpty == false else { return }
        items.removeLast()
        deleteDisabled = items.isEmpty
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">Item</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">Identifiable </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> name: </span><span style="color: #97E1F1; font-style: italic">String</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> id: UUID</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">init</span><span style="color: #F6F6F4">(</span><span style="color: #62E884; font-style: italic">name</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4">, </span><span style="color: #62E884; font-style: italic">id</span><span style="color: #F6F6F4">: UUID </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">UUID</span><span style="color: #F6F6F4">()) {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #BF9EEE; font-style: italic">self</span><span style="color: #F6F6F4">.name </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> name</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #BF9EEE; font-style: italic">self</span><span style="color: #F6F6F4">.id </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> id</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">static</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> examples: [Item] {</span></span>
<span class="line"><span style="color: #F6F6F4">        [</span><span style="color: #97E1F1">Item</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">name</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">first</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">), </span><span style="color: #97E1F1">Item</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">name</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">second</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">), </span><span style="color: #97E1F1">Item</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">name</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">third</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)]</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">final</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">class</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">ItemViewModel</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">ObservableObject </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@Published</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> items: [Item] </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> Item.examples</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@Published</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> deleteDisabled </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">false</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">removeLast</span><span style="color: #F6F6F4">() {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">guard</span><span style="color: #F6F6F4"> items.</span><span style="color: #BF9EEE">isEmpty</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">==</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">false</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">else</span><span style="color: #F6F6F4"> { </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> }</span></span>
<span class="line"><span style="color: #F6F6F4">        items.</span><span style="color: #97E1F1">removeLast</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        deleteDisabled </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> items.</span><span style="color: #BF9EEE">isEmpty</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:32px" aria-hidden="true" class="wp-block-spacer"></div>



<p>The main content view shows the items and a button to delete the last item:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="struct ContentView: View {
    
    @ObservedObject var viewModel: ItemViewModel
    
    var body: some View {
        VStack {
            ScrollView {
                ForEach(viewModel.items) { item in
                    Text(item.name)
                        .padding()
                        .frame(maxWidth: .infinity)
                        .background(Capsule().fill(Color.yellow))
                }
            }
            
            Button(&quot;Remove Last&quot;) {
               viewModel.deleteLastItem()
            }
            .buttonStyle(.borderedProminent)
            .disabled(viewModel.deleteDisabled)
        }
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ContentView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@ObservedObject</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> viewModel: ItemViewModel</span></span>
<span class="line"><span style="color: #F6F6F4">    </span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">VStack</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">ScrollView</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #97E1F1">ForEach</span><span style="color: #F6F6F4">(viewModel.items) { item </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #97E1F1">Text</span><span style="color: #F6F6F4">(item.name)</span></span>
<span class="line"><span style="color: #F6F6F4">                        .</span><span style="color: #97E1F1">padding</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">                        .</span><span style="color: #97E1F1">frame</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">maxWidth</span><span style="color: #F6F6F4">: .</span><span style="color: #BF9EEE">infinity</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">                        .</span><span style="color: #97E1F1">background</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">Capsule</span><span style="color: #F6F6F4">().</span><span style="color: #97E1F1">fill</span><span style="color: #F6F6F4">(Color.yellow))</span></span>
<span class="line"><span style="color: #F6F6F4">                }</span></span>
<span class="line"><span style="color: #F6F6F4">            }</span></span>
<span class="line"><span style="color: #F6F6F4">            </span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">Button</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Remove Last</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) {</span></span>
<span class="line"><span style="color: #F6F6F4">               viewModel.</span><span style="color: #97E1F1">deleteLastItem</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">            }</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">buttonStyle</span><span style="color: #F6F6F4">(.borderedProminent)</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">disabled</span><span style="color: #F6F6F4">(viewModel.deleteDisabled)</span></span>
<span class="line"><span style="color: #F6F6F4">        }</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:23px" aria-hidden="true" class="wp-block-spacer"></div>



<figure class="gb-block-image gb-block-image-874272c0"><img loading="lazy" decoding="async" width="1920" height="1229" class="gb-image gb-image-874272c0" src="http://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_demo.webp" alt="writing unit tests for a swiftui app" title="unit_testing_demo" srcset="https://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_demo.webp 1920w, https://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_demo-300x192.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_demo-1024x655.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_demo-768x492.webp 768w, https://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_demo-1536x983.webp 1536w" sizes="auto, (max-width: 1920px) 100vw, 1920px" /></figure>



<div style="height:41px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">Writing Unit Tests Using Swift Testing</h2>



<p>Swift 6 introduced a new way to write tests—no need for XCTestCase or inheritance. Here&#8217;s what a clean Swift Testing setup looks like:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);--cbp-line-highlight-color:rgba(251, 251, 239, 0.2);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="import Testing
@testable import ItemApp

struct ItemViewModelTests {

    @Test func items_when_remove_button_then_items_count_lowered() {
        let vm = ItemViewModel()
        
        vm.removeLast()

        #expect(vm.items.count == 2)
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">Testing</span></span>
<span class="line"><span style="color: #F286C4">@testable</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ItemApp</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ItemViewModelTests</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"></span>
<span class="line cbp-line-highlight"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@Test</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">items_when_remove_button_then_items_count_lowered</span><span style="color: #F6F6F4">() {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> vm </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">ItemViewModel</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        </span></span>
<span class="line"><span style="color: #F6F6F4">        vm.</span><span style="color: #97E1F1">removeLast</span><span style="color: #F6F6F4">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">#expect</span><span style="color: #F6F6F4">(vm.items.</span><span style="color: #BF9EEE">count</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">==</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">2</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:23px" aria-hidden="true" class="wp-block-spacer"></div>



<p>Add the @Test marco in front of the test function, otherwise you Xcode will not recognize it as a test function.</p>



<div style="height:29px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="gb-headline gb-headline-4c939502 gb-headline-text">&nbsp;Running Your Tests i<strong>n Xcode</strong></h2>



<p>You can run tests by any of the following methodes:</p>



<ul class="wp-block-list">
<li>Press ⌘ + U to run all tests.</li>



<li>Use the <strong>Test Navigator</strong> (⌘ + 6) to see individual test results.</li>



<li>run one test by pressing on the diamond next to the @Test macro</li>
</ul>



<figure class="gb-block-image gb-block-image-7667c028"><img loading="lazy" decoding="async" width="1944" height="1244" class="gb-image gb-image-7667c028" src="http://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_1-copy-3.webp" alt="running swift tests in xcode 16" title="unit_testing_1 copy 3" srcset="https://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_1-copy-3.webp 1944w, https://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_1-copy-3-300x192.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_1-copy-3-1024x655.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_1-copy-3-768x491.webp 768w, https://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_1-copy-3-1536x983.webp 1536w" sizes="auto, (max-width: 1944px) 100vw, 1944px" /></figure>



<div style="height:43px" aria-hidden="true" class="wp-block-spacer"></div>



<p>Failed tests will be shown with a red icon and passed tests will show with a green checkmark:</p>



<figure class="gb-block-image gb-block-image-ffee4863"><img loading="lazy" decoding="async" width="1929" height="1244" class="gb-image gb-image-ffee4863" src="http://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_1-copy-4.webp" alt="swift testing with passed and failed tests in xcode" title="unit_testing_1 copy 4" srcset="https://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_1-copy-4.webp 1929w, https://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_1-copy-4-300x193.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_1-copy-4-1024x660.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_1-copy-4-768x495.webp 768w, https://www.swiftyplace.com/wp-content/uploads/2025/05/unit_testing_1-copy-4-1536x991.webp 1536w" sizes="auto, (max-width: 1929px) 100vw, 1929px" /></figure>



<div style="height:43px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="gb-headline gb-headline-cdc8431c gb-headline-text"><strong>Testing Best practices</strong></h2>



<p>Writing tests is easy but writing tests that will help you catch and fix bugs in the future is hard. Here is an example of a test that is  readable and clearly structured with a good assertion:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);--cbp-line-highlight-color:rgba(251, 251, 239, 0.2);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="    @Test(&quot;Remove Button lowers item count&quot;)
    func items_when_remove_button_then_items_count_lowered() {
        // --- GIVEN ---
        let vm = ItemViewModel()
        let itemCount = vm.items.count
        let expectedItemCount = itemCount - 1
        #expect(vm.items.count &gt; 0) // precondition
        
        // --- WHEN ---
        vm.deleteLastItem()
        
        // --- THEN ---
        #expect(vm.items.count == expectedItemCount,
                &quot;Expected that the array has \(expectedItemCount) items&quot;)
    }" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@Test</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Remove Button lowers item count</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">items_when_remove_button_then_items_count_lowered</span><span style="color: #F6F6F4">() {</span></span>
<span class="line cbp-line-highlight"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// --- GIVEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> vm </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">ItemViewModel</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> itemCount </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> vm.items.</span><span style="color: #BF9EEE">count</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> expectedItemCount </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> itemCount </span><span style="color: #F286C4">-</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">1</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">#expect</span><span style="color: #F6F6F4">(vm.items.</span><span style="color: #BF9EEE">count</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">&gt;</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">0</span><span style="color: #F6F6F4">) </span><span style="color: #7B7F8B">// precondition</span></span>
<span class="line"><span style="color: #F6F6F4">        </span></span>
<span class="line cbp-line-highlight"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// --- WHEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">        vm.</span><span style="color: #97E1F1">deleteLastItem</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        </span></span>
<span class="line cbp-line-highlight"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// --- THEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">#expect</span><span style="color: #F6F6F4">(vm.items.</span><span style="color: #BF9EEE">count</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">==</span><span style="color: #F6F6F4"> expectedItemCount,</span></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Expected that the array has </span><span style="color: #F286C4">\(</span><span style="color: #E7EE98">expectedItemCount</span><span style="color: #F286C4">)</span><span style="color: #E7EE98"> items</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span></code></pre></div>



<div style="height:28px" aria-hidden="true" class="wp-block-spacer"></div>



<p>Tests follow&nbsp;<strong>Given–When–Then</strong> or &nbsp;Arrange-Act-Assert structure. </p>



<p>The <strong>When</strong> section should only have one line of code (clearly indicate what behavior you are testing). Otherwise you might consider refactoring your code to encapsulate the multiline code into one single call.</p>



<div style="height:28px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="gb-headline gb-headline-9f0d2505 gb-headline-text">Name Tests in Plain English</h3>



<p>Avoid tying test names to implementation details that might change. Prefer:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="func items_when_remove_button_then_items_count_lowered() " style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">items_when_remove_button_then_items_count_lowered</span><span style="color: #F6F6F4">() </span></span></code></pre></div>



<div style="height:22px" aria-hidden="true" class="wp-block-spacer"></div>



<p>over</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="func itemViewModel_removeLast_success()" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">itemViewModel_removeLast_success</span><span style="color: #F6F6F4">()</span></span></code></pre></div>



<div style="height:22px" aria-hidden="true" class="wp-block-spacer"></div>



<p>– if you rename&nbsp;<code>removeLast()</code>, the first stays meaningful, the second forces you to update tests too.</p>



<div style="height:59px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Make Tests Resilient while Refactoring</h3>



<p>Rather than asserting a hard-coded expected value (e.g.&nbsp;<code>2</code>):</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="#expect(vm.items.count == 2)" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #97E1F1">#expect</span><span style="color: #F6F6F4">(vm.items.</span><span style="color: #BF9EEE">count</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">==</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">2</span><span style="color: #F6F6F4">)</span></span></code></pre></div>



<div style="height:12px" aria-hidden="true" class="wp-block-spacer"></div>



<p>Instead derive it .Now if you change initial items (or add a new default), your test still passes without rewriting “2” everywhere.</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="let itemCount = vm.items.count
let expectedItemCount = itemCount - 1

#expect(vm.items.count == expectedItemCount)" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> itemCount </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> vm.items.</span><span style="color: #BF9EEE">count</span></span>
<span class="line"><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> expectedItemCount </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> itemCount </span><span style="color: #F286C4">-</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">1</span></span>
<span class="line"></span>
<span class="line"><span style="color: #97E1F1">#expect</span><span style="color: #F6F6F4">(vm.items.</span><span style="color: #BF9EEE">count</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">==</span><span style="color: #F6F6F4"> expectedItemCount)</span></span></code></pre></div>



<div style="height:15px" aria-hidden="true" class="wp-block-spacer"></div>



<p>I cannot stress enough how frustrating failed tests are that give you false alarms that means they fail but your app is working as expected. They are demotivating and will take addtional time away from you. Please try to write tests that are flexible enough in future refactorings. </p>



<p>You want to write tests that give me a clear failure notification. Spend a significant amount of your time finding good assertions. What is the expected behavior. In above example, I am not expecting that the item count i always a fixed number 2. I would expect that the itme count is lowered by 1. </p>



<div style="height:30px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Testing Edge Cases: Guard Clauses</h3>



<p>Our&nbsp;<code>removeLastItem()</code>&nbsp;returns early when the items array is early:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);--cbp-line-highlight-color:rgba(251, 251, 239, 0.2);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="func deleteLastItem() {
   guard items.isEmpty == false else { return }
   items.removeLast()
    deleteDisabled = items.isEmpty
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">deleteLastItem</span><span style="color: #F6F6F4">() {</span></span>
<span class="line cbp-line-highlight"><span style="color: #F6F6F4">   </span><span style="color: #F286C4">guard</span><span style="color: #F6F6F4"> items.</span><span style="color: #BF9EEE">isEmpty</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">==</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">false</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">else</span><span style="color: #F6F6F4"> { </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> }</span></span>
<span class="line"><span style="color: #F6F6F4">   items.</span><span style="color: #97E1F1">removeLast</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">    deleteDisabled </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> items.</span><span style="color: #BF9EEE">isEmpty</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:16px" aria-hidden="true" class="wp-block-spacer"></div>



<p> Test this edge case  too:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="@Test func no_items_when_remove_button_then_nothing_happens() async throws {
        // --- GIVEN ---
       let vm = ItemViewModel()
        vm.items = []
        
        // --- WHEN ---
        vm.deleteLastItem()
        
        // --- THEN ---
        #expect(vm.items.isEmpty)
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">@Test</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">no_items_when_remove_button_then_nothing_happens</span><span style="color: #F6F6F4">() </span><span style="color: #F286C4">async</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">throws</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// --- GIVEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">       </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> vm </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">ItemViewModel</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        vm.items </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> []</span></span>
<span class="line"><span style="color: #F6F6F4">        </span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// --- WHEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">        vm.</span><span style="color: #97E1F1">deleteLastItem</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        </span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// --- THEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">#expect</span><span style="color: #F6F6F4">(vm.items.</span><span style="color: #BF9EEE">isEmpty</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<p>This ensures your guard clause actually protects you, not just the happy path.</p>



<div style="height:16px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">What to Test (and What Not)</h2>



<p>Aim your tests at code that’s either:</p>



<ol class="wp-block-list">
<li><strong>Complex</strong>&nbsp;(lots of branches, business logic)</li>



<li><strong>Business-critical</strong>&nbsp;(payment flows, data integrity)</li>
</ol>



<p>Skip trivial models or boilerplate that you’ll rewrite constantly. If changing an initializer or model property breaks dozens of superficial tests, you’ll lose confidence fast and stop writing tests.</p>



<div style="height:25px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">How to Test the UI in SwiftUI Apps</h2>



<p>Unit tests are fast—but they don’t interact with the UI. XCUITests fill that gap for end-to-end flows (button taps, navigation), but they’re&nbsp;<strong>slow</strong>. Only automate the&nbsp;<em>most important</em>&nbsp;user journeys—don’t record a dozen tiny UI tests that take minutes to run every build. You can read more about the different types of tests in ios development in<a href="https://www.swiftyplace.com/blog/testing-in-ios-development"> this blog post</a>.</p>



<p><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2b07.png" alt="⬇" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Project files: <a href="https://github.com/gahntpo/iOS-testing" target="_blank" rel="noopener">https://github.com/gahntpo/iOS-testing</a></p>



<div style="height:0px" aria-hidden="true" class="wp-block-spacer"></div>



<p>An alternative to XCUITest are unit tests for SwiftUI interfaces. You can use snapshot tests, ViewInspector or<a href="http://Can You use PreferenceKeys for Testing SwiftUI Views" target="_blank"> preference testing</a>. These are much faster and reliable then XCUITests.</p>



<div style="height:35px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="gb-headline gb-headline-78f701ca gb-headline-text">Key Takeaways</h2>



<ul class="wp-block-list">
<li><strong>Start small</strong>: test one function at a time (AAA/Given-When-Then).</li>



<li><strong>Name clearly</strong>&nbsp;in plain English to survive refactors.</li>



<li><strong>Derive expected values</strong>&nbsp;instead of hard-coding.</li>



<li><strong>Cover edge cases</strong>: guard clauses, empty data.</li>



<li><strong>Prioritize</strong>&nbsp;complexity and business significance over boilerplate.</li>



<li><strong>Error messages</strong>:&nbsp;write clear failure messages so you know what you meant months later.</li>



<li><strong>Write good assertions</strong>:&nbsp;focus on behavior, avoid brittle checks (e.g. fixed numbers).</li>



<li><strong>Don’t write bad tests</strong>: if a test isn’t valuable or maintainable, it’s better left unwritten.</li>
</ul>



<p>With just a handful of focused, resilient tests, you’ll catch regressions instantly, code with confidence, and spend far less time repeating manual QA. Your future self (and your users) will thank you.</p>



<p></p>
<p>The post <a rel="nofollow" href="https://www.swiftyplace.com/blog/unit-testing-ios-apps">Getting Started with Unit Testing for iOS Development</a> appeared first on <a rel="nofollow" href="https://www.swiftyplace.com">swiftyplace</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.swiftyplace.com/blog/unit-testing-ios-apps/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Can You use PreferenceKeys for Testing SwiftUI Views</title>
		<link>https://www.swiftyplace.com/blog/swiftui-testing-with-preferencekeys?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=swiftui-testing-with-preferencekeys</link>
					<comments>https://www.swiftyplace.com/blog/swiftui-testing-with-preferencekeys#respond</comments>
		
		<dc:creator><![CDATA[Karin Prater]]></dc:creator>
		<pubDate>Fri, 09 May 2025 08:35:22 +0000</pubDate>
				<category><![CDATA[Testing in iOS development]]></category>
		<guid isPermaLink="false">https://www.swiftyplace.com/?p=1005519</guid>

					<description><![CDATA[<p>The Accidental Discovery One afternoon I was experimenting with EnvironmentKeys—trying to drive navigation and coordinate between screens without resorting to imperative hacks. SwiftUI’s environment feels like a top‑down broadcast: you inject a value at the ... <a title="Can You use PreferenceKeys for Testing SwiftUI Views" class="read-more" href="https://www.swiftyplace.com/blog/swiftui-testing-with-preferencekeys" aria-label="More on Can You use PreferenceKeys for Testing SwiftUI Views">Read more</a></p>
<p>The post <a rel="nofollow" href="https://www.swiftyplace.com/blog/swiftui-testing-with-preferencekeys">Can You use PreferenceKeys for Testing SwiftUI Views</a> appeared first on <a rel="nofollow" href="https://www.swiftyplace.com">swiftyplace</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">The Accidental Discovery</h2>



<p>One afternoon I was experimenting with EnvironmentKeys—trying to drive navigation and coordinate between screens without resorting to imperative hacks. SwiftUI’s environment feels like a top‑down broadcast: you inject a value at the root, and child views pick it up. But what if you want the opposite? What if a child view could whisper something back up the tree?</p>



<p><br>Enter <strong>PreferenceKeys</strong>—SwiftUI’s built‑in “bottom‑up” communication channel. I stumbled on them via the standard navigation‑title machinery (SwiftUI uses a preference under the hood to let child views set their own title). In that moment I thought:<br>“If SwiftUI itself can bubble up its internal state this way… why can’t I?”</p>



<p><br>So I attached a tiny custom preference to a view, let it bubble up, and then read it off in my hosting code. Suddenly I could observe exactly which views were on‑screen, what their internal state was, and react—<strong>without touching the view’s implementation</strong>.</p>



<p>This lead to a new testing framework that I published on Github: <a href="https://github.com/gahntpo/SwiftLens" target="_blank" rel="noreferrer noopener">SwiftLens</a></p>



<div style="height:31px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">From EnvironmentKeys to PreferenceKeys</h2>



<p><strong>EnvironmentKeys</strong> are SwiftUI’s way of injecting values from parent to children:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="extension EnvironmentValues {
    @Entry var themeColor: Color = .pink
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">extension</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">EnvironmentValues</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@Entry</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> themeColor: Color </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> .pink</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:23px" aria-hidden="true" class="wp-block-spacer"></div>



<p>The default color is pink, but you can pass a different color to childviews like:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="VStack {
    Text(&quot;This is another view&quot;)
    
    CustomView(text: &quot;This lives in another environment&quot;)
      .environment(\.themeColor, Color.blue)
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #97E1F1">VStack</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">Text</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">This is another view</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">CustomView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">text</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">This lives in another environment</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">      .</span><span style="color: #97E1F1">environment</span><span style="color: #F6F6F4">(\.themeColor, Color.blue)</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:23px" aria-hidden="true" class="wp-block-spacer"></div>



<p>All views inside the VStack get the new blue them color and use it like so:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="struct CustomView: View {
    @Environment(.themeColor) var themeColor
    let text: String
    var body: some View {
        Text(text)
            .foregroundStyle(themeColor)
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">CustomView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@Environment</span><span style="color: #F6F6F4">(.themeColor) </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> themeColor</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> text: </span><span style="color: #97E1F1; font-style: italic">String</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">Text</span><span style="color: #F6F6F4">(text)</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">foregroundStyle</span><span style="color: #F6F6F4">(themeColor)</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:13px" aria-hidden="true" class="wp-block-spacer"></div>



<figure class="gb-block-image gb-block-image-63a0b50c"><img loading="lazy" decoding="async" width="730" height="378" class="gb-image gb-image-63a0b50c" src="http://www.swiftyplace.com/wp-content/uploads/2025/05/value_propagation_environment.webp" alt="value propagation with environment in swiftui" title="value_propagation_environment" srcset="https://www.swiftyplace.com/wp-content/uploads/2025/05/value_propagation_environment.webp 730w, https://www.swiftyplace.com/wp-content/uploads/2025/05/value_propagation_environment-300x155.webp 300w" sizes="auto, (max-width: 730px) 100vw, 730px" /></figure>



<p>As you can see the environment passes keys <strong>down</strong> the view hierarchy. It is a top-down approach.</p>



<div style="height:38px" aria-hidden="true" class="wp-block-spacer"></div>



<p><strong>PreferenceKeys</strong> are the mirror image: children push values up, and parents collect them.</p>



<figure class="gb-block-image gb-block-image-506d79c7"><img loading="lazy" decoding="async" width="1243" height="535" class="gb-image gb-image-506d79c7" src="http://www.swiftyplace.com/wp-content/uploads/2025/05/value_propagation_preferences.webp" alt="value propagation with preference keys in swiftui" title="value_propagation_preferences" srcset="https://www.swiftyplace.com/wp-content/uploads/2025/05/value_propagation_preferences.webp 1243w, https://www.swiftyplace.com/wp-content/uploads/2025/05/value_propagation_preferences-300x129.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2025/05/value_propagation_preferences-1024x441.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2025/05/value_propagation_preferences-768x331.webp 768w" sizes="auto, (max-width: 1243px) 100vw, 1243px" /></figure>



<div style="height:27px" aria-hidden="true" class="wp-block-spacer"></div>



<p>For example, I could pass up the view hierachy the accessibility identifiers of the screens:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="struct LensCaptureKey: PreferenceKey {
    static var defaultValue: [String] = []
    static func reduce(value: inout [String], nextValue: () -&gt; [String]) {
        value.append(contentsOf: nextValue())
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">LensCaptureKey</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">PreferenceKey </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">static</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> defaultValue: [</span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4">] </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> []</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">static</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">reduce</span><span style="color: #F6F6F4">(</span><span style="color: #62E884; font-style: italic">value</span><span style="color: #F6F6F4">: </span><span style="color: #F286C4">inout</span><span style="color: #F6F6F4"> [</span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4">], </span><span style="color: #62E884; font-style: italic">nextValue</span><span style="color: #F6F6F4">: () </span><span style="color: #F286C4">-&gt;</span><span style="color: #F6F6F4"> [</span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4">]) {</span></span>
<span class="line"><span style="color: #F6F6F4">        value.</span><span style="color: #97E1F1">append</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">contentsOf</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1">nextValue</span><span style="color: #F6F6F4">())</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:23px" aria-hidden="true" class="wp-block-spacer"></div>



<p>and set them on the views like so:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="struct DetailView: View {
    let item: Int
    var body: some View {
        VStack {
           ...
        }
        .padding()
        .navigationTitle(&quot;Detail for (item)&quot;)
        .preference(key: LensCaptureKey.self, 
                    value: [&quot;screen.detailview.\(item)&quot;])
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">DetailView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> item: </span><span style="color: #97E1F1; font-style: italic">Int</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">VStack</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">           </span><span style="color: #F286C4">...</span></span>
<span class="line"><span style="color: #F6F6F4">        }</span></span>
<span class="line"><span style="color: #F6F6F4">        .</span><span style="color: #97E1F1">padding</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        .</span><span style="color: #97E1F1">navigationTitle</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Detail for (item)</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">        .</span><span style="color: #97E1F1">preference</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">key</span><span style="color: #F6F6F4">: LensCaptureKey.</span><span style="color: #F286C4">self</span><span style="color: #F6F6F4">, </span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #97E1F1">value</span><span style="color: #F6F6F4">: [</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">screen.detailview.</span><span style="color: #F286C4">\(</span><span style="color: #E7EE98">item</span><span style="color: #F286C4">)</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">])</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:23px" aria-hidden="true" class="wp-block-spacer"></div>



<p>You can think of this as any view can “tag” itself with an identifier and some metadata—and SwiftUI will deliver all of those tags to an ancestor via onPreferenceChange.</p>



<p>You can then <code>catch</code> them at a higher level in the view hierarchy with the <code>onPreferenceChange</code> modifier:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="struct ContentView: View {
    var body: some View {
        NavigationStack {
            RootView()
                .navigationTitle(&quot;Root&quot;)
                .navigationDestination(for: Int.self) { item in
                    DetailView(item: item)
                }
        }
        .onPreferenceChange(LensCaptureKey.self) { keys in
            print(keys)
        }
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ContentView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">NavigationStack</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">RootView</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">                .</span><span style="color: #97E1F1">navigationTitle</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Root</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">                .</span><span style="color: #97E1F1">navigationDestination</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">for</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">Int</span><span style="color: #F6F6F4">.</span><span style="color: #F286C4">self</span><span style="color: #F6F6F4">) { item </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #97E1F1">DetailView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">item</span><span style="color: #F6F6F4">: item)</span></span>
<span class="line"><span style="color: #F6F6F4">                }</span></span>
<span class="line"><span style="color: #F6F6F4">        }</span></span>
<span class="line"><span style="color: #F6F6F4">        .</span><span style="color: #97E1F1">onPreferenceChange</span><span style="color: #F6F6F4">(LensCaptureKey.</span><span style="color: #F286C4">self</span><span style="color: #F6F6F4">) { keys </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">print</span><span style="color: #F6F6F4">(keys)</span></span>
<span class="line"><span style="color: #F6F6F4">        }</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:27px" aria-hidden="true" class="wp-block-spacer"></div>



<figure class="gb-block-image gb-block-image-b6bfecb4"><img loading="lazy" decoding="async" width="1812" height="1080" class="gb-image gb-image-b6bfecb4" src="http://www.swiftyplace.com/wp-content/uploads/2025/05/preference_testing_navigation.webp" alt="tracking ui state with preference keys in swiftui" title="preference_testing_navigation" srcset="https://www.swiftyplace.com/wp-content/uploads/2025/05/preference_testing_navigation.webp 1812w, https://www.swiftyplace.com/wp-content/uploads/2025/05/preference_testing_navigation-300x179.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2025/05/preference_testing_navigation-1024x610.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2025/05/preference_testing_navigation-768x458.webp 768w, https://www.swiftyplace.com/wp-content/uploads/2025/05/preference_testing_navigation-1536x915.webp 1536w" sizes="auto, (max-width: 1812px) 100vw, 1812px" /></figure>



<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<p>When navigating inside the navigation stack, you will see the updated print statements:<br>Initially when you at the rootview -&gt;<strong> [&#8220;screen.rootview&#8221;]</strong><br>navigating to item 0                     -&gt;  <strong>[&#8220;screen.rootview&#8221;, &#8220;screen.detailview.0&#8221;]</strong><br>continue navigating to item 11     -&gt;  <strong>[&#8220;screen.rootview&#8221;, &#8220;screen.detailview.0&#8221;, &#8220;screen.detailview.11&#8221;]</strong><br>pressing back button                   -&gt;  <strong>[&#8220;screen.rootview&#8221;, &#8220;screen.detailview.0&#8221;]</strong><br>pressing back button                   -&gt; <strong> [&#8220;screen.rootview&#8221;]</strong></p>



<div style="height:13px" aria-hidden="true" class="wp-block-spacer"></div>



<p>Because each view calls .preferencKey(“…”), SwiftUI automatically collects them via your LensCaptureKey and invokes the parent’s onPreferenceChange. You get real‑time feedback on <strong>exactly</strong> which screens are active—no hacks, no introspection, no brittle accessibility selectors.</p>



<p><br><strong>Bottom‑up</strong>: PreferenceKeys bubble from children up to parents</p>



<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">How SwiftUI Uses PreferenceKeys Under the Hood</h3>



<p>Before I repurposed them for testing, I saw PreferenceKeys in action powering:</p>



<ul class="wp-block-list">
<li><strong>Navigation titles</strong></li>



<li><strong>Toolbar items</strong></li>



<li><strong>List section headers</strong></li>
</ul>



<p>⠀…all of which child views “declare,” and the system hoists up to configure the navigation bar or toolbar. It felt like discovering a secret API invitation from Apple.<br>I also want to stress this in case you wonder if I am using some magic hack here, which I don´t, it´s a SwiftUI native API that has been around since iOS 13 and never changed. I am simply using PreferenceKeys in a creative way <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f607.png" alt="😇" class="wp-smiley" style="height: 1em; max-height: 1em;" />.</p>



<p>Next up, we’ll see how to wire this same mechanism into a test suite (using async/await and a simple observer) so your UI tests can wait for exactly the right views to appear or disappear—with zero flakiness.</p>



<div style="height:46px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">Minimal Testing Setup</h2>



<p>Before we dive into programmatic navigation, let’s get our feet wet with the absolute bare minimum to test a PreferenceKey tag. I will use unit tests to run these tests, which will make them super fast.<br>Here’s what we need:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="import Testing
@testable import TestingWithPreferencesProject

struct TestingWithPreferencesProjectTests {

    @MainActor
    func rootScreenPreference_is_Captured() throws {
        //–– 1. Create the observer

        //–– 2. Wrap ContentView in an onPreferenceChange
        let sut = ContentView()
            .onPreferenceChange(LensCaptureKey.self) { preference in
                // pass the preferences to the observer
            }

        //–– 3. Host it in a window inside UIHostingController

        //–– 4. wait for SwiftUI render

        //–– 5. Assert that &quot;screen.rootview&quot; was emitted
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">Testing</span></span>
<span class="line"><span style="color: #F286C4">@testable</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">TestingWithPreferencesProject</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">TestingWithPreferencesProjectTests</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@MainActor</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">rootScreenPreference_is_Captured</span><span style="color: #F6F6F4">() </span><span style="color: #F286C4">throws</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">//–– 1. Create the observer</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">//–– 2. Wrap ContentView in an onPreferenceChange</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> sut </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">ContentView</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">onPreferenceChange</span><span style="color: #F6F6F4">(LensCaptureKey.</span><span style="color: #F286C4">self</span><span style="color: #F6F6F4">) { preference </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #7B7F8B">// pass the preferences to the observer</span></span>
<span class="line"><span style="color: #F6F6F4">            }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">//–– 3. Host it in a window inside UIHostingController</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">//–– 4. wait for SwiftUI render</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">//–– 5. Assert that &quot;screen.rootview&quot; was emitted</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:34px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Step‑by‑Step</h3>



<p>Lets <strong>define a simple observer</strong> where we’ll store any tags we see in an array. This file lives in the test suite.</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="import Foundation

final class LensObserver {
   @Published var lensCaptures: [String] = []
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">Foundation</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">final</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">class</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">LensObserver</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">   </span><span style="color: #F286C4">@Published</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> lensCaptures: [</span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4">] </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> []</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:17px" aria-hidden="true" class="wp-block-spacer"></div>



<p>and use it in the test function and write the assertion against the observer values:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="    @MainActor
    func rootScreenPreference_is_Captured() throws {
        //–– 1. Create the observer
        let observer = LensObserver()

        //–– 2. Wrap ContentView in an onPreferenceChange
        let testView = ContentView()
            .onPreferenceChange(LensCaptureKey.self) {
                observer.lensCaptures = $0
            }

        //–– 3. Host it in a window inside UIHostingController

        //–– 4. wait for SwiftUI render
        RunLoop.main.run(until: Date().addingTimeInterval(0.1))

        //–– 5. Assert that &quot;screen.rootview&quot; was emitted
        #expect(observer.lensCaptures.contains(&quot;screen.rootview&quot;))
    }" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@MainActor</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">rootScreenPreference_is_Captured</span><span style="color: #F6F6F4">() </span><span style="color: #F286C4">throws</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">//–– 1. Create the observer</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> observer </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">LensObserver</span><span style="color: #F6F6F4">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">//–– 2. Wrap ContentView in an onPreferenceChange</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> testView </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">ContentView</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">onPreferenceChange</span><span style="color: #F6F6F4">(LensCaptureKey.</span><span style="color: #F286C4">self</span><span style="color: #F6F6F4">) {</span></span>
<span class="line"><span style="color: #F6F6F4">                observer.lensCaptures </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE; font-style: italic">$0</span></span>
<span class="line"><span style="color: #F6F6F4">            }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">//–– 3. Host it in a window inside UIHostingController</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">//–– 4. wait for SwiftUI render</span></span>
<span class="line"><span style="color: #F6F6F4">        RunLoop.main.</span><span style="color: #97E1F1">run</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">until</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1">Date</span><span style="color: #F6F6F4">().</span><span style="color: #97E1F1">addingTimeInterval</span><span style="color: #F6F6F4">(</span><span style="color: #BF9EEE">0.1</span><span style="color: #F6F6F4">))</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">//–– 5. Assert that &quot;screen.rootview&quot; was emitted</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">#expect</span><span style="color: #F6F6F4">(observer.lensCaptures.</span><span style="color: #97E1F1">contains</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">screen.rootview</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">))</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span></code></pre></div>



<div style="height:17px" aria-hidden="true" class="wp-block-spacer"></div>



<p>Next we need to actually run the SwiftUI view. This is a bit clunky, but I took inspiration from other testing frameworks like ViewInspector and Snapshot testing</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="//–– 3. Host it in a window inside UIHostingController
let hostingController = UIHostingController(rootView: testView)

let frame = UIScreen.main.bounds
let window = UIWindow(frame: frame)
let rootVC = UIViewController()
window.rootViewController = rootVC
window.makeKeyAndVisible()

hostingController.view.translatesAutoresizingMaskIntoConstraints = false

// Add to parent
hostingController.willMove(toParent: rootVC)
rootVC.addChild(hostingController)
rootVC.view.addSubview(hostingController.view)

// Setup constraints
NSLayoutConstraint.activate([
    hostingController.view.leadingAnchor.constraint(equalTo: rootVC.view.leadingAnchor),
    hostingController.view.topAnchor.constraint(equalTo: rootVC.view.topAnchor),
    hostingController.view.widthAnchor.constraint(equalTo: rootVC.view.widthAnchor),
    hostingController.view.heightAnchor.constraint(equalTo: rootVC.view.heightAnchor)
])

hostingController.didMove(toParent: rootVC)
window.layoutIfNeeded()" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #7B7F8B">//–– 3. Host it in a window inside UIHostingController</span></span>
<span class="line"><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> hostingController </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">UIHostingController</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">rootView</span><span style="color: #F6F6F4">: testView)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> frame </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> UIScreen.main.bounds</span></span>
<span class="line"><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> window </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">UIWindow</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">frame</span><span style="color: #F6F6F4">: frame)</span></span>
<span class="line"><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> rootVC </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">UIViewController</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">window.rootViewController </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> rootVC</span></span>
<span class="line"><span style="color: #F6F6F4">window.</span><span style="color: #97E1F1">makeKeyAndVisible</span><span style="color: #F6F6F4">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">hostingController.</span><span style="color: #BF9EEE">view</span><span style="color: #F6F6F4">.translatesAutoresizingMaskIntoConstraints </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">false</span></span>
<span class="line"></span>
<span class="line"><span style="color: #7B7F8B">// Add to parent</span></span>
<span class="line"><span style="color: #F6F6F4">hostingController.</span><span style="color: #97E1F1">willMove</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">toParent</span><span style="color: #F6F6F4">: rootVC)</span></span>
<span class="line"><span style="color: #F6F6F4">rootVC.</span><span style="color: #97E1F1">addChild</span><span style="color: #F6F6F4">(hostingController)</span></span>
<span class="line"><span style="color: #F6F6F4">rootVC.</span><span style="color: #BF9EEE">view</span><span style="color: #F6F6F4">.</span><span style="color: #97E1F1">addSubview</span><span style="color: #F6F6F4">(hostingController.</span><span style="color: #BF9EEE">view</span><span style="color: #F6F6F4">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #7B7F8B">// Setup constraints</span></span>
<span class="line"><span style="color: #F6F6F4">NSLayoutConstraint.</span><span style="color: #97E1F1">activate</span><span style="color: #F6F6F4">([</span></span>
<span class="line"><span style="color: #F6F6F4">    hostingController.</span><span style="color: #BF9EEE">view</span><span style="color: #F6F6F4">.leadingAnchor.</span><span style="color: #97E1F1">constraint</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">equalTo</span><span style="color: #F6F6F4">: rootVC.</span><span style="color: #BF9EEE">view</span><span style="color: #F6F6F4">.leadingAnchor),</span></span>
<span class="line"><span style="color: #F6F6F4">    hostingController.</span><span style="color: #BF9EEE">view</span><span style="color: #F6F6F4">.topAnchor.</span><span style="color: #97E1F1">constraint</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">equalTo</span><span style="color: #F6F6F4">: rootVC.</span><span style="color: #BF9EEE">view</span><span style="color: #F6F6F4">.topAnchor),</span></span>
<span class="line"><span style="color: #F6F6F4">    hostingController.</span><span style="color: #BF9EEE">view</span><span style="color: #F6F6F4">.widthAnchor.</span><span style="color: #97E1F1">constraint</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">equalTo</span><span style="color: #F6F6F4">: rootVC.</span><span style="color: #BF9EEE">view</span><span style="color: #F6F6F4">.widthAnchor),</span></span>
<span class="line"><span style="color: #F6F6F4">    hostingController.</span><span style="color: #BF9EEE">view</span><span style="color: #F6F6F4">.heightAnchor.</span><span style="color: #97E1F1">constraint</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">equalTo</span><span style="color: #F6F6F4">: rootVC.</span><span style="color: #BF9EEE">view</span><span style="color: #F6F6F4">.heightAnchor)</span></span>
<span class="line"><span style="color: #F6F6F4">])</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">hostingController.</span><span style="color: #97E1F1">didMove</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">toParent</span><span style="color: #F6F6F4">: rootVC)</span></span>
<span class="line"><span style="color: #F6F6F4">window.</span><span style="color: #97E1F1">layoutIfNeeded</span><span style="color: #F6F6F4">()</span></span></code></pre></div>



<div style="height:17px" aria-hidden="true" class="wp-block-spacer"></div>



<p>Now if you build and run, the test should most likely pass <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" />.</p>



<p><strong>What’s happening?</strong></p>



<ul class="wp-block-list">
<li>We spin up a real UIWindow + UIHostingController.</li>



<li>onPreferenceChange(LensCaptureKey.self) catches any strings our views emit.</li>



<li>We run the main run‑loop briefly so SwiftUI has time to attach preferences.</li>



<li>Finally, we assert that our collector saw &#8220;screen.rootview&#8221;.</li>
</ul>



<div style="height:37px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">Ditch the Fixed Delay: Async/Await Waiting for Tags</h2>



<p>In our first test we used a hard‑coded sleep to give SwiftUI time to emit its preferences:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="RunLoop.main.run(until: Date().addingTimeInterval(0.1))" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F6F6F4">RunLoop.main.</span><span style="color: #97E1F1">run</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">until</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1">Date</span><span style="color: #F6F6F4">().</span><span style="color: #97E1F1">addingTimeInterval</span><span style="color: #F6F6F4">(</span><span style="color: #BF9EEE">0.1</span><span style="color: #F6F6F4">))</span></span></code></pre></div>



<div style="height:17px" aria-hidden="true" class="wp-block-spacer"></div>



<p>That works… most of the time. But it’s brittle: if your CI is under load you might need to bump the delay, and your suite gets slower. Instead, let’s write a tiny async helper that <strong>waits</strong> for our tag to appear, and errors out if it doesn’t arrive in a reasonable timeout.</p>



<p>First I will define a useful (but ugly) helper like so:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="import Foundation
import Combine

extension Published.Publisher where Value: Equatable {
  public func waitUntilMatches(
         _ target: Value,
         errorMessage: String,
         timeout: TimeInterval = 1.0
     ) async throws {
         try await self.waitUntilMatches({ $0 == target },
                                         errorMessage: errorMessage,
                                         timeout: timeout)
     }

  public func waitUntilMatches(
        _ predicate: @escaping (Value) -&gt; Bool,
        errorMessage: String,
        timeout: TimeInterval = 1.0
    ) async throws {
        let subject = PassthroughSubject&lt;Value, Error&gt;()
        var cancellables = Set&lt;AnyCancellable&gt;()

        let timeoutPublisher = Fail&lt;Value, Error&gt;(error: WaitUntilError.timeout(description: errorMessage))
            .delay(for: .seconds(timeout), scheduler: RunLoop.main)
            .eraseToAnyPublisher()

        let valuePublisher = self
            .mapError { $0 as Error }
            .eraseToAnyPublisher()

        valuePublisher
            .merge(with: timeoutPublisher)
            .sink(receiveCompletion: { completion in
                if case .failure(let error) = completion {
                    subject.send(completion: .failure(error))
                }
            }, receiveValue: { value in
                if predicate(value) {
                    subject.send(value)
                    subject.send(completion: .finished)
                }
            })
            .store(in: &amp;cancellables)

        for try await _ in subject.values {}
    }
}

public struct TestTimeoutError: Error, Equatable {}

public enum WaitUntilError: Error, CustomStringConvertible, Equatable {
    case timeout(description: String)

    public var description: String {
        switch self {
            case .timeout(let message):
                return &quot;&#x23f0; Timeout: (message)&quot;
        }
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">Foundation</span></span>
<span class="line"><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">Combine</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">extension</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">Published</span><span style="color: #F6F6F4">.Publisher </span><span style="color: #F286C4">where</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">Value</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">Equatable</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">  </span><span style="color: #F286C4">public</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">waitUntilMatches</span><span style="color: #F6F6F4">(</span></span>
<span class="line"><span style="color: #F6F6F4">         </span><span style="color: #62E884">_</span><span style="color: #F6F6F4"> </span><span style="color: #FFB86C; font-style: italic">target</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">Value</span><span style="color: #F6F6F4">,</span></span>
<span class="line"><span style="color: #F6F6F4">         </span><span style="color: #62E884; font-style: italic">errorMessage</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4">,</span></span>
<span class="line"><span style="color: #F6F6F4">         </span><span style="color: #62E884; font-style: italic">timeout</span><span style="color: #F6F6F4">: TimeInterval </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">1.0</span></span>
<span class="line"><span style="color: #F6F6F4">     ) </span><span style="color: #F286C4">async</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">throws</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">         </span><span style="color: #F286C4">try</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE; font-style: italic">self</span><span style="color: #F6F6F4">.</span><span style="color: #97E1F1">waitUntilMatches</span><span style="color: #F6F6F4">({ </span><span style="color: #BF9EEE; font-style: italic">$0</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">==</span><span style="color: #F6F6F4"> target },</span></span>
<span class="line"><span style="color: #F6F6F4">                                         </span><span style="color: #97E1F1">errorMessage</span><span style="color: #F6F6F4">: errorMessage,</span></span>
<span class="line"><span style="color: #F6F6F4">                                         </span><span style="color: #97E1F1">timeout</span><span style="color: #F6F6F4">: timeout)</span></span>
<span class="line"><span style="color: #F6F6F4">     }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">  </span><span style="color: #F286C4">public</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">waitUntilMatches</span><span style="color: #F6F6F4">(</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #62E884">_</span><span style="color: #F6F6F4"> </span><span style="color: #FFB86C; font-style: italic">predicate</span><span style="color: #F6F6F4">: </span><span style="color: #F286C4">@escaping</span><span style="color: #F6F6F4"> (</span><span style="color: #97E1F1; font-style: italic">Value</span><span style="color: #F6F6F4">) </span><span style="color: #F286C4">-&gt;</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">Bool</span><span style="color: #F6F6F4">,</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #62E884; font-style: italic">errorMessage</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4">,</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #62E884; font-style: italic">timeout</span><span style="color: #F6F6F4">: TimeInterval </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">1.0</span></span>
<span class="line"><span style="color: #F6F6F4">    ) </span><span style="color: #F286C4">async</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">throws</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> subject </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> PassthroughSubject</span><span style="color: #F286C4">&lt;</span><span style="color: #97E1F1; font-style: italic">Value</span><span style="color: #F6F6F4">, </span><span style="color: #97E1F1; font-style: italic">Error</span><span style="color: #F286C4">&gt;</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> cancellables </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">Set</span><span style="color: #F286C4">&lt;</span><span style="color: #F6F6F4">AnyCancellable</span><span style="color: #F286C4">&gt;</span><span style="color: #F6F6F4">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> timeoutPublisher </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> Fail</span><span style="color: #F286C4">&lt;</span><span style="color: #97E1F1; font-style: italic">Value</span><span style="color: #F6F6F4">, </span><span style="color: #97E1F1; font-style: italic">Error</span><span style="color: #F286C4">&gt;</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">error</span><span style="color: #F6F6F4">: WaitUntilError.</span><span style="color: #97E1F1">timeout</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">description</span><span style="color: #F6F6F4">: errorMessage))</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">delay</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">for</span><span style="color: #F6F6F4">: .</span><span style="color: #97E1F1">seconds</span><span style="color: #F6F6F4">(timeout), </span><span style="color: #97E1F1">scheduler</span><span style="color: #F6F6F4">: RunLoop.main)</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">eraseToAnyPublisher</span><span style="color: #F6F6F4">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> valuePublisher </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE; font-style: italic">self</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">mapError</span><span style="color: #F6F6F4"> { </span><span style="color: #BF9EEE; font-style: italic">$0</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">as</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">Error</span><span style="color: #F6F6F4"> }</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">eraseToAnyPublisher</span><span style="color: #F6F6F4">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        valuePublisher</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">merge</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">with</span><span style="color: #F6F6F4">: timeoutPublisher)</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">sink</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">receiveCompletion</span><span style="color: #F6F6F4">: { completion </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #F286C4">if</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .</span><span style="color: #97E1F1">failure</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> error) </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> completion {</span></span>
<span class="line"><span style="color: #F6F6F4">                    subject.</span><span style="color: #97E1F1">send</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">completion</span><span style="color: #F6F6F4">: .</span><span style="color: #97E1F1">failure</span><span style="color: #F6F6F4">(error))</span></span>
<span class="line"><span style="color: #F6F6F4">                }</span></span>
<span class="line"><span style="color: #F6F6F4">            }, </span><span style="color: #97E1F1">receiveValue</span><span style="color: #F6F6F4">: { value </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #F286C4">if</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">predicate</span><span style="color: #F6F6F4">(value) {</span></span>
<span class="line"><span style="color: #F6F6F4">                    subject.</span><span style="color: #97E1F1">send</span><span style="color: #F6F6F4">(value)</span></span>
<span class="line"><span style="color: #F6F6F4">                    subject.</span><span style="color: #97E1F1">send</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">completion</span><span style="color: #F6F6F4">: .finished)</span></span>
<span class="line"><span style="color: #F6F6F4">                }</span></span>
<span class="line"><span style="color: #F6F6F4">            })</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">store</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">in</span><span style="color: #F6F6F4">: </span><span style="color: #F286C4">&amp;</span><span style="color: #F6F6F4">cancellables)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">for</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">try</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">_</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">in</span><span style="color: #F6F6F4"> subject.</span><span style="color: #BF9EEE">values</span><span style="color: #F6F6F4"> {}</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">public</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">TestTimeoutError</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">Error</span><span style="color: #F6F6F4">, </span><span style="color: #97E1F1; font-style: italic">Equatable </span><span style="color: #F6F6F4">{}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">public</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">enum</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">WaitUntilError</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">Error</span><span style="color: #F6F6F4">, </span><span style="color: #97E1F1; font-style: italic">CustomStringConvertible</span><span style="color: #F6F6F4">, </span><span style="color: #97E1F1; font-style: italic">Equatable </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> timeout(</span><span style="color: #FFB86C; font-style: italic">description</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">public</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> description: </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">switch</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE; font-style: italic">self</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .</span><span style="color: #97E1F1">timeout</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> message)</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">&#x23f0; Timeout: (message)</span><span style="color: #DEE492">&quot;</span></span>
<span class="line"><span style="color: #F6F6F4">        }</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:17px" aria-hidden="true" class="wp-block-spacer"></div>



<p>This is an extensin to @Pubisher. I need to make my test function async.<br>Now I can write test code like so:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="@MainActor
@Test
func rootScreenPreference_is_Captured() async throws {
    //–– 1. Create the observer
    let observer = LensObserver()

    //–– 2. Wrap ContentView in an onPreferenceChange
    let testView = ContentView()
        .onPreferenceChange(LensCaptureKey.self) {
            observer.lensCaptures = $0
        }

    //–– 3. Host it in a window inside UIHostingController
    let hostingController = UIHostingController(rootView: testView)
   ...

    //–– 4. wait for SwiftUI render
    try await observer.$lensCaptures.waitUntilMatches({ $0.contains(&quot;screen.rootview&quot;) },
                                                      errorMessage: &quot;We should see the rootview&quot;)

    //observer.$lensCaptures.assertNoFailure()
    //–– 5. Assert that &quot;screen.rootview&quot; was emitted
    #expect(observer.lensCaptures.contains(&quot;screen.rootview&quot;))
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">@MainActor</span></span>
<span class="line"><span style="color: #F286C4">@Test</span></span>
<span class="line"><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">rootScreenPreference_is_Captured</span><span style="color: #F6F6F4">() </span><span style="color: #F286C4">async</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">throws</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">//–– 1. Create the observer</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> observer </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">LensObserver</span><span style="color: #F6F6F4">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">//–– 2. Wrap ContentView in an onPreferenceChange</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> testView </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">ContentView</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        .</span><span style="color: #97E1F1">onPreferenceChange</span><span style="color: #F6F6F4">(LensCaptureKey.</span><span style="color: #F286C4">self</span><span style="color: #F6F6F4">) {</span></span>
<span class="line"><span style="color: #F6F6F4">            observer.lensCaptures </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE; font-style: italic">$0</span></span>
<span class="line"><span style="color: #F6F6F4">        }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">//–– 3. Host it in a window inside UIHostingController</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> hostingController </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">UIHostingController</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">rootView</span><span style="color: #F6F6F4">: testView)</span></span>
<span class="line"><span style="color: #F6F6F4">   </span><span style="color: #F286C4">...</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">//–– 4. wait for SwiftUI render</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">try</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> observer.$lensCaptures.</span><span style="color: #97E1F1">waitUntilMatches</span><span style="color: #F6F6F4">({ </span><span style="color: #BF9EEE; font-style: italic">$0</span><span style="color: #F6F6F4">.</span><span style="color: #97E1F1">contains</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">screen.rootview</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) },</span></span>
<span class="line"><span style="color: #F6F6F4">                                                      </span><span style="color: #97E1F1">errorMessage</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">We should see the rootview</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">//observer.$lensCaptures.assertNoFailure()</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">//–– 5. Assert that &quot;screen.rootview&quot; was emitted</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">#expect</span><span style="color: #F6F6F4">(observer.lensCaptures.</span><span style="color: #97E1F1">contains</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">screen.rootview</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">))</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:26px" aria-hidden="true" class="wp-block-spacer"></div>



<p><strong>What changed?</strong></p>



<ul class="wp-block-list">
<li>We removed the RunLoop.main.run(&#8230;) hack.</li>



<li>waitUntilMatches polls our published tags array until it sees the desired ID or times out.</li>



<li>Tests now wait <strong>just long enough</strong>—no more, no less.</li>
</ul>



<div style="height:36px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">Refactoring for better testing workflows</h2>



<p>You migh think that the code from the above test is very verbose and you are right. But luckily most of it is boilerplate that every test will need. I will create a separate instance that acts like the <code>WorkBench</code> where the view is hosted and connected to the observe. Create a new file in your test target and add this:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="import SwiftUI
import TestingWithPreferencesProject

@MainActor
public struct LensWorkBench {

    public let observer: LensObserver
    public var hostingController: UIViewController?

    public init&lt;Content: View&gt;(
        @ViewBuilder content: (_ sut: LensWorkBench) -&gt; Content
    ) {
        let observer = LensObserver()
        self.observer = observer

        let window = {
            let frame = UIScreen.main.bounds
            let window = UIWindow(frame: frame)
            let rootVC = UIViewController()
            window.rootViewController = rootVC
            window.makeKeyAndVisible()
            return window
        }()

        let rootView = content(self)
            .onPreferenceChange(LensCaptureKey.self) { metas in
                observer.lensCaptures = metas
            }

        let hostingController = UIHostingController(rootView: rootView)
        self.hostingController = hostingController

        // Add as child of root view controller
        let rootVC = window.rootViewController!
        hostingController.view.translatesAutoresizingMaskIntoConstraints = false

        // Add to parent
        hostingController.willMove(toParent: rootVC)
        rootVC.addChild(hostingController)
        rootVC.view.addSubview(hostingController.view)

        // Setup constraints
        NSLayoutConstraint.activate([
            hostingController.view.leadingAnchor.constraint(equalTo: rootVC.view.leadingAnchor),
            hostingController.view.topAnchor.constraint(equalTo: rootVC.view.topAnchor),
            hostingController.view.widthAnchor.constraint(equalTo: rootVC.view.widthAnchor),
            hostingController.view.heightAnchor.constraint(equalTo: rootVC.view.heightAnchor)
        ])

        hostingController.didMove(toParent: rootVC)
        window.layoutIfNeeded()
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">SwiftUI</span></span>
<span class="line"><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">TestingWithPreferencesProject</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">@MainActor</span></span>
<span class="line"><span style="color: #F286C4">public</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">LensWorkBench</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">public</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> observer: LensObserver</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">public</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> hostingController: UIViewController</span><span style="color: #F286C4">?</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">public</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">init</span><span style="color: #F6F6F4">&lt;</span><span style="color: #BF9EEE; font-style: italic">Content</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View</span><span style="color: #F6F6F4">&gt;(</span></span>
<span class="line"><span style="color: #F6F6F4">        @</span><span style="color: #62E884">ViewBuilder</span><span style="color: #F6F6F4"> </span><span style="color: #FFB86C; font-style: italic">content</span><span style="color: #F6F6F4">: (_ sut: LensWorkBench) </span><span style="color: #F286C4">-&gt;</span><span style="color: #F6F6F4"> Content</span></span>
<span class="line"><span style="color: #F6F6F4">    ) {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> observer </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">LensObserver</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #BF9EEE; font-style: italic">self</span><span style="color: #F6F6F4">.observer </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> observer</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> window </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> frame </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> UIScreen.main.bounds</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> window </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">UIWindow</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">frame</span><span style="color: #F6F6F4">: frame)</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> rootVC </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">UIViewController</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">            window.rootViewController </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> rootVC</span></span>
<span class="line"><span style="color: #F6F6F4">            window.</span><span style="color: #97E1F1">makeKeyAndVisible</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> window</span></span>
<span class="line"><span style="color: #F6F6F4">        }()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> rootView </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">content</span><span style="color: #F6F6F4">(</span><span style="color: #BF9EEE; font-style: italic">self</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">onPreferenceChange</span><span style="color: #F6F6F4">(LensCaptureKey.</span><span style="color: #F286C4">self</span><span style="color: #F6F6F4">) { metas </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">                observer.lensCaptures </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> metas</span></span>
<span class="line"><span style="color: #F6F6F4">            }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> hostingController </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">UIHostingController</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">rootView</span><span style="color: #F6F6F4">: rootView)</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #BF9EEE; font-style: italic">self</span><span style="color: #F6F6F4">.hostingController </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> hostingController</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// Add as child of root view controller</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> rootVC </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> window.rootViewController</span><span style="color: #F286C4">!</span></span>
<span class="line"><span style="color: #F6F6F4">        hostingController.</span><span style="color: #BF9EEE">view</span><span style="color: #F6F6F4">.translatesAutoresizingMaskIntoConstraints </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">false</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// Add to parent</span></span>
<span class="line"><span style="color: #F6F6F4">        hostingController.</span><span style="color: #97E1F1">willMove</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">toParent</span><span style="color: #F6F6F4">: rootVC)</span></span>
<span class="line"><span style="color: #F6F6F4">        rootVC.</span><span style="color: #97E1F1">addChild</span><span style="color: #F6F6F4">(hostingController)</span></span>
<span class="line"><span style="color: #F6F6F4">        rootVC.</span><span style="color: #BF9EEE">view</span><span style="color: #F6F6F4">.</span><span style="color: #97E1F1">addSubview</span><span style="color: #F6F6F4">(hostingController.</span><span style="color: #BF9EEE">view</span><span style="color: #F6F6F4">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// Setup constraints</span></span>
<span class="line"><span style="color: #F6F6F4">        NSLayoutConstraint.</span><span style="color: #97E1F1">activate</span><span style="color: #F6F6F4">([</span></span>
<span class="line"><span style="color: #F6F6F4">            hostingController.</span><span style="color: #BF9EEE">view</span><span style="color: #F6F6F4">.leadingAnchor.</span><span style="color: #97E1F1">constraint</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">equalTo</span><span style="color: #F6F6F4">: rootVC.</span><span style="color: #BF9EEE">view</span><span style="color: #F6F6F4">.leadingAnchor),</span></span>
<span class="line"><span style="color: #F6F6F4">            hostingController.</span><span style="color: #BF9EEE">view</span><span style="color: #F6F6F4">.topAnchor.</span><span style="color: #97E1F1">constraint</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">equalTo</span><span style="color: #F6F6F4">: rootVC.</span><span style="color: #BF9EEE">view</span><span style="color: #F6F6F4">.topAnchor),</span></span>
<span class="line"><span style="color: #F6F6F4">            hostingController.</span><span style="color: #BF9EEE">view</span><span style="color: #F6F6F4">.widthAnchor.</span><span style="color: #97E1F1">constraint</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">equalTo</span><span style="color: #F6F6F4">: rootVC.</span><span style="color: #BF9EEE">view</span><span style="color: #F6F6F4">.widthAnchor),</span></span>
<span class="line"><span style="color: #F6F6F4">            hostingController.</span><span style="color: #BF9EEE">view</span><span style="color: #F6F6F4">.heightAnchor.</span><span style="color: #97E1F1">constraint</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">equalTo</span><span style="color: #F6F6F4">: rootVC.</span><span style="color: #BF9EEE">view</span><span style="color: #F6F6F4">.heightAnchor)</span></span>
<span class="line"><span style="color: #F6F6F4">        ])</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">        hostingController.</span><span style="color: #97E1F1">didMove</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">toParent</span><span style="color: #F6F6F4">: rootVC)</span></span>
<span class="line"><span style="color: #F6F6F4">        window.</span><span style="color: #97E1F1">layoutIfNeeded</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:29px" aria-hidden="true" class="wp-block-spacer"></div>



<p>with this utily in place your tests become very short and readable:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="@MainActor
@Test func rootScreenPreference_is_Captured() async throws {
    // --- GIVEN ---
    let sut = LensWorkBench { _ in
        ContentView()
    }

    // --- THEN ---
    try await sut.observer.$lensCaptures.waitUntilMatches({ $0.contains(&quot;screen.rootview&quot;) },
                                errorMessage: &quot;We should see the rootview&quot;)
} " style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">@MainActor</span></span>
<span class="line"><span style="color: #F286C4">@Test</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">rootScreenPreference_is_Captured</span><span style="color: #F6F6F4">() </span><span style="color: #F286C4">async</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">throws</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- GIVEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> sut </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">LensWorkBench</span><span style="color: #F6F6F4"> { </span><span style="color: #BF9EEE">_</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">ContentView</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- THEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">try</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> sut.observer.$lensCaptures.</span><span style="color: #97E1F1">waitUntilMatches</span><span style="color: #F6F6F4">({ </span><span style="color: #BF9EEE; font-style: italic">$0</span><span style="color: #F6F6F4">.</span><span style="color: #97E1F1">contains</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">screen.rootview</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) },</span></span>
<span class="line"><span style="color: #F6F6F4">                                </span><span style="color: #97E1F1">errorMessage</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">We should see the rootview</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">} </span></span></code></pre></div>



<div style="height:29px" aria-hidden="true" class="wp-block-spacer"></div>



<p>You can further shorted this by adding shortcuts to the observer like so:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="public final class LensObserver {
   @Published public var lensCaptures: [String] = []

    public func waitForViewVisible(withID id: String,
                            timeout: TimeInterval = 1.0) async throws {
        try await $lensCaptures.waitUntilMatches(
            { $0.contains { $0 == id } },
            errorMessage: &quot;Expected view visible with identifier: (id)&quot;,
            timeout: timeout
        )
    }

    public func waitForViewHidden(withID id: String,
                           timeout: TimeInterval = 1.0) async throws {
        try await $lensCaptures.waitUntilMatches(
            { !$0.contains { $0 == id } },
            errorMessage: &quot;Expected view hidden with identifier: (id)&quot;,
            timeout: timeout
        )
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">public</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">final</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">class</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">LensObserver</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">   </span><span style="color: #F286C4">@Published</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">public</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> lensCaptures: [</span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4">] </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> []</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">public</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">waitForViewVisible</span><span style="color: #F6F6F4">(</span><span style="color: #62E884">withID</span><span style="color: #F6F6F4"> </span><span style="color: #FFB86C; font-style: italic">id</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4">,</span></span>
<span class="line"><span style="color: #F6F6F4">                            </span><span style="color: #62E884; font-style: italic">timeout</span><span style="color: #F6F6F4">: TimeInterval </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">1.0</span><span style="color: #F6F6F4">) </span><span style="color: #F286C4">async</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">throws</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">try</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> $lensCaptures.</span><span style="color: #97E1F1">waitUntilMatches</span><span style="color: #F6F6F4">(</span></span>
<span class="line"><span style="color: #F6F6F4">            { </span><span style="color: #BF9EEE; font-style: italic">$0</span><span style="color: #F6F6F4">.</span><span style="color: #97E1F1">contains</span><span style="color: #F6F6F4"> { </span><span style="color: #BF9EEE; font-style: italic">$0</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">==</span><span style="color: #F6F6F4"> id } },</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">errorMessage</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Expected view visible with identifier: (id)</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">,</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">timeout</span><span style="color: #F6F6F4">: timeout</span></span>
<span class="line"><span style="color: #F6F6F4">        )</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">public</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">waitForViewHidden</span><span style="color: #F6F6F4">(</span><span style="color: #62E884">withID</span><span style="color: #F6F6F4"> </span><span style="color: #FFB86C; font-style: italic">id</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4">,</span></span>
<span class="line"><span style="color: #F6F6F4">                           </span><span style="color: #62E884; font-style: italic">timeout</span><span style="color: #F6F6F4">: TimeInterval </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">1.0</span><span style="color: #F6F6F4">) </span><span style="color: #F286C4">async</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">throws</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">try</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> $lensCaptures.</span><span style="color: #97E1F1">waitUntilMatches</span><span style="color: #F6F6F4">(</span></span>
<span class="line"><span style="color: #F6F6F4">            { </span><span style="color: #F286C4">!</span><span style="color: #BF9EEE; font-style: italic">$0</span><span style="color: #F6F6F4">.</span><span style="color: #97E1F1">contains</span><span style="color: #F6F6F4"> { </span><span style="color: #BF9EEE; font-style: italic">$0</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">==</span><span style="color: #F6F6F4"> id } },</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">errorMessage</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Expected view hidden with identifier: (id)</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">,</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">timeout</span><span style="color: #F6F6F4">: timeout</span></span>
<span class="line"><span style="color: #F6F6F4">        )</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:29px" aria-hidden="true" class="wp-block-spacer"></div>



<p>This allows you to write tests as short as this:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="@MainActor
@Test func rootScreenPreference_is_Captured() async throws {
    // --- GIVEN ---
    let sut = LensWorkBench { _ in
        ContentView()
    }

    // --- THEN ---
    try await sut.observer.waitForViewVisible(withID: &quot;screen.rootview&quot;)
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">@MainActor</span></span>
<span class="line"><span style="color: #F286C4">@Test</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">rootScreenPreference_is_Captured</span><span style="color: #F6F6F4">() </span><span style="color: #F286C4">async</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">throws</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- GIVEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> sut </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">LensWorkBench</span><span style="color: #F6F6F4"> { </span><span style="color: #BF9EEE">_</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">ContentView</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- THEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">try</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> sut.observer.</span><span style="color: #97E1F1">waitForViewVisible</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">withID</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">screen.rootview</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:29px" aria-hidden="true" class="wp-block-spacer"></div>



<p>With this async helper in place, your tests become deterministic and fast. Next up: let’s wire in a tiny <strong>NavigationCoordinator</strong> and see how we can <strong>programmatically</strong> push and pop screens—and test it with the PreferenceKey tags.</p>



<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">Programmatic Navigation with an Injectable Coordinator</h2>



<p>Now that we’ve ditched fixed delays and can <strong>await</strong> for preference tags, let’s introduce a minimal navigation coordinator so our tests can drive pushes and pops <strong>programmatically</strong>. We’ll refactor our ContentView to accept an external NavigationCoordinator and then write an async test that:</p>



<ol class="wp-block-list">
<li>Boots the view.</li>



<li>Waits for the &#8220;screen.rootview&#8221; tag.</li>



<li>Calls coordinator.showDetail(0).</li>



<li>Waits for the &#8220;screen.detailview.0&#8221; tag.</li>



<li>Calls coordinator.popToRoot().</li>



<li>Waits again for only the root tag.</li>
</ol>



<div style="height:44px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Refactor ContentView to Inject the Coordinator</h3>



<p>I will define simple coordinator driving a NavigationStack and add 2 functions for popToRoot and showDetail:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="final class NavigationCoordinator: ObservableObject {
    @Published var path = NavigationPath()

    func showDetail(_ item: Int) {
        path.append(item)
    }

    func popToRoot() {
        path = NavigationPath()
    }
}

struct ContentView: View {

    @ObservedObject var coordinator: NavigationCoordinator

    var body: some View {
        NavigationStack(path: $coordinator.path) {
            RootView()
                .navigationTitle(&quot;Root&quot;)
                .navigationDestination(for: Int.self) { item in
                    DetailView(item: item)
                }
        }
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">final</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">class</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">NavigationCoordinator</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">ObservableObject </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@Published</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> path </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">NavigationPath</span><span style="color: #F6F6F4">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">showDetail</span><span style="color: #F6F6F4">(</span><span style="color: #62E884">_</span><span style="color: #F6F6F4"> </span><span style="color: #FFB86C; font-style: italic">item</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">Int</span><span style="color: #F6F6F4">) {</span></span>
<span class="line"><span style="color: #F6F6F4">        path.</span><span style="color: #97E1F1">append</span><span style="color: #F6F6F4">(item)</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">popToRoot</span><span style="color: #F6F6F4">() {</span></span>
<span class="line"><span style="color: #F6F6F4">        path </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">NavigationPath</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ContentView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@ObservedObject</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> coordinator: NavigationCoordinator</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">NavigationStack</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">path</span><span style="color: #F6F6F4">: $coordinator.path) {</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">RootView</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">                .</span><span style="color: #97E1F1">navigationTitle</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Root</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">                .</span><span style="color: #97E1F1">navigationDestination</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">for</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">Int</span><span style="color: #F6F6F4">.</span><span style="color: #F286C4">self</span><span style="color: #F6F6F4">) { item </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #97E1F1">DetailView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">item</span><span style="color: #F6F6F4">: item)</span></span>
<span class="line"><span style="color: #F6F6F4">                }</span></span>
<span class="line"><span style="color: #F6F6F4">        }</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:48px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Testing if the Programmtic Navigation to Detail View is Working</h3>



<p>I will write a new test function with the NavigationCoordinator:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="@MainActor
@Test func programmatically_navigating_to_detail_view() async throws {
    // --- GIVEN ---
    let coordinator = NavigationCoordinator()
    let sut = LensWorkBench { _ in
        ContentView(coordinator: coordinator)
    }

    try await sut.observer.$lensCaptures.waitUntilMatches({ $0.contains(&quot;screen.rootview&quot;) },
                                                          errorMessage: &quot;We should see the rootview&quot;)
    let targetDetailId = 2

    // --- WHEN ---
    coordinator.showDetail(targetDetailId)

    // --- THEN ---
    try await sut.observer.waitForViewVisible(withID: &quot;screen.detailview.(targetDetailId)&quot;)
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">@MainActor</span></span>
<span class="line"><span style="color: #F286C4">@Test</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">programmatically_navigating_to_detail_view</span><span style="color: #F6F6F4">() </span><span style="color: #F286C4">async</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">throws</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- GIVEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> coordinator </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">NavigationCoordinator</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> sut </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">LensWorkBench</span><span style="color: #F6F6F4"> { </span><span style="color: #BF9EEE">_</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">ContentView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">coordinator</span><span style="color: #F6F6F4">: coordinator)</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">try</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> sut.observer.$lensCaptures.</span><span style="color: #97E1F1">waitUntilMatches</span><span style="color: #F6F6F4">({ </span><span style="color: #BF9EEE; font-style: italic">$0</span><span style="color: #F6F6F4">.</span><span style="color: #97E1F1">contains</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">screen.rootview</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) },</span></span>
<span class="line"><span style="color: #F6F6F4">                                                          </span><span style="color: #97E1F1">errorMessage</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">We should see the rootview</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> targetDetailId </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">2</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- WHEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    coordinator.</span><span style="color: #97E1F1">showDetail</span><span style="color: #F6F6F4">(targetDetailId)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- THEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">try</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> sut.observer.</span><span style="color: #97E1F1">waitForViewVisible</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">withID</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">screen.detailview.(targetDetailId)</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:18px" aria-hidden="true" class="wp-block-spacer"></div>



<p>In the above code, I am setting up the SUT which is the ContentView with the coordinator. The action under test is the <code>showDetail(targetDetailId)</code> function. Then I am waiting for the observer to collect the identifier for the detail view.</p>



<p><strong>Why this works:</strong></p>



<ul class="wp-block-list">
<li>No magic, just your NavigationCoordinator, a preference tag on each view, and our AsyncPreferenceObserver.</li>



<li>waitForViewVisible polls until the desired tag shows up (or times out), so you never have to guess how long to sleep.</li>



<li>Your tests become <strong>fast</strong>, <strong>deterministic</strong>, and <strong>self‑documenting</strong>: you can read the steps top‑to‑bottom and see exactly what UI state you’re awaiting.</li>
</ul>



<div style="height:34px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Testing if the Programmtic Pop to Root is Working</h3>



<p>Let`s test now the pop to root function of the coordinator:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="@MainActor
@Test func programmatically_navigating_pop_to_root() async throws {
    // --- GIVEN ---
    let coordinator = NavigationCoordinator()
    let sut = LensWorkBench { _ in
        ContentView(coordinator: coordinator)
    }

    let targetDetailId = 2
    let targetViewId = &quot;screen.detailview.(targetDetailId)&quot;
    coordinator.showDetail(targetDetailId)
    try await sut.observer.waitForViewVisible(withID: targetViewId)

    // --- WHEN ---
    coordinator.popToRoot()

    // --- THEN ---
    try await sut.observer.waitForViewHidden(withID: targetViewId)
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">@MainActor</span></span>
<span class="line"><span style="color: #F286C4">@Test</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">programmatically_navigating_pop_to_root</span><span style="color: #F6F6F4">() </span><span style="color: #F286C4">async</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">throws</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- GIVEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> coordinator </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">NavigationCoordinator</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> sut </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">LensWorkBench</span><span style="color: #F6F6F4"> { </span><span style="color: #BF9EEE">_</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">ContentView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">coordinator</span><span style="color: #F6F6F4">: coordinator)</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> targetDetailId </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">2</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> targetViewId </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">screen.detailview.(targetDetailId)</span><span style="color: #DEE492">&quot;</span></span>
<span class="line"><span style="color: #F6F6F4">    coordinator.</span><span style="color: #97E1F1">showDetail</span><span style="color: #F6F6F4">(targetDetailId)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">try</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> sut.observer.</span><span style="color: #97E1F1">waitForViewVisible</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">withID</span><span style="color: #F6F6F4">: targetViewId)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- WHEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    coordinator.</span><span style="color: #97E1F1">popToRoot</span><span style="color: #F6F6F4">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- THEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">try</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> sut.observer.</span><span style="color: #97E1F1">waitForViewHidden</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">withID</span><span style="color: #F6F6F4">: targetViewId)</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:22px" aria-hidden="true" class="wp-block-spacer"></div>



<p>First I need to start the workbench with the coordinator. Then I programmatically push to the detail view. After waiting for the views to be updated, I start the test and call <code>popToRoot</code>. As the test assertion, I check if the detail view is hidden.<br>Thats it you can test you navigation in a very clear and short. Each test is fast and if I run all 3 above tests in parallet it take</p>



<p><br><strong><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2714.png" alt="✔" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Test run with 3 tests passed after 0.130 seconds.</strong></p>



<div style="height:53px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">Is this really working</h2>



<p>When I tested this, I was honestly surprised how well this is working. It feels a bit like flying blindly. So I decice to add some snapshot tests. I will use this <a href="https://github.com/pointfreeco/swift-snapshot-testing" target="_blank" rel="noopener">library</a> and write a test where i navigate to the detail, wait for the view to appear and take the snapshot:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="@MainActor
@Test func snapshot_navigating_to_detail_view() async throws {
    // --- GIVEN ---
    let coordinator = NavigationCoordinator()
    let sut = LensWorkBench { _ in
        ContentView(coordinator: coordinator)
    }

    try await sut.observer.$lensCaptures.waitUntilMatches({ $0.contains(&quot;screen.rootview&quot;) },
                                                          errorMessage: &quot;We should see the rootview&quot;)
    let targetDetailId = 2

    // --- WHEN ---
    coordinator.showDetail(targetDetailId)

    // --- THEN ---
    try await sut.observer.waitForViewVisible(withID: &quot;screen.detailview.(targetDetailId)&quot;)
    assertSnapshots(of: sut.hostingController!, as: [.image], record: false)
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">@MainActor</span></span>
<span class="line"><span style="color: #F286C4">@Test</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">snapshot_navigating_to_detail_view</span><span style="color: #F6F6F4">() </span><span style="color: #F286C4">async</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">throws</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- GIVEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> coordinator </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">NavigationCoordinator</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> sut </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">LensWorkBench</span><span style="color: #F6F6F4"> { </span><span style="color: #BF9EEE">_</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">ContentView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">coordinator</span><span style="color: #F6F6F4">: coordinator)</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">try</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> sut.observer.$lensCaptures.</span><span style="color: #97E1F1">waitUntilMatches</span><span style="color: #F6F6F4">({ </span><span style="color: #BF9EEE; font-style: italic">$0</span><span style="color: #F6F6F4">.</span><span style="color: #97E1F1">contains</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">screen.rootview</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) },</span></span>
<span class="line"><span style="color: #F6F6F4">                                                          </span><span style="color: #97E1F1">errorMessage</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">We should see the rootview</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> targetDetailId </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">2</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- WHEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    coordinator.</span><span style="color: #97E1F1">showDetail</span><span style="color: #F6F6F4">(targetDetailId)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// --- THEN ---</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">try</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> sut.observer.</span><span style="color: #97E1F1">waitForViewVisible</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">withID</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">screen.detailview.(targetDetailId)</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">assertSnapshots</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">of</span><span style="color: #F6F6F4">: sut.hostingController</span><span style="color: #F286C4">!</span><span style="color: #F6F6F4">, </span><span style="color: #97E1F1">as</span><span style="color: #F6F6F4">: [.</span><span style="color: #BF9EEE">image</span><span style="color: #F6F6F4">], </span><span style="color: #97E1F1">record</span><span style="color: #F6F6F4">: </span><span style="color: #BF9EEE">false</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:21px" aria-hidden="true" class="wp-block-spacer"></div>



<p>Note: snapshots are sometimes wired, I had to restart Xcode, clean the project and relaunch the testing to get proper screenshots.</p>



<p>Here is the result from my Xcode project</p>



<figure class="gb-block-image gb-block-image-34289b24"><img loading="lazy" decoding="async" width="1702" height="1400" class="gb-image gb-image-34289b24" src="https://www.swiftyplace.com/wp-content/uploads/2025/05/preference_testing_snapshot.webp" alt="" title="preference_testing_snapshot" srcset="https://www.swiftyplace.com/wp-content/uploads/2025/05/preference_testing_snapshot.webp 1702w, https://www.swiftyplace.com/wp-content/uploads/2025/05/preference_testing_snapshot-300x247.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2025/05/preference_testing_snapshot-1024x842.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2025/05/preference_testing_snapshot-768x632.webp 768w, https://www.swiftyplace.com/wp-content/uploads/2025/05/preference_testing_snapshot-1536x1263.webp 1536w" sizes="auto, (max-width: 1702px) 100vw, 1702px" /></figure>



<p>This is showing the expected ui from the detail view. Pretty cool <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<div style="height:56px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="gb-headline gb-headline-1b6e55d1 gb-headline-text">Why Preference Testing Avoids Flakiness</h2>



<ul class="wp-block-list">
<li>Traditional&nbsp;UI&nbsp;tests&nbsp;often&nbsp;rely&nbsp;on&nbsp;<em>assumptions</em>&nbsp;about&nbsp;when&nbsp;the&nbsp;UI&nbsp;will&nbsp;update (e.g.&nbsp;<code>.wait(for: 2)</code>&nbsp;or&nbsp;<code>XCTestExpectation</code>).</li>



<li>Observer&nbsp;uses&nbsp;<code>.onPreferenceChange</code>&nbsp;and&nbsp;tracks&nbsp;views&nbsp;through&nbsp;actual&nbsp;appearance/disappearance&nbsp;in&nbsp;the&nbsp;view&nbsp;hierarchy.</li>



<li>Your&nbsp;assertions (<code>waitForViewVisible</code>,&nbsp;<code>waitForViewCount</code>,&nbsp;etc.)&nbsp;wait&nbsp;<em>only</em>&nbsp;until&nbsp;the&nbsp;UI&nbsp;<strong>actually&nbsp;renders</strong>&nbsp;the&nbsp;state,&nbsp;regardless&nbsp;of&nbsp;what&nbsp;caused&nbsp;the&nbsp;delay (network,&nbsp;animation,&nbsp;debounce,&nbsp;etc.).</li>
</ul>



<p>That&nbsp;means:</p>



<ul class="wp-block-list">
<li>You&nbsp;can&nbsp;<strong>refactor&nbsp;internal&nbsp;logic</strong>&nbsp;without&nbsp;breaking&nbsp;tests.</li>



<li><strong>Race&nbsp;conditions</strong>&nbsp;are&nbsp;avoided&nbsp;because&nbsp;the&nbsp;test&nbsp;synchronizes&nbsp;with&nbsp;what&nbsp;the&nbsp;user&nbsp;would&nbsp;actually&nbsp;see.</li>
</ul>



<div style="height:56px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="gb-headline gb-headline-2ac16803 gb-headline-text">TDD with Preference &#8211; Reducing Brittleness</h2>



<p>With preferences as `tags` you can say “<em>something&nbsp;meaningful&nbsp;is&nbsp;visible&nbsp;here</em>” —&nbsp;then&nbsp;later&nbsp;add: “<em>and&nbsp;it’s&nbsp;a&nbsp;button</em>”</p>



<p>We started with observing the screens in the navigation stack. During test driven development, you might have placeholder views for some of your screens:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="Text(&quot;Placeholder View&quot;) 
     .preference(key: LensCaptureKey.self,
                 value: [&quot;screen.detail&quot;])" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #97E1F1">Text</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Placeholder View</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) </span></span>
<span class="line"><span style="color: #F6F6F4">     .</span><span style="color: #97E1F1">preference</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">key</span><span style="color: #F6F6F4">: LensCaptureKey.</span><span style="color: #F286C4">self</span><span style="color: #F6F6F4">,</span></span>
<span class="line"><span style="color: #F6F6F4">                 </span><span style="color: #97E1F1">value</span><span style="color: #F6F6F4">: [</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">screen.detail</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">])</span></span></code></pre></div>



<div style="height:14px" aria-hidden="true" class="wp-block-spacer"></div>



<p>You write a test for this tag. Later you decide to change the placeholder and implement the screen e.g.:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="struct ProductDetailScreen: View {
    
    let product: Product
    @State private var showAuth = false
    
    var body: some View {
        ScrollView {
            VStack(spacing: 20) {
                Image(product.image)
                
               Button(action: {
                  // call buy 
                }) {
                   Label(&quot;Buy Now&quot;, systemImage: &quot;cart.badge.plus&quot;)
               }
            }
        }
        .preference(key: LensCaptureKey.self,
                    value: [&quot;screen.detail&quot;])
    }
}
   " style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ProductDetailScreen</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> product: Product</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@State</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">private</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> showAuth </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">false</span></span>
<span class="line"><span style="color: #F6F6F4">    </span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">ScrollView</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">VStack</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">spacing</span><span style="color: #F6F6F4">: </span><span style="color: #BF9EEE">20</span><span style="color: #F6F6F4">) {</span></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #97E1F1">Image</span><span style="color: #F6F6F4">(product.</span><span style="color: #BF9EEE">image</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">                </span></span>
<span class="line"><span style="color: #F6F6F4">               </span><span style="color: #97E1F1">Button</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">action</span><span style="color: #F6F6F4">: {</span></span>
<span class="line"><span style="color: #F6F6F4">                  </span><span style="color: #7B7F8B">// call buy </span></span>
<span class="line"><span style="color: #F6F6F4">                }) {</span></span>
<span class="line"><span style="color: #F6F6F4">                   </span><span style="color: #97E1F1">Label</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Buy Now</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">, </span><span style="color: #97E1F1">systemImage</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">cart.badge.plus</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">               }</span></span>
<span class="line"><span style="color: #F6F6F4">            }</span></span>
<span class="line"><span style="color: #F6F6F4">        }</span></span>
<span class="line"><span style="color: #F6F6F4">        .</span><span style="color: #97E1F1">preference</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">key</span><span style="color: #F6F6F4">: LensCaptureKey.</span><span style="color: #F286C4">self</span><span style="color: #F6F6F4">,</span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #97E1F1">value</span><span style="color: #F6F6F4">: [</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">screen.detail</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">])</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span>
<span class="line"><span style="color: #F6F6F4">   </span></span></code></pre></div>



<div style="height:14px" aria-hidden="true" class="wp-block-spacer"></div>



<p>Your first test against the &#8220;screen.detail&#8221; tag is still working because you did not use an assertion against an implementation detail.</p>



<p>You want to further define the test and make sure the &#8220;Buy Now&#8221; button is visible. </p>



<div style="height:23px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="gb-headline gb-headline-2028dee0 gb-headline-text">Grouping Preference Values</h3>



<p>Preference keys are propagaged and reduced in the view hierarchy.  You can use <strong>transformPreference</strong> to group individual preference values. I first change the preference value to include a child array:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="public struct LensCapture: Equatable {
    public let viewType: String
    public let identifier: String
    public var info: [String: AnyHashable]
    public var children: [LensCapture] = [] // use transformPreferences
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">public</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">LensCapture</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">Equatable </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">public</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> viewType: </span><span style="color: #97E1F1; font-style: italic">String</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">public</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> identifier: </span><span style="color: #97E1F1; font-style: italic">String</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">public</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> info: [</span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">AnyHashable</span><span style="color: #F6F6F4">]</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">public</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> children: [LensCapture] </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> [] </span><span style="color: #7B7F8B">// use transformPreferences</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:15px" aria-hidden="true" class="wp-block-spacer"></div>



<p>And then change the view like so:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="ScrollView {
    VStack(spacing: 20) {
        Image(product.image)
        
       Button(action: {
          // call buy
        }) {
           Label(&quot;Buy Now&quot;, systemImage: &quot;cart.badge.plus&quot;)
       }
       .preference(key: LensCaptureKey.self,
                   value: [LensCapture(identifier: &quot;screen.detail.buy.button&quot;,
                                       children: [])
    }
    .onPreferenceChange(LensCaptureKey.self) { values in
        values = [LensCapture(identifier: &quot;screen.detail&quot;,
                              children: values]
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #97E1F1">ScrollView</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">VStack</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">spacing</span><span style="color: #F6F6F4">: </span><span style="color: #BF9EEE">20</span><span style="color: #F6F6F4">) {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">Image</span><span style="color: #F6F6F4">(product.</span><span style="color: #BF9EEE">image</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">        </span></span>
<span class="line"><span style="color: #F6F6F4">       </span><span style="color: #97E1F1">Button</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">action</span><span style="color: #F6F6F4">: {</span></span>
<span class="line"><span style="color: #F6F6F4">          </span><span style="color: #7B7F8B">// call buy</span></span>
<span class="line"><span style="color: #F6F6F4">        }) {</span></span>
<span class="line"><span style="color: #F6F6F4">           </span><span style="color: #97E1F1">Label</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Buy Now</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">, </span><span style="color: #97E1F1">systemImage</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">cart.badge.plus</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">       }</span></span>
<span class="line"><span style="color: #F6F6F4">       .</span><span style="color: #97E1F1">preference</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">key</span><span style="color: #F6F6F4">: LensCaptureKey.</span><span style="color: #F286C4">self</span><span style="color: #F6F6F4">,</span></span>
<span class="line"><span style="color: #F6F6F4">                   </span><span style="color: #97E1F1">value</span><span style="color: #F6F6F4">: [</span><span style="color: #97E1F1">LensCapture</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">identifier</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">screen.detail.buy.button</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">,</span></span>
<span class="line"><span style="color: #F6F6F4">                                       </span><span style="color: #97E1F1">children</span><span style="color: #F6F6F4">: [])</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">    .</span><span style="color: #97E1F1">onPreferenceChange</span><span style="color: #F6F6F4">(LensCaptureKey.</span><span style="color: #F286C4">self</span><span style="color: #F6F6F4">) { values </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">        values </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> [</span><span style="color: #97E1F1">LensCapture</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">identifier</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">screen.detail</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">,</span></span>
<span class="line"><span style="color: #F6F6F4">                              </span><span style="color: #97E1F1">children</span><span style="color: #F6F6F4">: values]</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:15px" aria-hidden="true" class="wp-block-spacer"></div>



<p>I then can write my test assertions agains these different levels of scope: </p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="@MainActor
@Test
func show_buy_button() {
    let sut = LensWorkBench { _ in
        ProductDetailScreen()
    }
    
    try await sut.observer.waitForViewVisible(withID: &quot;screen.detail&quot;)
    try await sut.observer.waitForViewVisible(withID: &quot;screen.detail.buy.button&quot;)
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">@MainActor</span></span>
<span class="line"><span style="color: #F286C4">@Test</span></span>
<span class="line"><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">show_buy_button</span><span style="color: #F6F6F4">() {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> sut </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">LensWorkBench</span><span style="color: #F6F6F4"> { </span><span style="color: #BF9EEE">_</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">ProductDetailScreen</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">    </span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">try</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> sut.observer.</span><span style="color: #97E1F1">waitForViewVisible</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">withID</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">screen.detail</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">try</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> sut.observer.</span><span style="color: #97E1F1">waitForViewVisible</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">withID</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">screen.detail.buy.button</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:15px" aria-hidden="true" class="wp-block-spacer"></div>



<p>I am using the following pattern that helps me avoid brittle tests:</p>



<ul class="wp-block-list">
<li><strong>Layered&nbsp;identifiers</strong>&nbsp;(via&nbsp;<code>onPreferenceChange</code>&nbsp;and&nbsp;<code>preference</code>)</li>



<li>Lets&nbsp;you&nbsp;say: “<em>something&nbsp;meaningful&nbsp;is&nbsp;visible&nbsp;here</em>” —&nbsp;then&nbsp;later&nbsp;add: “<em>and&nbsp;it’s&nbsp;a&nbsp;button</em>”</li>



<li>You&nbsp;write&nbsp;tests&nbsp;<strong>while&nbsp;developing</strong>,&nbsp;not&nbsp;just&nbsp;after</li>



<li><strong>Tests&nbsp;adapt&nbsp;to&nbsp;UI&nbsp;evolution</strong>,&nbsp;not&nbsp;break&nbsp;on&nbsp;it</li>
</ul>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Think&nbsp;of&nbsp;it&nbsp;as&nbsp;<em>“progressive&nbsp;disclosure”&nbsp;for&nbsp;test&nbsp;intent</em>&nbsp;—&nbsp;start&nbsp;coarse,&nbsp;go&nbsp;detailed&nbsp;as&nbsp;the&nbsp;UI&nbsp;settles.</p>
</blockquote>



<p>This benefit of writing scoped tests, is probably one of my favorite approaches. It allows me to write and develop my code and write flexibile tests.</p>



<div style="height:56px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="gb-headline gb-headline-8676ef96 gb-headline-text">Whats Next</h2>



<p>I was very happy with the results from my test the test setup. I will in the future write more to show you:</p>



<ul class="wp-block-list">
<li>How to test for internal state like button disabled, testfield text</li>



<li>Creating scoped tests with assertions that don`t give you brittle tests</li>



<li>Simulate user interaction</li>
</ul>



<p>You can look at the open source package I published on Github here: <a href="https://github.com/gahntpo/SwiftLens" target="_blank" rel="noopener">SwiftLens</a>. You can have a look at the test cases to get more examples</p>



<p>Edge cases &amp; caveats: Note that PreferenceKeys only fire when a view is in the hierarchy, and that very large hierarchies may add minor overhead. Please choose only specific views/scopes for you tests and don`t add this blindly to everyview. I prefere to use these specifically for conditional views e.g. inside a if or switch statement.</p>



<div style="height:56px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="gb-headline gb-headline-e50971cd gb-headline-text">Summary</h2>



<p>With just a few small pieces in place—PreferenceKeys tagging your views, an async observer to await tags, and a minimal hosting setup—you get:</p>



<ul class="wp-block-list">
<li><strong>Blazing‑fast, in‑process test</strong>s  No simulator launch, no XCUITest. Your suite runs in milliseconds.</li>



<li><strong>Scoped &amp; deterministic</strong> You only observe exactly the screens or controls you tag, and waitUntil… ensures you never race or over‑sleep.</li>



<li><strong>Zero flakiness </strong>Polling your published preference array beats hard‑coded delays every time.</li>



<li><strong>Minimal boilerplate</strong>  A tiny LensWorkBench wrapper and a handful of helpers keep your test code concise and readable.</li>



<li><strong>Full control over navigation</strong> Programmatic pushes and pops become first‑class test actions, and you can await each state transition.</li>



<li><strong>Ready to extend </strong>The same pattern works for loading spinners, empty states, toggle and text‑field states, sheets, full‑screen covers—you name it.</li>
</ul>
<p>The post <a rel="nofollow" href="https://www.swiftyplace.com/blog/swiftui-testing-with-preferencekeys">Can You use PreferenceKeys for Testing SwiftUI Views</a> appeared first on <a rel="nofollow" href="https://www.swiftyplace.com">swiftyplace</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.swiftyplace.com/blog/swiftui-testing-with-preferencekeys/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Why We Keep Avoiding Tests in iOS—And What the Tools Should Do About It</title>
		<link>https://www.swiftyplace.com/blog/testing-in-ios-development?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=testing-in-ios-development</link>
					<comments>https://www.swiftyplace.com/blog/testing-in-ios-development#comments</comments>
		
		<dc:creator><![CDATA[Karin Prater]]></dc:creator>
		<pubDate>Fri, 09 May 2025 08:33:06 +0000</pubDate>
				<category><![CDATA[Testing in iOS development]]></category>
		<category><![CDATA[Swift Testing]]></category>
		<category><![CDATA[Unit Tests]]></category>
		<guid isPermaLink="false">https://www.swiftyplace.com/?p=1005510</guid>

					<description><![CDATA[<p>Let&#8217;s be honest—many of us avoid writing tests for our iOS apps. Not because we don&#8217;t care about quality, but because the current tooling often feels like it&#8217;s working against us rather than with us. ... <a title="Why We Keep Avoiding Tests in iOS—And What the Tools Should Do About It" class="read-more" href="https://www.swiftyplace.com/blog/testing-in-ios-development" aria-label="More on Why We Keep Avoiding Tests in iOS—And What the Tools Should Do About It">Read more</a></p>
<p>The post <a rel="nofollow" href="https://www.swiftyplace.com/blog/testing-in-ios-development">Why We Keep Avoiding Tests in iOS—And What the Tools Should Do About It</a> appeared first on <a rel="nofollow" href="https://www.swiftyplace.com">swiftyplace</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Let&#8217;s be honest—many of us avoid <a href="https://developer.apple.com/documentation/xcode/testing" target="_blank" rel="noopener">writing tests for our iOS apps</a>. Not because we don&#8217;t care about quality, but because the current tooling often feels like it&#8217;s working against us rather than with us. The problem isn&#8217;t laziness; it&#8217;s practicality.</p>



<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">The Testing Paradox in iOS Development</h2>



<p>iOS is fundamentally a UI-first platform. Yet our testing ecosystem remains largely focused on backend-style logic testing. This disconnect creates a situation where we end up <strong>testing the stuff that rarely breaks</strong> (business logic) while <strong>avoiding tests for the stuff that breaks all the time</strong> (UI and interactions).<br>In my experience, the majority of bugs don&#8217;t come from business logic errors. They come from UI issues, state management mistakes, and incorrect component connections.<br>Yet where do we spend most of our testing effort? You guessed it—on business logic, the area that&#8217;s least likely to cause problems in production.</p>



<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">Apple&#8217;s Role in the Testing Ecosystem</h2>



<p>Let&#8217;s address the elephant in the room: Apple has consistently prioritized developer tooling that makes building apps easier, but testing remains something of an afterthought.<br>SwiftUI is a prime example. It revolutionized how we build UIs, but the testing story was minimal at launch and remains incomplete years later. While UIKit had its issues, at least we could inspect the view hierarchy and access individual elements.<br><strong>Apple&#8217;s reluctance to provide first-party testing solutions for UI behavior verification</strong> suggests they either don&#8217;t consider it a priority or don&#8217;t fully understand the day-to-day testing challenges developers face.</p>



<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">The Pain Points That Drive Us Away</h2>



<p>Let’s not sugarcoat it—most iOS tests suck to write and maintain. The common complaints:</p>



<ul class="wp-block-list">
<li><strong>Flaky</strong>: They fail randomly and shake our trust.</li>



<li><strong>Brittle</strong>: A small UI tweak breaks five unrelated tests.</li>



<li><strong>Slow</strong>: Especially with UI and integration tests on CI.</li>
</ul>



<p>This creates a vicious cycle: devs don’t trust tests → they stop writing tests → bugs increase → testing gets blamed → nothing improves. We&#8217;ve all seen it.</p>



<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">The State of iOS Testing Tools: What Works and What Doesn&#8217;t</h2>



<h3 class="wp-block-heading">XCTest: Still Solid for Unit Testing</h3>



<p>XCTest remains the foundation for unit testing in iOS. It&#8217;s:</p>



<ul class="wp-block-list">
<li>Fast and reliable for testing business logic</li>



<li>Well-integrated with Xcode</li>



<li>Recently improved with parameterized tests and async/await support</li>
</ul>



<p>For testing pure functions, services, and view models, it gets the job done—but it&#8217;s not designed for testing UI behavior.<br>What we actually need is the ability to run UI behavior tests within the XCTest framework that would execute quickly and reliably.</p>



<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Swift Testing: The New Kid on the Block</h3>



<p>Swift Testing represents Apple&#8217;s vision for the future of testing. It focuses on:</p>



<ul class="wp-block-list">
<li>Clarity and readability</li>



<li>Predictable behavior</li>



<li>Developer ergonomics</li>
</ul>



<p>The improvements are welcome, but the fundamental issues with UI testing remain unaddressed.</p>



<figure class="gb-block-image gb-block-image-e3c510f5"><img loading="lazy" decoding="async" width="1280" height="720" class="gb-image gb-image-e3c510f5" src="http://www.swiftyplace.com/wp-content/uploads/2025/05/ios_testing_example.webp" alt="unit testing with Swift testing in Xcode" title="ios_testing_example" srcset="https://www.swiftyplace.com/wp-content/uploads/2025/05/ios_testing_example.webp 1280w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ios_testing_example-300x169.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ios_testing_example-1024x576.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ios_testing_example-768x432.webp 768w" sizes="auto, (max-width: 1280px) 100vw, 1280px" /></figure>



<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">XCUITest: The Double-Edged Sword</h3>



<p>This one hurts the most. In theory, XCUITest should be amazing. It lets you drive your app like a user, tap buttons, type text, scroll through lists, and verify the results. In practice, though:</p>



<ul class="wp-block-list">
<li>Tests take forever to run</li>



<li>Tests break when UI elements move or change</li>



<li>Tests frequently fail due to timing issues or animations</li>



<li>Maintenance becomes a nightmare</li>
</ul>



<p>UI tests that pass locally will often fail on CI for reasons like timing issues, animations, or just randomness. You end up adding sleep calls or retry logic just to get through a test run. And because it’s so expensive to run these tests, we mostly stick to happy path coverage—&#8221;Can a user log in?&#8221;—and not much else. So yeah, technically covered. But not actually protected.</p>



<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">The SwiftUI Testing Conundrum</h2>



<p>SwiftUI changes the way we write UIs—but not the way we test them, at least not with Apple’s tooling. There’s still no official support for inspecting views, simulating bindings, or asserting visual hierarchy. You’re stuck either testing your view models or reaching for hacks..<br>SwiftUI is essentially a black box—unlike UIKit where you could inspect view controllers and their subviews.<br>What we need is the ability to:</p>



<ol class="wp-block-list">
<li>Mount SwiftUI views in an isolated test environment</li>



<li>Inspect the resulting view hierarchy</li>



<li>Interact with it programmatically</li>



<li>Make assertions about state and appearance</li>
</ol>



<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Community Solutions: Filling the Gaps</h3>



<p>The iOS community hasn&#8217;t been sitting idle while waiting for Apple to improve testing tools. Several notable community-driven solutions have emerged:</p>



<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">ViewInspector: Promising but Limited</h3>



<p><a href="https://www.google.com/url?sa=t&amp;source=web&amp;rct=j&amp;opi=89978449&amp;url=https://github.com/nalexn/ViewInspector&amp;ved=2ahUKEwi53auvvpONAxVcSPEDHbdGNrQQFnoECCsQAQ&amp;usg=AOvVaw1KpjDj4a_0ecD7wQ1jYNSO" target="_blank" rel="noopener">ViewInspector</a> provides XCTest integration for SwiftUI views with decent performance (4/5 on speed). It lets you:</p>



<ul class="wp-block-list">
<li>Inspect the presence of views</li>



<li>Check some state properties</li>



<li>Trigger basic interactions</li>
</ul>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="    import ViewInspector
    
    @MainActor
    func test_categoryRow_when_appear_then_category_title_visible() async throws {
        // ---- GIVEN ----
        let category =  Category.electronics
        
        // ---- WHEN ----
        let sut = CategoryRow(category: category)
        
        // ---- THEN ----
        _ = try sut.inspect().find(text: category.title)
    }" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ViewInspector</span></span>
<span class="line"><span style="color: #F6F6F4">    </span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@MainActor</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">test_categoryRow_when_appear_then_category_title_visible</span><span style="color: #F6F6F4">() </span><span style="color: #F286C4">async</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">throws</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// ---- GIVEN ----</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> category </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4">  Category.electronics</span></span>
<span class="line"><span style="color: #F6F6F4">        </span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// ---- WHEN ----</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> sut </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">CategoryRow</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">category</span><span style="color: #F6F6F4">: category)</span></span>
<span class="line"><span style="color: #F6F6F4">        </span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// ---- THEN ----</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #BF9EEE">_</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">try</span><span style="color: #F6F6F4"> sut.</span><span style="color: #97E1F1">inspect</span><span style="color: #F6F6F4">().</span><span style="color: #97E1F1">find</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">text</span><span style="color: #F6F6F4">: category.title)</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span></code></pre></div>



<p>But it&#8217;s &#8220;hacky&#8221; under the hood and breaks frequently with SwiftUI updates. It also can&#8217;t inspect many important states like button enablement or trigger NavigationLinks in a NavigationStack.</p>



<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Snapshot Testing: Good for Regression, Bad for TDD</h3>



<p>Snapshot testing tools like <a href="https://www.google.com/url?sa=t&amp;source=web&amp;rct=j&amp;opi=89978449&amp;url=https://github.com/pointfreeco/swift-snapshot-testing&amp;ved=2ahUKEwixrKa4vpONAxX_QvEDHaHIM5EQFnoECCYQAQ&amp;usg=AOvVaw1fzyVXsy6rhRGAmmZ0oVCC" target="_blank" rel="noopener">Point-Free&#8217;s SnapshotTesting</a> offer visual regression testing. They&#8217;re great for:</p>



<ul class="wp-block-list">
<li>Detecting layout changes</li>



<li>Verifying theming/appearance updates</li>



<li>Ensuring UI consistency</li>
</ul>



<p>When you run a snapshot test for the first time, you will have to take the snapshot and store it in your test target. These files can quickly add up in storage. You can also imaging that the images can vary strongly by device type:</p>



<div style="height:26px" aria-hidden="true" class="wp-block-spacer"></div>



<figure class="gb-block-image gb-block-image-86098d32"><img loading="lazy" decoding="async" width="1208" height="720" class="gb-image gb-image-86098d32" src="http://www.swiftyplace.com/wp-content/uploads/2025/05/ios_testing_example_snapshot.webp" alt="ios testing with snapshot testing for regression tests" title="ios_testing_example_snapshot" srcset="https://www.swiftyplace.com/wp-content/uploads/2025/05/ios_testing_example_snapshot.webp 1208w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ios_testing_example_snapshot-300x179.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ios_testing_example_snapshot-1024x610.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2025/05/ios_testing_example_snapshot-768x458.webp 768w" sizes="auto, (max-width: 1208px) 100vw, 1208px" /></figure>



<div style="height:26px" aria-hidden="true" class="wp-block-spacer"></div>



<p>⠀But they&#8217;re also:</p>



<ul class="wp-block-list">
<li>Brittle and extremely sensitive to minor changes</li>



<li>Difficult to use in CI pipelines</li>



<li>Not suitable for test-driven development</li>



<li>Slow compared to unit tests</li>
</ul>



<p>⠀My recommendation: Use sparingly and only when nothing else works.</p>



<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">What We Actually Need: The Requirements</h2>



<p>For SwiftUI testing to really work in the long run, we need tools that provide:</p>



<ol class="wp-block-list">
<li><strong>Speed</strong>: Tests should run quickly (under 100ms per test)</li>



<li><strong>Stability</strong>: No flakiness, no random failures</li>



<li><strong>Feedback</strong>: Clear information about what&#8217;s rendered and what state it&#8217;s in</li>



<li><strong>Direct Assertions</strong>: Ability to make precise assertions against views and state</li>



<li><strong>Interaction</strong>: Programmatically interact with views like a user would</li>



<li><strong>Maintainability</strong>: Tests that clearly show intent and what broke</li>
</ol>



<div style="height:21px" aria-hidden="true" class="wp-block-spacer"></div>



<p>In my next blog post, I&#8217;ll show you how we can start building a better testing experience for SwiftUI apps. I&#8217;ll introduce a pattern that:</p>



<ul class="wp-block-list">
<li>Uses XCTest/Swift Testing for speed</li>



<li>Mounts SwiftUI views in isolation</li>



<li>Provides direct feedback on rendered state</li>



<li>Allows for stable assertions and interactions</li>



<li>Remains maintainable as your app evolves</li>
</ul>



<p>Read about it here <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4cb.png" alt="📋" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <a href="https://www.swiftyplace.com/blog/swiftui-testing-with-preferencekeys">Discovering PreferenceKeys How I Accidentally Unlocked SwiftUI’s Secret Testing API</a></p>



<div style="height:33px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">Conclusion: Don’t Blame Yourself—Blame the Toolbox (a Little)</h2>



<p>Testing iOS apps isn’t inherently painful. It’s just that the tools often don’t align with the realities of frontend development. If you’ve felt that frustration—you’re not alone. It’s not that you’re doing it wrong. It’s that the toolbox still needs work.<br>Thankfully, change is happening. If Apple can double down on testability in SwiftUI and close the gap on tooling, we might finally get to a place where tests are as delightful as the apps we build.</p>



<p><br>What patterns have you found effective for testing your SwiftUI apps? Let me know in the comments, and stay tuned for the next post where I&#8217;ll dive into practical solutions.</p>
<p>The post <a rel="nofollow" href="https://www.swiftyplace.com/blog/testing-in-ios-development">Why We Keep Avoiding Tests in iOS—And What the Tools Should Do About It</a> appeared first on <a rel="nofollow" href="https://www.swiftyplace.com">swiftyplace</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.swiftyplace.com/blog/testing-in-ios-development/feed</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title>The Composable Architecture: How Architectural Design Decisions Influence Performance</title>
		<link>https://www.swiftyplace.com/blog/the-composable-architecture-performance?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=the-composable-architecture-performance</link>
					<comments>https://www.swiftyplace.com/blog/the-composable-architecture-performance#comments</comments>
		
		<dc:creator><![CDATA[Karin Prater]]></dc:creator>
		<pubDate>Mon, 24 Mar 2025 11:44:55 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://www.swiftyplace.com/?p=1005492</guid>

					<description><![CDATA[<p>Hey there! So I&#8217;ve been diving into architectural patterns lately, and it&#8217;s been quite the journey. I wanted to share some thoughts on how the design decisions we make can have these ripple effects throughout ... <a title="The Composable Architecture: How Architectural Design Decisions Influence Performance" class="read-more" href="https://www.swiftyplace.com/blog/the-composable-architecture-performance" aria-label="More on The Composable Architecture: How Architectural Design Decisions Influence Performance">Read more</a></p>
<p>The post <a rel="nofollow" href="https://www.swiftyplace.com/blog/the-composable-architecture-performance">The Composable Architecture: How Architectural Design Decisions Influence Performance</a> appeared first on <a rel="nofollow" href="https://www.swiftyplace.com">swiftyplace</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Hey there! So I&#8217;ve been diving into architectural patterns lately, and it&#8217;s been quite the journey. I wanted to share some thoughts on how the design decisions we make can have these ripple effects throughout our apps &#8211; especially when it comes to performance.</p>


<div class="gb-container gb-container-0e9c2f76">

<p><strong>Disclaimer</strong>: This article is not a review of TCA&#8217;s current state or implementation. The TCA framework has evolved significantly since some of the issues discussed here were first identified, with many improvements already released and more in development. Rather, this is an analysis of how architectural decisions influence performance over time, using TCA&#8217;s evolution as a case study. My goal is to examine the relationship between design principles and performance outcomes, and extract lessons that apply to any architecture we might create or adopt.</p>

</div>


<div style="height:48px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">Why I&#8217;m Looking at TCA</h2>



<p>Full disclosure: I haven&#8217;t personally used <a href="https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/" target="_blank" rel="noopener">The Composable Architecture (TCA)</a> in production, and honestly, I probably won&#8217;t in the future. But that&#8217;s not because it&#8217;s bad &#8211; it&#8217;s just not aligned with my preferred approach to building apps.</p>



<p>That said, TCA makes for a fascinating case study. After 5 years of SwiftUI, we&#8217;ve all learned that <strong>performance isn&#8217;t something that just happens automatically. You really have to work for it,</strong> <em>right?</em> And architectural choices can either make that easier or&#8230; well, much harder.</p>



<p>So I thought it would be interesting to look at what TCA users have been reporting, see where they&#8217;ve struggled with performance, and connect those struggles back to the core architectural decisions. Not to bash TCA, but to learn from it &#8211; because these lessons apply to any architecture we might design or adopt.</p>



<p>During <a href="https://youtu.be/ie0ava5PoYE?si=8ZYo-eE7qrqkSPdW" target="_blank" rel="noopener">Krzysztof Zabłocki&#8217;s talk at Swift Heroes 2023</a>, he showed that in a large-scale application, processing just five lines of text took 9 seconds of CPU time in vanilla TCA. That&#8217;s&#8230; not great. Noted that this was for a macOS app “The Arc browser”. But he also said that people have reported on similar problems for iOS apps.</p>



<p>It&#8217;s worth noting that Krzysztof isn&#8217;t just any developer criticizing TCA &#8211; he&#8217;s one of the most experienced TCA developers in the community. He&#8217;s built five full applications with TCA, including working at The Browser Company on what was likely the largest TCA codebase in production. He&#8217;s also consulted with the Point-Free team to improve TCA&#8217;s ergonomics. When someone with this level of expertise reports performance issues, it&#8217;s particularly worth examining.</p>



<p>But why is that happening? Let&#8217;s break down how TCA&#8217;s design principles translate to performance challenges:</p>



<div style="height:48px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">The Foundation: Struct-Based Global State</h3>



<p>First, let&#8217;s understand TCA&#8217;s fundamental approach to state. In TCA, all application state is represented as a single Swift struct:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="struct AppState: Equatable {
    var settings: SettingsState
    var profile: ProfileState
    var feed: FeedState
    // ... more state properties
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">AppState</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">Equatable </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> settings: SettingsState</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> profile: ProfileState</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> feed: FeedState</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// ... more state properties</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<p>This struct contains everything &#8211; all user data, UI state, feature states, everything. And because it&#8217;s a Swift struct (a value type), it gets copied whenever it&#8217;s modified.</p>



<div style="height:48px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">The Critical Rule: Global State Flows Down the Entire View Hierarchy</h3>



<p>This is where TCA makes a critical architectural decision: <strong>the entire global state struct is passed down through the entire view hierarchy</strong>. This is a fundamental rule of TCA&#8217;s design.</p>



<p>In practice, it looks like this:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="struct RootView: View {
    let store: Store&lt;AppState, AppAction&gt;

    var body: some View {
        HomeView(store: store)  // Passing the ENTIRE store down
    }
}

struct HomeView: View {
    let store: Store&lt;AppState, AppAction&gt;  // Gets the ENTIRE store

    var body: some View {
        VStack {
            Text(&quot;Hello, (store.state.userProfile.name)&quot;)
            SettingsButton(store: store)  // Passes the ENTIRE store down again
            FeedView(store: store)  // And again...
        }
    }
}

struct FeedView: View {
    let store: Store&lt;AppState, AppAction&gt;  // Gets the ENTIRE store

    var body: some View {
        // Even though this view only cares about feed data,
        // it receives the entire application state
        List(store.state.feed.items) { item in
            FeedItemView(store: store, item: item)  // Passes entire store down again
        }
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">RootView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> store: Store&lt;AppState, AppAction&gt;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">HomeView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">store</span><span style="color: #F6F6F4">: store)  </span><span style="color: #7B7F8B">// Passing the ENTIRE store down</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">HomeView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> store: Store&lt;AppState, AppAction&gt;  </span><span style="color: #7B7F8B">// Gets the ENTIRE store</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">VStack</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">Text</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Hello, (store.state.userProfile.name)</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">SettingsButton</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">store</span><span style="color: #F6F6F4">: store)  </span><span style="color: #7B7F8B">// Passes the ENTIRE store down again</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">FeedView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">store</span><span style="color: #F6F6F4">: store)  </span><span style="color: #7B7F8B">// And again...</span></span>
<span class="line"><span style="color: #F6F6F4">        }</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">FeedView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> store: Store&lt;AppState, AppAction&gt;  </span><span style="color: #7B7F8B">// Gets the ENTIRE store</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// Even though this view only cares about feed data,</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// it receives the entire application state</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">List</span><span style="color: #F6F6F4">(store.state.feed.items) { item </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">FeedItemView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">store</span><span style="color: #F6F6F4">: store, </span><span style="color: #97E1F1">item</span><span style="color: #F6F6F4">: item)  </span><span style="color: #7B7F8B">// Passes entire store down again</span></span>
<span class="line"><span style="color: #F6F6F4">        }</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<p>Every view in your hierarchy, no matter how deeply nested, receives the entire application state struct. This is by design in TCA &#8211; it ensures every component has access to the full state.</p>



<figure class="gb-block-image gb-block-image-7fc2c63b"><img loading="lazy" decoding="async" width="1368" height="750" class="gb-image gb-image-7fc2c63b" src="http://www.swiftyplace.com/wp-content/uploads/2025/03/tca-flow.webp" alt="The Composable Architcture with SwiftUI data flow" title="tca flow" srcset="https://www.swiftyplace.com/wp-content/uploads/2025/03/tca-flow.webp 1368w, https://www.swiftyplace.com/wp-content/uploads/2025/03/tca-flow-300x164.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2025/03/tca-flow-1024x561.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2025/03/tca-flow-768x421.webp 768w" sizes="auto, (max-width: 1368px) 100vw, 1368px" /></figure>



<div style="height:48px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Why This Causes Performance Problems</h3>



<p>This approach directly conflicts with how SwiftUI is designed to work efficiently.</p>



<p>This directly contradicts one of SwiftUI&#8217;s core performance recommendations. Remember that <a href="https://www.google.com/url?sa=t&amp;source=web&amp;rct=j&amp;opi=89978449&amp;url=https://developer.apple.com/videos/play/wwdc2021/10022/&amp;ved=2ahUKEwjgj7us0qKMAxV_QfEDHS0ULV0QtwJ6BAgLEAI&amp;usg=AOvVaw11oMNT6BVU6LcnejX9Pkpj" target="_blank" rel="noopener">WWDC session &#8220;Demystify SwiftUI&#8221;</a>? They explicitly warned against passing down more data than a view needs. The presenters emphasized that we should &#8220;only pass down information that the view really needs&#8221; to avoid unnecessary view updates and evaluations.</p>



<p>When SwiftUI decides whether to redraw a view, it evaluates what data that view depends on. The problem is that when you pass the entire state struct to a view:</p>



<ol class="wp-block-list">
<li><p><strong>SwiftUI sees a dependency on the entire state</strong>: From SwiftUI&#8217;s perspective, the view depends on everything in that struct</p></li>



<li><p><strong>Any state change can trigger reevaluation</strong>: When any property in the state changes, SwiftUI has to check if the view needs redrawing</p></li>



<li><p><strong>Cascading reevaluations</strong>: This happens for every view in your hierarchy that received the state</p></li>
</ol>



<p>In a large application where:</p>



<ul class="wp-block-list">
<li>The state struct might contain hundreds of properties</li>



<li>The view hierarchy might be dozens of levels deep</li>



<li>Multiple state changes happen in quick succession</li>
</ul>



<p>&#8230;this creates a <strong><em>perfect storm of performance problems</em></strong>. When a single property changes, potentially hundreds of views need to reevaluate whether they should redraw, even if most of them don&#8217;t actually display anything related to that property.</p>



<p>TCA&#8217;s approach initially caused massive redraw performance problems because any state change would potentially cause all views to be evaluated. It&#8217;s like if changing your profile picture somehow made your settings screen recalculate &#8211; totally unnecessary work!</p>



<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">The ViewStore Partial Solution</h3>



<p>To work around this fundamental issue, TCA introduced ViewStore wrappers that only expose certain slices of state to views:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="struct ContentView: View {
    let store: Store&lt;AppState, AppAction&gt;

    var body: some View {
        WithViewStore(store, observe: { $0.profile }) { viewStore in
            // This view only &quot;sees&quot; the profile part of state
            ProfileView(name: viewStore.name)
        }
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ContentView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> store: Store&lt;AppState, AppAction&gt;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">WithViewStore</span><span style="color: #F6F6F4">(store, </span><span style="color: #97E1F1">observe</span><span style="color: #F6F6F4">: { </span><span style="color: #BF9EEE; font-style: italic">$0</span><span style="color: #F6F6F4">.profile }) { viewStore </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #7B7F8B">// This view only &quot;sees&quot; the profile part of state</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">ProfileView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">name</span><span style="color: #F6F6F4">: viewStore.name)</span></span>
<span class="line"><span style="color: #F6F6F4">        }</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<p>ViewStore tries to solve the problem by:</p>



<ol class="wp-block-list">
<li>Still receiving the entire state (following TCA&#8217;s rule)</li>



<li>But only extracting and observing a specific slice of it</li>



<li>Only triggering view updates when that specific slice changes</li>
</ol>



<p>While this helps, it&#8217;s essentially a workaround for a problem created by the architecture itself. It adds boilerplate and complexity to solve an issue that wouldn&#8217;t exist with a more granular state management approach. And as we&#8217;ll see, even these ViewStores become performance bottlenecks at scale.</p>



<div style="height:40px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">The Double-Diffing Problem</h2>



<p>This &#8220;entire state flows down&#8221; rule creates another significant performance issue: double diffing. Let me explain what happens:</p>



<h4 class="wp-block-heading">1. SwiftUI&#8217;s Built-in Diffing</h4>



<p>SwiftUI already has its own diffing mechanism. When state changes, SwiftUI compares the old and new values to determine if a view needs to be redrawn. This is a core part of SwiftUI&#8217;s performance optimization strategy.</p>



<p>For example, when you write:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="struct ProfileView: View {
    let name: String

    var body: some View {
        Text(&quot;Hello, (name)&quot;)
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ProfileView</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">View </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> name: </span><span style="color: #97E1F1; font-style: italic">String</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> body: </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">Text</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Hello, (name)</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<p>SwiftUI tracks that this view depends on the <code>name</code> property. When <code>name</code> changes, SwiftUI knows to update the view. When <code>name</code> doesn&#8217;t change, SwiftUI skips redrawing the view entirely. This is efficient and happens automatically.</p>



<h4 class="wp-block-heading">2. TCA&#8217;s Required Diffing</h4>



<p>But in TCA, we have another layer of diffing happening. Because the entire state struct is passed everywhere, TCA needs its own diffing mechanism to determine what actually changed:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(2 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="// Inside TCA's implementation
func send(_ action: Action) {
    let oldState = self.state
    self.state = self.reducer(self.state, action)

    // TCA has to diff the entire state to see what changed
    if oldState != self.state {
        // Notify observers about the new state
        self.observers.forEach { $0(self.state) }
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #7B7F8B">// Inside TCA&#39;s implementation</span></span>
<span class="line"><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">send</span><span style="color: #F6F6F4">(</span><span style="color: #62E884">_</span><span style="color: #F6F6F4"> </span><span style="color: #FFB86C; font-style: italic">action</span><span style="color: #F6F6F4">: Action) {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> oldState </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE; font-style: italic">self</span><span style="color: #F6F6F4">.state</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #BF9EEE; font-style: italic">self</span><span style="color: #F6F6F4">.state </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE; font-style: italic">self</span><span style="color: #F6F6F4">.</span><span style="color: #97E1F1">reducer</span><span style="color: #F6F6F4">(</span><span style="color: #BF9EEE; font-style: italic">self</span><span style="color: #F6F6F4">.state, action)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// TCA has to diff the entire state to see what changed</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">if</span><span style="color: #F6F6F4"> oldState </span><span style="color: #F286C4">!=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE; font-style: italic">self</span><span style="color: #F6F6F4">.state {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// Notify observers about the new state</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #BF9EEE; font-style: italic">self</span><span style="color: #F6F6F4">.observers.</span><span style="color: #97E1F1">forEach</span><span style="color: #F6F6F4"> { </span><span style="color: #BF9EEE; font-style: italic">$0</span><span style="color: #F6F6F4">(</span><span style="color: #BF9EEE; font-style: italic">self</span><span style="color: #F6F6F4">.state) }</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<p>This is why TCA requires all state to conform to <code>Equatable</code> &#8211; it needs to compare old and new states. For large state structs, this comparison can be expensive.</p>



<div style="height:32px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">The Compounding Performance Hit</h3>



<p>So now we have two expensive operations happening for every state change:</p>



<ol class="wp-block-list">
<li><strong>TCA diffing the entire state struct</strong> to determine if anything changed</li>



<li><strong>SwiftUI diffing its dependencies</strong> to determine which views need updating</li>
</ol>



<p>And because the entire state flows down to every view, these costs multiply across your view hierarchy. In a large application, this creates a cascade of diffing operations:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="Action → State Change → TCA Diffing → SwiftUI Diffing in View 1 → SwiftUI Diffing in View 2 → ... → SwiftUI Diffing in View N" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F6F6F4">Action </span><span style="color: #F286C4">→</span><span style="color: #F6F6F4"> State Change </span><span style="color: #F286C4">→</span><span style="color: #F6F6F4"> TCA Diffing </span><span style="color: #F286C4">→</span><span style="color: #F6F6F4"> SwiftUI Diffing </span><span style="color: #F286C4">in</span><span style="color: #F6F6F4"> View </span><span style="color: #BF9EEE">1</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">→</span><span style="color: #F6F6F4"> SwiftUI Diffing </span><span style="color: #F286C4">in</span><span style="color: #F6F6F4"> View </span><span style="color: #BF9EEE">2</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">→</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">...</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">→</span><span style="color: #F6F6F4"> SwiftUI Diffing </span><span style="color: #F286C4">in</span><span style="color: #F6F6F4"> View N</span></span></code></pre></div>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<p>Each step adds overhead, and this happens for every action processed by the system.</p>



<h3 class="wp-block-heading">The ViewStore Overhead</h3>



<p>ViewStore helps with the SwiftUI diffing part by limiting what each view observes, but it adds its own overhead:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="WithViewStore(store, observe: { state in
    // This projection function runs on EVERY state change
    return SomeProjectedState(
        name: state.profile.name,
        isActive: state.settings.isActive
    )
}) { viewStore in
    // View body using viewStore
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #97E1F1">WithViewStore</span><span style="color: #F6F6F4">(store, </span><span style="color: #97E1F1">observe</span><span style="color: #F6F6F4">: { state </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// This projection function runs on EVERY state change</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">SomeProjectedState</span><span style="color: #F6F6F4">(</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">name</span><span style="color: #F6F6F4">: state.profile.name,</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">isActive</span><span style="color: #F6F6F4">: state.settings.isActive</span></span>
<span class="line"><span style="color: #F6F6F4">    )</span></span>
<span class="line"><span style="color: #F6F6F4">}) { viewStore </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// View body using viewStore</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<p>That projection function in <code>observe:</code> runs on every state update, even for updates completely unrelated to what it&#8217;s extracting. In a large application with dozens or hundreds of ViewStores, this creates significant CPU work just to determine that most views don&#8217;t need to update.</p>



<div style="height:48px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">The Action Processing Bottleneck</h3>



<p>Every state change in TCA must go through an action, which creates another bottleneck:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="enum AppAction {
    case settings(SettingsAction)
    case profile(ProfileAction)
    case feed(FeedAction)
    // ... potentially hundreds more actions
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">enum</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">AppAction</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> settings(SettingsAction)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> profile(ProfileAction)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> feed(FeedAction)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// ... potentially hundreds more actions</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<p>Each action goes through the entire reducer hierarchy, even if it only affects a small part of your state. This serializes all state updates through a single pipeline, which becomes a performance bottleneck as your application grows.</p>



<p>As Zabłocki noted in his talk, even a no-op action (one that doesn&#8217;t change state) had a cost of around 6ms in a large application. At 60fps, you only have about 16ms per frame, so spending 6ms just to process an action that does nothing is a huge performance hit.</p>



<h3 class="wp-block-heading">The Struct Copying Overhead</h3>



<p>Remember that Swift structs are value types, so when any property changes, the entire struct gets copied. In a large state tree, this means:</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono-NL.ttf" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono-NL,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:calc(1 * 0.6 * .875rem);line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span role="button" tabindex="0" data-code="// When changing one tiny property
state.deeplyNested.verySpecific.someFlag = true

// A new copy is created for:
// - someFlag's parent struct
// - verySpecific's parent struct
// - deeplyNested's parent struct
// - The entire AppState" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #7B7F8B">// When changing one tiny property</span></span>
<span class="line"><span style="color: #F6F6F4">state.deeplyNested.verySpecific.someFlag </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">true</span></span>
<span class="line"></span>
<span class="line"><span style="color: #7B7F8B">// A new copy is created for:</span></span>
<span class="line"><span style="color: #7B7F8B">// - someFlag&#39;s parent struct</span></span>
<span class="line"><span style="color: #7B7F8B">// - verySpecific&#39;s parent struct</span></span>
<span class="line"><span style="color: #7B7F8B">// - deeplyNested&#39;s parent struct</span></span>
<span class="line"><span style="color: #7B7F8B">// - The entire AppState</span></span></code></pre></div>



<div style="height:20px" aria-hidden="true" class="wp-block-spacer"></div>



<p>Each level of nesting requires a new struct to be created. For deeply nested state, this creates a cascade of allocations and copies that adds significant overhead.</p>



<div style="height:48px" aria-hidden="true" class="wp-block-spacer"></div>



<h3 class="wp-block-heading">Real-World Impact</h3>



<p>These design decisions create a compounding effect. As your application grows:</p>



<ol class="wp-block-list">
<li><strong>Larger state structs</strong> = more expensive diffing and copying</li>



<li><strong>More views</strong> = more places where the entire state flows</li>



<li><strong>More actions</strong> = more processing through the central bottleneck</li>



<li><strong>More ViewStores</strong> = more projection functions running on every update</li>
</ol>



<p>This is why TCA can work beautifully for small to medium apps but hit a performance wall in larger applications. The architectural decisions that make TCA clean and predictable for small apps create significant performance challenges at scale.</p>



<div style="height:48px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">The Elegant Solution: The Observation Feature</h2>



<p>Apple&#8217;s introduction of the Observation framework in Swift 5.9 feels almost like a deus ex machina for TCA&#8217;s performance woes. This new feature provides fine-grained state tracking that elegantly solves many of the problems we&#8217;ve discussed.</p>



<p>While Observation elegantly solves many of TCA&#8217;s performance issues, it&#8217;s worth noting that TCA got lucky here. The TCA framework was designed around capabilities that Swift simply didn&#8217;t have at the time, betting that the language would eventually evolve in that direction.</p>



<p>This approach works beautifully, but it wasn&#8217;t available when TCA was designed and adopted by many developers.</p>



<div style="height:48px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">What Can We Learn From This?</h2>



<p>So what are the takeaways for us as developers thinking about architecture?</p>



<ol class="wp-block-list">
<li><p><strong>Consider scale from the beginning</strong>: Architectural patterns that work beautifully for small apps might become performance bottlenecks at scale.</p></li>



<li><p><strong style="font-size: inherit;">Be pragmatic about principles</strong><span style="font-size: inherit; background-color: rgb(255, 255, 255);">: Sometimes you need to bend the rules (like the single store principle) to make things work in the real world.</span></p></li>



<li><p><strong style="font-size: inherit;">Measure, don&#8217;t assume</strong><span style="font-size: inherit;">: Performance issues often come from unexpected places. Zabłocki built custom tools to identify exactly where the bottlenecks were.</span></p></li>



<li><p><strong>Design with the platform</strong>: Architectures should work with the grain of the platform, not against it. TCA&#8217;s Redux-inspired approach sometimes fights against SwiftUI&#8217;s natural patterns.</p></li>



<li><p><strong>Anticipate evolution</strong>: The best architectures can adapt as the platform evolves, incorporating new capabilities (like Observation) without requiring complete rewrites.</p></li>
</ol>



<div style="height:48px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">Final Thoughts</h2>



<p>To be fair, the TCA team hasn&#8217;t been standing still. Recent versions have introduced significant improvements like the new reducer protocol, and they&#8217;re actively working on Observation framework integration that should address many of the performance challenges discussed here. These improvements show that the framework is evolving &#8211; though the fundamental architectural decisions we&#8217;ve examined will continue to shape how TCA performs at scale.</p>



<p>I find it fascinating how architectural decisions made with the best intentions can have such significant performance implications. TCA isn&#8217;t &#8220;bad&#8221; &#8211; it&#8217;s just making specific trade-offs that prioritize certain qualities (predictability, testability) over others (raw performance).</p>



<p>The question for us as developers isn&#8217;t &#8220;Is TCA good or bad?&#8221; but rather &#8220;What trade-offs am I willing to make for my specific application?&#8221; Understanding these trade-offs helps us make more informed decisions about the architectures we adopt or design.</p>



<p>What about you? Have you used TCA or other architectures that made interesting performance trade-offs? I&#8217;d love to hear about your experiences in the comments!</p>
<p>The post <a rel="nofollow" href="https://www.swiftyplace.com/blog/the-composable-architecture-performance">The Composable Architecture: How Architectural Design Decisions Influence Performance</a> appeared first on <a rel="nofollow" href="https://www.swiftyplace.com">swiftyplace</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.swiftyplace.com/blog/the-composable-architecture-performance/feed</wfw:commentRss>
			<slash:comments>9</slash:comments>
		
		
			</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/?utm_source=w3tc&utm_medium=footer_comment&utm_campaign=free_plugin

Page Caching using Disk: Enhanced 

Served from: www.swiftyplace.com @ 2026-05-18 03:01:15 by W3 Total Cache
-->