<?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>Sat, 17 Oct 2026 08:02:17 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.3</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>What Is a View Model in SwiftUI MVVM, Really</title>
		<link>https://www.swiftyplace.com/blog/swiftui-mvvm?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=swiftui-mvvm</link>
					<comments>https://www.swiftyplace.com/blog/swiftui-mvvm#respond</comments>
		
		<dc:creator><![CDATA[Karin Prater]]></dc:creator>
		<pubDate>Fri, 16 Oct 2026 11:02:43 +0000</pubDate>
				<category><![CDATA[SwiftUI]]></category>
		<category><![CDATA[SwiftUI Architecture]]></category>
		<guid isPermaLink="false">https://www.swiftyplace.com/?p=1005742</guid>

					<description><![CDATA[<p>Learn how to apply MVVM in SwiftUI. Find out what a view model actually is, what it’s responsible for, and how it fits between your views and your data. A clear, practical introduction with real SwiftUI code examples to help you keep your views simple and your app state under control.</p>
<p>The post <a rel="nofollow" href="https://www.swiftyplace.com/blog/swiftui-mvvm">What Is a View Model in SwiftUI MVVM, Really</a> appeared first on <a rel="nofollow" href="https://www.swiftyplace.com">swiftyplace</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">If you&#8217;ve spent any time reading about SwiftUI architecture, you&#8217;ve probably run into the term &#8220;view model.&#8221; And you&#8217;ve probably noticed that it means different things to different people. Some tutorials add a view model to every single view without ever explaining why. Others say SwiftUI doesn&#8217;t need view models at all, because &#8220;the view is already the view model.&#8221;</p>



<p class="wp-block-paragraph">So what is a view model, really? In this post, I want to give you a clear, practical definition that you can apply to your own code. Here&#8217;s the main rule that I find most practical, so you have something to hold on to:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>A view model owns UI state and is responsible for mutating that state properly and consistently.</strong></p>
</blockquote>



<p class="wp-block-paragraph">Notice what&#8217;s not in that sentence: a single view. A view model isn&#8217;t tied to one view. It can support one view, an entire screen, or several views that share the same state. (Other people might say otherwise, but I am giving you the system that works best for me)</p>



<p class="wp-block-paragraph">That responsibility is the whole point. In SwiftUI, state drives the UI. When something on screen is wrong, the cause is almost always state that&#8217;s wrong. And state goes wrong because the code that mutates it is messy: scattered across views, duplicated in slightly different versions, or running steps in the wrong order. A view model gives those mutations one home, so they happen the same way every time.</p>



<p class="wp-block-paragraph">That&#8217;s it. Everything else in this post explains what that means, why it&#8217;s useful, and where the boundaries are.</p>



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



<h2 class="wp-block-heading">Where MVVM Comes From</h2>



<p class="wp-block-paragraph">MVVM stands for <a href="https://en.wikipedia.org/wiki/Model–view–viewmodel" data-type="link" data-id="https://en.wikipedia.org/wiki/Model–view–viewmodel" target="_blank" rel="noopener">Model-View-ViewModel.</a> It was introduced in 2005 by Microsoft architects Ken Cooper and Ted Peters. I don&#8217;t usually spend much time on history, but here it matters because understanding where the pattern came from helps us understand what it&#8217;s actually trying to do.</p>



<p class="wp-block-paragraph">MVVM was born in Windows Presentation Foundation, or WPF. The UI in WPF is written in XAML, a declarative markup language, and it relies heavily on data bindings and reactive updates. In other words, it&#8217;s state-driven, declarative programming. Sound familiar? That&#8217;s very close to how SwiftUI works.</p>



<p class="wp-block-paragraph">So what problem was MVVM designed to solve? It came down to people. In WPF, teams naturally split into two groups: UI-facing developers, who Microsoft called &#8220;designer developers,&#8221; and logic developers working behind the scenes. The challenge was letting these two groups work in parallel without stepping on each other&#8217;s toes.</p>



<p class="wp-block-paragraph">What kept happening was that logic crept into the view: network requests, formatting, data transformations. The view code became cluttered and hard to maintain, and when everyone has to work in the same file, collaboration breaks down fast.</p>



<p class="wp-block-paragraph">The solution was to pull that complex logic out of the view and into a separate layer. The view was left with nothing but declarative UI, and the logic developers had their own layer to work in. That separate layer is the view model.</p>



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



<h2 class="wp-block-heading">The Backing Layer: A Presentation Layer Pattern</h2>



<p class="wp-block-paragraph">The best way to think about a view model is as the backing layer behind your UI. You have the view on one side and your data on the other, and you place an intermediate layer in between. That layer handles the more complex concerns: state management, formatting, anything you don&#8217;t want cluttering your view code.</p>



<p class="wp-block-paragraph">I like to compare it to an old-school calculator. On the front, you see the buttons and the display. That&#8217;s your view. Now imagine you unscrew the case and look inside: the battery, the circuit board, some cables. That&#8217;s your backing layer. As a user, you just want to press buttons and get results. You don&#8217;t want to see or deal with the internals. A view model works the same way: it keeps the inner workings hidden behind a clean surface.</p>



<figure class="gb-block-image gb-block-image-7f65d7e5"><img fetchpriority="high" decoding="async" width="978" height="314" class="gb-image gb-image-7f65d7e5" src="https://www.swiftyplace.com/wp-content/uploads/2026/10/mvvm-layers-calculator.webp" alt="" title="mvvm-layers-calculator" srcset="https://www.swiftyplace.com/wp-content/uploads/2026/10/mvvm-layers-calculator.webp 978w, https://www.swiftyplace.com/wp-content/uploads/2026/10/mvvm-layers-calculator-300x96.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2026/10/mvvm-layers-calculator-768x247.webp 768w" sizes="(max-width: 978px) 100vw, 978px" /></figure>



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



<p class="wp-block-paragraph">That&#8217;s why I often prefer the term <strong>UI backing layer</strong> over &#8220;view model.&#8221; It describes what the thing actually does.</p>



<p class="wp-block-paragraph">This also leads to an important clarification. Because we&#8217;re talking about layers, you&#8217;ll sometimes hear MVVM described as a &#8220;UI architecture pattern.&#8221; That can be misleading, because it gives the impression that MVVM is a full app architecture. It&#8217;s not. <strong>MVVM is a presentation layer pattern.</strong> It&#8217;s about the view and the code that directly backs it.</p>



<ul class="wp-block-list">
<li>It&#8217;s not about visual design, like layout, colors, or components.</li>



<li>It doesn&#8217;t tell you how to structure your whole app. Networking, persistence, navigation, and dependencies need their own layers and patterns.</li>



<li>At its core, it&#8217;s about separation of concerns within the UI layer.</li>
</ul>



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



<h2 class="wp-block-heading">The Three Layers</h2>



<p class="wp-block-paragraph">Let&#8217;s look at each part of the pattern and what it&#8217;s responsible for.</p>


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

<figure class="gb-block-image gb-block-image-c09039d3"><img decoding="async" width="966" height="674" class="gb-image gb-image-c09039d3" src="https://www.swiftyplace.com/wp-content/uploads/2026/10/mvvm-layers-flow.webp" alt="representation layer in SwiftUI apps with MVVM" title="mvvm-layers-flow" srcset="https://www.swiftyplace.com/wp-content/uploads/2026/10/mvvm-layers-flow.webp 966w, https://www.swiftyplace.com/wp-content/uploads/2026/10/mvvm-layers-flow-300x209.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2026/10/mvvm-layers-flow-768x536.webp 768w" sizes="(max-width: 966px) 100vw, 966px" /></figure>

</div>


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



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



<p class="wp-block-paragraph">The view is the UI layer. In SwiftUI, these are your <code>View</code> structs, and their job is to <strong>take state and decide what to display.</strong> Logged in? Show the main content. Logged out? Show the login screen.</p>



<p class="wp-block-paragraph">Views also handle the finer details of presentation. Is a button enabled or disabled? What text goes into a label? What font, what position, what layout? These are all view concerns. The view is the part that makes things look good.</p>



<p class="wp-block-paragraph">Views also receive user interactions. When someone taps a button, the view picks that up. Sometimes it handles the interaction directly, but more often it passes the user&#8217;s intent along to the view model. The view just says &#8220;the user tapped this&#8221; and moves on.</p>



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



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



<p class="wp-block-paragraph">The view model is the UI backing layer. Because SwiftUI is state-driven, everything revolves around UI state, and managing that state can get complex. When do you change it? What moves you from loading to success, or to an error? What happens when a network request comes back? All of this needs to live somewhere, and that somewhere is the view model.</p>



<p class="wp-block-paragraph">The view model owns the UI state and decides when and how it changes. It can also prepare data for display, like formatting a price, and act as a gatekeeper for what&#8217;s allowed to happen.</p>



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



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



<p class="wp-block-paragraph">The model is your data: the structures your app works with, and the logic that naturally belongs to that data, like validation rules.</p>



<p class="wp-block-paragraph">You&#8217;ll find that different developers mean different things by &#8220;model.&#8221; Some put their networking code here too. That&#8217;s a valid interpretation, but to keep things clear, I keep the model narrow: the data itself, plus the logic intrinsic to it.</p>



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



<h2 class="wp-block-heading">What Defines a View Model: UI State + Mutations</h2>



<p class="wp-block-paragraph">You can characterize any view model by two things: what state it holds, and how it can change that state. That&#8217;s really all that defines it.</p>



<ul class="wp-block-list">
<li><strong>UI state:</strong> the data your views need to render. The items in a list, whether something is loading, an error message, which item is selected, whether a button is allowed to be tapped.</li>



<li><strong>Methods to change UI state:</strong> controlled mutations of that data. Adding an item, logging in, deleting a theme.</li>
</ul>



<p class="wp-block-paragraph">Because the view model backs the UI, its state has to be connected to the views that display it. That comes with two technical requirements:</p>



<ul class="wp-block-list">
<li><strong><code>@Observable</code></strong>: SwiftUI needs to track the state so it can update the views whenever it changes. Without observation, the view has no way of knowing that something happened.</li>



<li><strong><code>@MainActor</code></strong>: the UI reads this state, and UI work happens on the main actor. So the state, and the code that changes it, lives there too.</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(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>@MainActor
@Observable
final class CartViewModel {
    private(set) var items: &#91;CartItem&#93; = []   // UI state

    func addItem(id: String) { /* ... */ }    // mutation
    func removeItem(id: String) { /* ... */ } // mutation
}</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">@MainActor</span></span>
<span class="line"><span style="color: #F286C4">@Observable</span></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">CartViewModel</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">set</span><span style="color: #F6F6F4">) </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> items: &#91;CartItem&#93; </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> []   </span><span style="color: #7B7F8B">// UI state</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">id</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4">) { </span><span style="color: #7B7F8B">/* ... */</span><span style="color: #F6F6F4"> }    </span><span style="color: #7B7F8B">// mutation</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">removeItem</span><span style="color: #F6F6F4">(</span><span style="color: #62E884; 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 style="color: #7B7F8B">/* ... */</span><span style="color: #F6F6F4"> } </span><span style="color: #7B7F8B">// mutation</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



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



<p class="wp-block-paragraph">There&#8217;s one more rule I find really helpful: <strong>a view model communicates through state, never through UI instructions.</strong> Say a user isn&#8217;t allowed to buy a certain product. The view model doesn&#8217;t say &#8220;disable the button.&#8221; It says &#8220;this action is not allowed.&#8221; The view is the one that translates that into a disabled button.</p>



<p class="wp-block-paragraph">In the same way, a view model can prepare data for display, like turning a raw price into a formatted string. But it never decides how that information is rendered. It knows <em>what</em> to show, never <em>how</em> to show it. That&#8217;s view territory.</p>



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



<h2 class="wp-block-heading">Why Put State and Mutations Together?</h2>



<p class="wp-block-paragraph">I like to use the law of proximity here: <strong>things that change together should live together.</strong></p>



<p class="wp-block-paragraph">You can look at UI state from two directions. When the state changes, the UI changes, so the state should live close to the views that use it. That&#8217;s true. But the state and the code that <em>changes</em> it belong together just as tightly.</p>



<p class="wp-block-paragraph">Think about a SwiftUI view hierarchy where several views use the same state, and each of those subviews can mutate it. The state declaration moves higher up the view tree, but the code that changes it is scattered across different subviews. Things get messy quickly. If you want to know where a value was changed, you have to go hunting through the hierarchy.</p>



<p class="wp-block-paragraph">A view model brings them back together. The state lives in one place, and the methods that change it live right next to it. You get a single source of truth for the state, and one place that&#8217;s responsible for changing it properly and consistently.</p>



<p class="wp-block-paragraph">That gives you a few concrete benefits:</p>



<ul class="wp-block-list">
<li><strong>Consistent mutations:</strong> every change goes through the same methods, so it happens the same way every time.</li>



<li><strong>No duplicated logic:</strong> if several views need to make the same change, they call the same method. Duplicated logic tends to drift. Sooner or later you update one copy and forget the other, and your state goes out of sync.</li>



<li><strong>Easier to follow:</strong> when you want to know how the state changes, you look in one place.</li>
</ul>



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



<h2 class="wp-block-heading">Example: The Shopping Cart</h2>



<p class="wp-block-paragraph">Let&#8217;s see how these layers play out with a shopping cart.</p>



<p class="wp-block-paragraph"></p>


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

<figure class="gb-block-image gb-block-image-ad983c4f"><img decoding="async" width="1158" height="1152" class="gb-image gb-image-ad983c4f" src="https://www.swiftyplace.com/wp-content/uploads/2026/10/Preseentation-Layer-Backing-example.jpg" alt="" title="Preseentation Layer - Backing - example" srcset="https://www.swiftyplace.com/wp-content/uploads/2026/10/Preseentation-Layer-Backing-example.jpg 1158w, https://www.swiftyplace.com/wp-content/uploads/2026/10/Preseentation-Layer-Backing-example-300x298.jpg 300w, https://www.swiftyplace.com/wp-content/uploads/2026/10/Preseentation-Layer-Backing-example-1024x1019.jpg 1024w, https://www.swiftyplace.com/wp-content/uploads/2026/10/Preseentation-Layer-Backing-example-150x150.jpg 150w, https://www.swiftyplace.com/wp-content/uploads/2026/10/Preseentation-Layer-Backing-example-768x764.jpg 768w" sizes="(max-width: 1158px) 100vw, 1158px" /></figure>

</div>


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



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



<p class="wp-block-paragraph">The model is the raw data: a cart item with a product ID and a quantity, and the product information itself.</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 CartItem {
    let id: String
    var quantity: Int
}

struct Product {
    let id: String
    let name: String
    let price: Double
}</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">CartItem</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"> id: </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"> quantity: </span><span style="color: #97E1F1; font-style: italic">Int</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">Product</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"> id: </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"> 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"> price: </span><span style="color: #97E1F1; font-style: italic">Double</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



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



<p class="wp-block-paragraph">This isn&#8217;t what the UI wants to show, though. The view needs a name, a quantity, and nicely formatted prices, all in one place. So we add a display type that has exactly what the view needs:</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 CartDisplayItem {
    let id: String
    let name: String
    let quantity: Int
    let formattedPrice: String
    let formattedSubtotal: String
}</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">CartDisplayItem</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"> id: </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"> 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"> quantity: </span><span style="color: #97E1F1; font-style: italic">Int</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> formattedPrice: </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"> formattedSubtotal: </span><span style="color: #97E1F1; font-style: italic">String</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



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



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



<p class="wp-block-paragraph">The view model owns the state of what&#8217;s in the cart. It turns the raw data into display items, calculates the total, and acts as a gatekeeper: if the view wants to change the cart, it has to call <code>addItem</code> or <code>removeItem</code>. It can&#8217;t reach in and change the items directly, which is what <code>private(set)</code> makes sure of.</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>@MainActor
@Observable
final class CartViewModel {
    private(set) var items: &#91;CartItem&#93; = []
    private let products: &#91;String: Product&#93;

    init(products: &#91;String: Product&#93;) {
        self.products = products
    }

    // Converts model data into what the view displays
    var displayItems: &#91;CartDisplayItem&#93; {
        items.compactMap { item in
            guard let product = products&#91;item.id&#93; else { return nil }
            let subtotal = product.price * Double(item.quantity)
            return CartDisplayItem(
                id: item.id,
                name: product.name,
                quantity: item.quantity,
                formattedPrice: formatPrice(product.price),
                formattedSubtotal: formatPrice(subtotal)
            )
        }
    }

    var formattedTotal: String {
        let total = items.reduce(0.0) { sum, item in
            guard let product = products&#91;item.id&#93; else { return sum }
            return sum + product.price * Double(item.quantity)
        }
        return formatPrice(total)
    }

    // Gatekeeper: the view tells us what it wants, we decide if it's allowed
    func canAddItem(id: String) -> Bool {
        true // e.g. check stock or purchase limits
    }

    func addItem(id: String) {
        if let index = items.firstIndex(where: { $0.id == id }) {
            items&#91;index&#93;.quantity += 1
        } else {
            items.append(CartItem(id: id, quantity: 1))
        }
    }

    func removeItem(id: String) {
        items.removeAll { $0.id == id }
    }

    private func formatPrice(_ price: Double) -> String {
        price.formatted(.currency(code: "USD"))
    }
}</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">@MainActor</span></span>
<span class="line"><span style="color: #F286C4">@Observable</span></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">CartViewModel</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">set</span><span style="color: #F6F6F4">) </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> items: &#91;CartItem&#93; </span><span style="color: #F286C4">=</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">let</span><span style="color: #F6F6F4"> products: &#91;</span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> Product&#93;</span></span>
<span class="line"></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">products</span><span style="color: #F6F6F4">: &#91;</span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> Product&#93;) {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #BF9EEE; font-style: italic">self</span><span style="color: #F6F6F4">.products </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> products</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">// Converts model data into what the view displays</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> displayItems: &#91;CartDisplayItem&#93; {</span></span>
<span class="line"><span style="color: #F6F6F4">        items.</span><span style="color: #97E1F1">compactMap</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: #F286C4">guard</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> product </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> products&#91;item.id&#93; </span><span style="color: #F286C4">else</span><span style="color: #F6F6F4"> { </span><span style="color: #F286C4">return</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: #F286C4">let</span><span style="color: #F6F6F4"> subtotal </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> product.price </span><span style="color: #F286C4">*</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">Double</span><span style="color: #F6F6F4">(item.quantity)</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">CartDisplayItem</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">: item.id,</span></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #97E1F1">name</span><span style="color: #F6F6F4">: product.name,</span></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #97E1F1">quantity</span><span style="color: #F6F6F4">: item.quantity,</span></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #97E1F1">formattedPrice</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1">formatPrice</span><span style="color: #F6F6F4">(product.price),</span></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #97E1F1">formattedSubtotal</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1">formatPrice</span><span style="color: #F6F6F4">(subtotal)</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: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> formattedTotal: </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">let</span><span style="color: #F6F6F4"> total </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> items.</span><span style="color: #97E1F1">reduce</span><span style="color: #F6F6F4">(</span><span style="color: #BF9EEE">0.0</span><span style="color: #F6F6F4">) { sum, item </span><span style="color: #F286C4">in</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"> product </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> products&#91;item.id&#93; </span><span style="color: #F286C4">else</span><span style="color: #F6F6F4"> { </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> sum }</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> sum </span><span style="color: #F286C4">+</span><span style="color: #F6F6F4"> product.price </span><span style="color: #F286C4">*</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">Double</span><span style="color: #F6F6F4">(item.quantity)</span></span>
<span class="line"><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">formatPrice</span><span style="color: #F6F6F4">(total)</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">// Gatekeeper: the view tells us what it wants, we decide if it&#39;s allowed</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">canAddItem</span><span style="color: #F6F6F4">(</span><span style="color: #62E884; 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 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: #BF9EEE">true</span><span style="color: #F6F6F4"> </span><span style="color: #7B7F8B">// e.g. check stock or purchase limits</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">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: #F286C4">if</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> index </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> items.</span><span style="color: #97E1F1">firstIndex</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"> id }) {</span></span>
<span class="line"><span style="color: #F6F6F4">            items&#91;index&#93;.quantity </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: #F286C4">else</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">CartItem</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">id</span><span style="color: #F6F6F4">: id, </span><span style="color: #97E1F1">quantity</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>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">removeItem</span><span style="color: #F6F6F4">(</span><span style="color: #62E884; 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">        items.</span><span style="color: #97E1F1">removeAll</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"> 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">private</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">formatPrice</span><span style="color: #F6F6F4">(</span><span style="color: #62E884">_</span><span style="color: #F6F6F4"> </span><span style="color: #FFB86C; font-style: italic">price</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">Double</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">        price.</span><span style="color: #97E1F1">formatted</span><span style="color: #F6F6F4">(.</span><span style="color: #97E1F1">currency</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">code</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">USD</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>



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



<p class="wp-block-paragraph">With all that logic in the view model, the view becomes very simple. No formatting, no calculations. Just a <code>ForEach</code> with some <code>Text</code> views, a font, and a <code>disabled</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" 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 CartView: View {
     var viewModel: CartViewModel

    var body: some View {
        VStack {
            ForEach(viewModel.displayItems, id: .id) { item in
                HStack {
                    Text(item.name)
                    Text("×(item.quantity)")
                    Spacer()
                    Text(item.formattedSubtotal)
                }
            }

            Text("Total: (viewModel.formattedTotal)")
                .font(.headline)

            Button("Add iPhone") {
                viewModel.addItem(id: "iphone")
            }
            .disabled(!viewModel.canAddItem(id: "iphone"))
        }
    }
}</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">CartView</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"> viewModel: CartViewModel</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">ForEach</span><span style="color: #F6F6F4">(viewModel.displayItems, </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">HStack</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">(item.name)</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">×(item.quantity)</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">Spacer</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">(item.formattedSubtotal)</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: #97E1F1">Text</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Total: (viewModel.formattedTotal)</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">font</span><span style="color: #F6F6F4">(.headline)</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 iPhone</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">addItem</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">id</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">iphone</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">disabled</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">!</span><span style="color: #F6F6F4">viewModel.</span><span style="color: #97E1F1">canAddItem</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">id</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">iphone</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:30px" aria-hidden="true" class="wp-block-spacer"></div>



<p class="wp-block-paragraph">Notice how <code>canAddItem</code> works. The view model only answers &#8220;is this allowed?&#8221; The view decides that &#8220;not allowed&#8221; means a disabled button.</p>



<p class="wp-block-paragraph">Now imagine all of this living inside the view: the price formatting, the subtotal and total calculations, the rules for adding and removing items. The view would get big and messy fast, and every other view that touches the cart would need its own copy of that logic.</p>



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



<h2 class="wp-block-heading">&#8220;One View Model per Screen&#8221;: A Rule That Trips You Up</h2>



<p class="wp-block-paragraph">You&#8217;ll often hear that MVVM comes with a rule: one view model per screen, or even one per view. I understand why people like it. It&#8217;s simple, it&#8217;s easy to explain, and every screen gets its own neat little backing class.</p>



<p class="wp-block-paragraph">But I think it&#8217;s a rule that breaks your app sooner or later, because it goes directly against the single source of truth.</p>



<p class="wp-block-paragraph">Think about the shopping cart. The cart screen shows the items. But the product list also needs the cart: the user adds items from there, and maybe the tab bar shows a badge with the number of items. If every screen gets its own view model, you now have two view models holding their own copy of the cart. Add an item on the product list, and the cart screen doesn&#8217;t know about it. So you start syncing them: notifications, callbacks, reloading on appear. That&#8217;s exactly the messy state we wanted to avoid. You also end up writing the same <code>addItem</code> logic twice, and those two versions will drift apart.</p>



<p class="wp-block-paragraph">There&#8217;s a second problem: lifetime. A view model created by a screen lives only as long as that screen. Navigate away and back, and the state is gone and has to be loaded again, even if it should have survived.</p>



<p class="wp-block-paragraph">The issue is that the rule scopes view models by screens. But screens are a UI concern. What a view model actually manages is state, so that&#8217;s what should decide its scope.</p>



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



<h3 class="wp-block-heading">A Practical Approach: Scope by State, Not by Screen</h3>



<p class="wp-block-paragraph">Here are the guidelines I use instead:</p>



<ol class="wp-block-list">
<li><strong>Start from the state, not the screen.</strong> Ask: what state is this, and which views need it?</li>



<li><strong>Every piece of UI state has exactly one owner.</strong> One view model owns the cart. Not one per screen that shows the cart.</li>



<li><strong>Place the owner where the state needs to live.</strong> High enough in the view tree that every view that needs the state can reach it, and long-lived enough that the state survives as long as it should. If only one screen needs it, keep it there.</li>



<li><strong>Share the instance, don&#8217;t copy the state.</strong> Views that need the same state get the same view model passed in, and they all call the same methods to change it.</li>
</ol>



<p class="wp-block-paragraph">In the example above, <code>CartView</code> is short lived because its inside a sheet. It gets the ViewModel passed in from its parent. The product list needs it too. The ownership moves up: the app owns one <code>CartViewModel</code> and hands it to every view that needs 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(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 {
    @State private var cartViewModel = CartViewModel()
    @State private var cartIsShown = false
    
    var body: some View {
        ProductListView(cartViewModel, cartViewModel)
           .sheet(isShown: cartIsShown) {
               CartView(cartViewModel: cartViewModel)
            }
    }
}</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">@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"> cartViewModel </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">CartViewModel</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">private</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> cartIsShown </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">ProductListView</span><span style="color: #F6F6F4">(cartViewModel, cartViewModel)</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">isShown</span><span style="color: #F6F6F4">: cartIsShown) {</span></span>
<span class="line"><span style="color: #F6F6F4">               </span><span style="color: #97E1F1">CartView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">cartViewModel</span><span style="color: #F6F6F4">: cartViewModel)</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:30px" aria-hidden="true" class="wp-block-spacer"></div>



<p class="wp-block-paragraph">The product list adds an item, the badge updates, and the cart screen shows it, because they&#8217;re all looking at the same state, changed through the same methods.</p>



<p class="wp-block-paragraph">Notice that &#8220;one view model per screen&#8221; can still happen with this approach. If some state really belongs to only one screen, its view model naturally ends up there. The difference is that it&#8217;s a result of where the state belongs, not a rule you apply up front.</p>



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



<h2 class="wp-block-heading">Drawing the Line: UI State vs. Background Work</h2>



<p class="wp-block-paragraph">In the cart example, everything happened locally. In a real app, adding an item to a cart probably means asking a server whether the item is still available. So where does that network code go? Into the view model?</p>



<p class="wp-block-paragraph">This is where one question helps you decide which layer code belongs to: <strong>does it own UI state?</strong></p>



<ul class="wp-block-list">
<li><strong>Yes → it&#8217;s a view model</strong> (or a store, whatever you want to call it). It&#8217;s <code>@Observable</code>, it runs on <code>@MainActor</code>, and views are connected to it.</li>



<li><strong>No → it belongs in a supporting layer behind the view model</strong>, like a service. It&#8217;s not observable, it&#8217;s not on the main actor, and it&#8217;s free to do its work in the background, for example with <code>@concurrent</code> functions.</li>
</ul>



<p class="wp-block-paragraph">This isn&#8217;t just a naming preference. It&#8217;s the reason I draw the line exactly here. Anything the UI needs to observe has to be observable and has to live on the main actor, because that&#8217;s where SwiftUI reads it. Anything that doesn&#8217;t touch UI state shouldn&#8217;t be there. Networking, decoding JSON, reading and writing files: that&#8217;s heavy work, and we want it off the main actor so the UI stays responsive.</p>


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

<figure class="gb-block-image gb-block-image-4cd9fbca"><img loading="lazy" decoding="async" width="1863" height="1056" class="gb-image gb-image-4cd9fbca" src="https://www.swiftyplace.com/wp-content/uploads/2026/10/Presentation-Layer-Backing.webp" alt="" title="Presentation Layer - Backing" srcset="https://www.swiftyplace.com/wp-content/uploads/2026/10/Presentation-Layer-Backing.webp 1863w, https://www.swiftyplace.com/wp-content/uploads/2026/10/Presentation-Layer-Backing-300x170.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2026/10/Presentation-Layer-Backing-1024x580.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2026/10/Presentation-Layer-Backing-768x435.webp 768w, https://www.swiftyplace.com/wp-content/uploads/2026/10/Presentation-Layer-Backing-1536x871.webp 1536w" sizes="auto, (max-width: 1863px) 100vw, 1863px" /></figure>

</div>


<p class="wp-block-paragraph">The user taps a button. The view passes that intent to the view model. The view model asks the service to do the work in the background. The data comes back, the view model updates its UI state on the main actor, and SwiftUI updates the view.</p>



<p class="wp-block-paragraph">In code, the service is a small helper with no state of its own:</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>nonisolated struct CartService {
    @concurrent
    func addItem(id: String) async throws -> &#91;CartItem&#93; {
        // Send the request, decode the response,
        // and return the cart as the server sees it
    }
}</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">nonisolated</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">CartService</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@concurrent</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">addItem</span><span style="color: #F6F6F4">(</span><span style="color: #62E884; 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 style="color: #F286C4">async</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">throws</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">-&gt;</span><span style="color: #F6F6F4"> &#91;CartItem&#93; {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// Send the request, decode the response,</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #7B7F8B">// and return the cart as the server sees it</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:30px" aria-hidden="true" class="wp-block-spacer"></div>



<p class="wp-block-paragraph">And the view model uses it, but stays in charge of the 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>@MainActor
@Observable
final class CartViewModel {
    private(set) var items: &#91;CartItem&#93; = []
    private(set) var errorMessage: String?
    private let service = CartService()

    func addItem(id: String) async {
        do {
            items = try await service.addItem(id: id)
        } catch {
            errorMessage = "Couldn't add the item. Please try again."
        }
    }
}</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">@MainActor</span></span>
<span class="line"><span style="color: #F286C4">@Observable</span></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">CartViewModel</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">set</span><span style="color: #F6F6F4">) </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> items: &#91;CartItem&#93; </span><span style="color: #F286C4">=</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">set</span><span style="color: #F6F6F4">) </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> errorMessage: </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F286C4">?</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"> service </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">CartService</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">addItem</span><span style="color: #F6F6F4">(</span><span style="color: #62E884; 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 style="color: #F286C4">async</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">            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"> service.</span><span style="color: #97E1F1">addItem</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">id</span><span style="color: #F6F6F4">: id)</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">            errorMessage </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Couldn&#39;t add the item. Please try again.</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:30px" aria-hidden="true" class="wp-block-spacer"></div>



<p class="wp-block-paragraph">The service doesn&#8217;t know anything about the UI. The view model doesn&#8217;t know how the network request works. Each one has a clear job, and the boundary between them is exactly the boundary between the main actor and the background. This is good use of Separation of Concern principle (SoC).</p>



<p class="wp-block-paragraph">This also explains why naming matters. If something owns UI state, call it a view model or a store, even if someone labeled it a &#8220;service.&#8221; I see this a lot in code on GitHub and in blog posts: a &#8220;service&#8221; that holds UI state in a project that supposedly has no view models. It&#8217;s basically one, and the wrong name hides which code runs where and what the UI connects to.</p>



<p class="wp-block-paragraph">There&#8217;s more to say about this supporting layer, including the difference between services and repositories. I&#8217;ll cover that in a separate post.</p>



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



<h2 class="wp-block-heading">A View Model Is Not &#8220;All Business Logic&#8221;</h2>



<p class="wp-block-paragraph">You&#8217;ll often read that the view model is where your &#8220;business logic&#8221; goes. The problem is that &#8220;business logic&#8221; is a bag of everything. It doesn&#8217;t actually help you decide where a piece of code belongs.</p>



<p class="wp-block-paragraph">If you split it up, it becomes much clearer. Some business logic is about state: deciding what the UI should show and formatting data so it&#8217;s easy to display. That belongs in the view model. But a lot of business logic is about data access: network requests, file I/O, encoding and decoding. That belongs behind the view model, in the supporting layer.</p>



<p class="wp-block-paragraph">So instead of &#8220;the view model holds the business logic,&#8221; I&#8217;d put it this way: the view model holds the UI state and the logic that changes it. Everything else gets offloaded. This can lead to the dreaded &#8220;God&#8221; View Model, another mess we want to avoid.</p>



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



<h2 class="wp-block-heading">A Tool, Not a Rule</h2>



<p class="wp-block-paragraph">MVVM doesn&#8217;t mean you have to use a view model for every single view. In one of the original blog posts, Cooper and Peters made a point of saying they didn&#8217;t see it as a purist thing you apply everywhere. <strong>They saw it as a pattern that comes up naturally during development, something you reach for when the situation calls for it.</strong></p>



<p class="wp-block-paragraph">You always have the choice to keep all your code in the view. It&#8217;s your codebase. But when your views start piling up logic and state mutations, a view model is a very good tool to have.</p>



<p class="wp-block-paragraph">This brings us back to the argument that &#8220;the view is already the view model.&#8221; If you compare SwiftUI with WPF, where MVVM started, there are a lot of similarities: declarative UI, data bindings, reactive updates. And the goals are the same: keep your code organized, keep your views simple, and have clear rules for separating code into layers.</p>



<p class="wp-block-paragraph">So the argument is a bit strange when you think about it. Nobody ever said you <em>need</em> a view model. The real question is when one helps you, and in which situations.</p>



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



<h2 class="wp-block-heading">Wrapping Up</h2>



<p class="wp-block-paragraph">Let&#8217;s bring it all together. A view model is a UI backing layer, and it&#8217;s defined by two things:</p>



<ul class="wp-block-list">
<li><strong>A single source of truth for UI state:</strong> the data your views need to render.</li>



<li><strong>A single source of state mutations:</strong> the methods responsible for changing that state properly and consistently.</li>
</ul>



<p class="wp-block-paragraph">Because it backs the UI, it&#8217;s <code>@Observable</code> and runs on <code>@MainActor</code>. Anything that doesn&#8217;t own UI state, like networking or file access, belongs in a supporting layer behind it, where it can do its work in the background.</p>



<p class="wp-block-paragraph">But a view model is also an extra layer, and every layer has a cost. In the next post, When to Use a View Model (and When It&#8217;s Overkill), we&#8217;ll look at concrete examples of when that cost is worth paying, and when a view-only solution is the better choice.</p>
<p>The post <a rel="nofollow" href="https://www.swiftyplace.com/blog/swiftui-mvvm">What Is a View Model in SwiftUI MVVM, Really</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-mvvm/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>How I Architect SwiftUI Apps for Swift Concurrency &#8211; So the Compiler Stays Quiet</title>
		<link>https://www.swiftyplace.com/blog/swiftui-architecture-swift-concurrency?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=swiftui-architecture-swift-concurrency</link>
					<comments>https://www.swiftyplace.com/blog/swiftui-architecture-swift-concurrency#respond</comments>
		
		<dc:creator><![CDATA[Karin Prater]]></dc:creator>
		<pubDate>Sat, 10 Oct 2026 12:16:08 +0000</pubDate>
				<category><![CDATA[Swift Concurrency]]></category>
		<category><![CDATA[SwiftUI Architecture]]></category>
		<guid isPermaLink="false">https://www.swiftyplace.com/?p=1005711</guid>

					<description><![CDATA[<p>How I structure SwiftUI apps with Swift concurrency: where to use @MainActor, actors, and @concurrent so Sendable errors stop showing up.</p>
<p>The post <a rel="nofollow" href="https://www.swiftyplace.com/blog/swiftui-architecture-swift-concurrency">How I Architect SwiftUI Apps for Swift Concurrency &#8211; So the Compiler Stays Quiet</a> appeared first on <a rel="nofollow" href="https://www.swiftyplace.com">swiftyplace</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Structuring a SwiftUI app has always been about the same goals for me: well-organized code, components that each do one job, and an app that stays maintainable as it grows. I want to find things quickly, change a feature without breaking something else, and use previews without depending on a server.</p>



<p class="wp-block-paragraph">Swift concurrency added a new problem on top of that, and it&#8217;s painful. As soon as you add networking, caching, or authentication, the compiler starts complaining. Sendable errors, main actor isolation errors, data race warnings. You add <code>@MainActor</code> somewhere, and a new error shows up somewhere else. At some point you start wondering: does Swift concurrency even work with SwiftUI? And if it does, how are you supposed to structure your app so it fits?</p>



<p class="wp-block-paragraph">It does work. In this post, I&#8217;ll show you how I structure my SwiftUI apps so that they stay organized and maintainable, and so that SwiftUI and Swift concurrency work together instead of against each other.</p>



<figure class="gb-block-image gb-block-image-79d04854"><img loading="lazy" decoding="async" width="1437" height="674" class="gb-image gb-image-79d04854" src="http://www.swiftyplace.com/wp-content/uploads/2026/09/UI-Backing-Supporting-Layer-copy.png" alt="Layer architecture in SwiftUI with Swift Concurrency" title="UI &amp; Backing &amp; Supporting Layer copy" srcset="https://www.swiftyplace.com/wp-content/uploads/2026/09/UI-Backing-Supporting-Layer-copy.png 1437w, https://www.swiftyplace.com/wp-content/uploads/2026/09/UI-Backing-Supporting-Layer-copy-300x141.png 300w, https://www.swiftyplace.com/wp-content/uploads/2026/09/UI-Backing-Supporting-Layer-copy-1024x480.png 1024w, https://www.swiftyplace.com/wp-content/uploads/2026/09/UI-Backing-Supporting-Layer-copy-768x360.png 768w" sizes="auto, (max-width: 1437px) 100vw, 1437px" /></figure>



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



<h2 class="wp-block-heading">Concurrency Is Part of Your Architecture</h2>



<p class="wp-block-paragraph">Swift concurrency is built into the type system. Isolation is part of a type, just like its properties and functions. That means every time you decide what goes into a type, you&#8217;re also making a concurrency decision, whether you realize it or not.</p>



<p class="wp-block-paragraph">What decides the isolation a type needs is the state it holds. So when I separate my code into components, I look at their state. There are three kinds I care about: UI state, non-UI state, and no state at all. Each one leads to a different kind of component.</p>



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



<h2 class="wp-block-heading">My View Model is for UI State, and Methods to change UI State</h2>



<p class="wp-block-paragraph">UI state is anything your views read to render themselves: the list of products, a loading flag, an error message. Apple gave us <code>@Observable</code> for exactly this. It tracks which properties a view reads and updates the view when they change.</p>



<p class="wp-block-paragraph">Because SwiftUI renders on the main actor, that state belongs on the main actor 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" 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>@MainActor
@Observable
final class ProductListViewModel {
    var products: &#91;Product&#93; = []
    var isLoading = false
    var errorMessage: String?

    private let service: ProductService

    init(service: ProductService) {
        self.service = service
    }

    func loadProducts() async {
        isLoading = true
        defer { isLoading = false }

        do {
            products = try await service.fetchProducts()
        } catch {
            errorMessage = "Could not load products."
        }
    }
}</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">@MainActor</span></span>
<span class="line"><span style="color: #F286C4">@Observable</span></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">ProductListViewModel</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"> products: &#91;Product&#93; </span><span style="color: #F286C4">=</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"> isLoading </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">var</span><span style="color: #F6F6F4"> errorMessage: </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F286C4">?</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">let</span><span style="color: #F6F6F4"> service: ProductService</span></span>
<span class="line"></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">service</span><span style="color: #F6F6F4">: ProductService) {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #BF9EEE; font-style: italic">self</span><span style="color: #F6F6F4">.service </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> service</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">loadProducts</span><span style="color: #F6F6F4">() </span><span style="color: #F286C4">async</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">        isLoading </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: #F286C4">defer</span><span style="color: #F6F6F4"> { isLoading </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">false</span><span style="color: #F6F6F4"> }</span></span>
<span class="line"></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">            products </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"> service.</span><span style="color: #97E1F1">fetchProducts</span><span style="color: #F6F6F4">()</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">            errorMessage </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Could not load products.</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:30px" aria-hidden="true" class="wp-block-spacer"></div>



<p class="wp-block-paragraph">Thinking about the view model this way made me rethink what I put into it. My view models used to collect everything: caches, tokens, helper state, anything that didn&#8217;t fit elsewhere. Now I follow a simple rule: <strong>the view model owns and mutates UI state</strong>. Functions that are called fromt the UI e.g. button press, call the view model methods that are @MainActor.</p>



<p class="wp-block-paragraph">That keeps my view models small, and focused on a clear Separation of Concern.</p>



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



<h2 class="wp-block-heading">My Services Are Stateless</h2>



<p class="wp-block-paragraph">A service fetches data, decodes it, maybe transforms it, and returns it. It doesn&#8217;t need to remember anything. And when <strong>a type has no state</strong>, there&#8217;s nothing to protect from data races.</p>



<p class="wp-block-paragraph">That makes services the easiest layer to get right. A service is just a 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" 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>nonisolated struct RemoteProductService: ProductService {
    @concurrent
    func fetchProducts() async throws -> &#91;Product&#93; {
        let url = URL(string: "https://api.example.com/products")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode(&#91;Product&#93;.self, from: data)
    }
}</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">nonisolated</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">RemoteProductService</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">ProductService </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@concurrent</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">fetchProducts</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 style="color: #F286C4">-&gt;</span><span style="color: #F6F6F4"> &#91;Product&#93; {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> url </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">URL</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">string</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">https://api.example.com/products</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span><span style="color: #F286C4">!</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> (data, </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"> </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> URLSession.shared.</span><span style="color: #97E1F1">data</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">from</span><span style="color: #F6F6F4">: url)</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">try</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">JSONDecoder</span><span style="color: #F6F6F4">().</span><span style="color: #97E1F1">decode</span><span style="color: #F6F6F4">(&#91;Product&#93;.</span><span style="color: #F286C4">self</span><span style="color: #F6F6F4">, </span><span style="color: #97E1F1">from</span><span style="color: #F6F6F4">: data)</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:30px" aria-hidden="true" class="wp-block-spacer"></div>



<p class="wp-block-paragraph">It&#8217;s nonisolated because it doesn&#8217;t belong to any actor. This allows me to move it across actors. For the heavy work, like decoding a large response, I mark the function with <code>@concurrent</code> so it runs in the background instead of blocking the main actor.</p>



<p class="wp-block-paragraph">The view model calls it with <code>await</code>, the work happens off the main actor, and the result comes back to the view model, which assigns it to its UI state. One clear crossing point, and the compiler checks it for me.</p>



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



<h2 class="wp-block-heading">Heavy Work Inside a View Model</h2>



<p class="wp-block-paragraph">Sometimes part of a view model function does heavy work. Filtering and sorting a large list, for example, or processing an image. The view model is on the main actor, so that work would block the UI.</p>



<p class="wp-block-paragraph">In that case, I pull the heavy part out into its own function and mark it <code>@concurrent</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(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>@MainActor
@Observable
final class ProductListViewModel {
    var allProducts: &#91;Product&#93; = []
    var filteredProducts: &#91;Product&#93; = []

    func search(for query: String) async {
        filteredProducts = await filter(allProducts, matching: query)
    }

    @concurrent
    private func filter(_ products: &#91;Product&#93;, matching query: String) async -> &#91;Product&#93; {
        products
            .filter { $0.name.localizedCaseInsensitiveContains(query) }
            .sorted { $0.name &lt; $1.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">@MainActor</span></span>
<span class="line"><span style="color: #F286C4">@Observable</span></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">ProductListViewModel</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"> allProducts: &#91;Product&#93; </span><span style="color: #F286C4">=</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"> filteredProducts: &#91;Product&#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">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">search</span><span style="color: #F6F6F4">(</span><span style="color: #62E884">for</span><span style="color: #F6F6F4"> </span><span style="color: #FFB86C; font-style: italic">query</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4">) </span><span style="color: #F286C4">async</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">        filteredProducts </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">filter</span><span style="color: #F6F6F4">(allProducts, </span><span style="color: #97E1F1">matching</span><span style="color: #F6F6F4">: query)</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">@concurrent</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">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">filter</span><span style="color: #F6F6F4">(</span><span style="color: #62E884">_</span><span style="color: #F6F6F4"> </span><span style="color: #FFB86C; font-style: italic">products</span><span style="color: #F6F6F4">: &#91;Product&#93;, </span><span style="color: #62E884">matching</span><span style="color: #F6F6F4"> </span><span style="color: #FFB86C; font-style: italic">query</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4">) </span><span style="color: #F286C4">async</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">-&gt;</span><span style="color: #F6F6F4"> &#91;Product&#93; {</span></span>
<span class="line"><span style="color: #F6F6F4">        products</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">filter</span><span style="color: #F6F6F4"> { </span><span style="color: #BF9EEE; font-style: italic">$0</span><span style="color: #F6F6F4">.name.</span><span style="color: #97E1F1">localizedCaseInsensitiveContains</span><span style="color: #F6F6F4">(query) }</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #97E1F1">sorted</span><span style="color: #F6F6F4"> { </span><span style="color: #BF9EEE; font-style: italic">$0</span><span style="color: #F6F6F4">.name </span><span style="color: #F286C4">&lt;</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE; font-style: italic">$1</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 style="height:29px" aria-hidden="true" class="wp-block-spacer"></div>



<p class="wp-block-paragraph">The <code>@concurrent</code> function runs in the background, so it can&#8217;t touch the view model&#8217;s state directly. Everything it needs comes in as parameters, and the result goes back as a return value. The main actor function passes the data in, awaits the result, and assigns it to the UI state.</p>



<p class="wp-block-paragraph">It&#8217;s the same pattern as with services: the state stays on the main actor, and only <code>Sendable</code> values cross over to the background and back.</p>



<p class="wp-block-paragraph">If you notice that you&#8217;re writing a lot of these functions in one view model, that&#8217;s usually a sign the work belongs in a service instead.</p>



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



<h2 class="wp-block-heading">Protocols and Sendable</h2>



<p class="wp-block-paragraph">Sendable errors are really annoying. They show up in places you don&#8217;t expect, and the messages don&#8217;t always tell you what&#8217;s actually wrong.</p>



<p class="wp-block-paragraph">Most of my Sendable errors came from services that were classes with some state hidden inside: a cached response, a counter, a mutable configuration. The moment such a service was stored in a main actor view model and called across isolation boundaries, the compiler complained. And it was right, because that state really could be accessed from two places at once.</p>



<p class="wp-block-paragraph">With a stateless struct, the problem disappears. A struct without mutable state is <code>Sendable</code> naturally, so I put the requirement right into the protocol:</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>nonisolated protocol ProductService: Sendable {
    func fetchProducts() async throws -> &#91;Product&#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">nonisolated</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">protocol</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ProductService</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">Sendable </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">fetchProducts</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 style="color: #F286C4">-&gt;</span><span style="color: #F6F6F4"> &#91;Product&#93;</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



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



<p class="wp-block-paragraph">Every service I inject is now guaranteed to be safe to pass around. And the protocol gives me something I use all the time: I can swap the real service for a mock in previews and tests.</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>nonisolated struct MockProductService: ProductService {
    func fetchProducts() async throws -> &#91;Product&#93; {
        &#91;Product(id: UUID(), name: "Preview Product", price: 9.99)&#93;
    }
}

#Preview {
    ProductListView(viewModel: ProductListViewModel(service: MockProductService()))
}</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">nonisolated</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">MockProductService</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">ProductService </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">fetchProducts</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 style="color: #F286C4">-&gt;</span><span style="color: #F6F6F4"> &#91;Product&#93; {</span></span>
<span class="line"><span style="color: #F6F6F4">        &#91;</span><span style="color: #97E1F1">Product</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">id</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1">UUID</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">Preview Product</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">, </span><span style="color: #97E1F1">price</span><span style="color: #F6F6F4">: </span><span style="color: #BF9EEE">9.99</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>
<span class="line"><span style="color: #97E1F1">#Preview</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">ProductListView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">viewModel</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1">ProductListViewModel</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">service</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1">MockProductService</span><span style="color: #F6F6F4">()))</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



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



<p class="wp-block-paragraph">No server, no network, no Sendable errors. Just data in my preview.</p>



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



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



<h2 class="wp-block-heading">Models: What Crosses From the Background to the Main Actor</h2>



<p class="wp-block-paragraph">Data travels from the service, which runs in the background, to the view model on the main actor. Whatever crosses that boundary has to be <code>Sendable</code>.</p>



<h3 class="wp-block-heading">The Simple Way: A Struct</h3>



<p class="wp-block-paragraph">In most cases, I just use a struct for my model:</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>nonisolated struct Product: Identifiable, Codable, Sendable {
    let id: String
    let name: String
    let price: Decimal
}</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">nonisolated</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">Product</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">Identifiable</span><span style="color: #F6F6F4">, </span><span style="color: #97E1F1; font-style: italic">Codable</span><span style="color: #F6F6F4">, </span><span style="color: #97E1F1; font-style: italic">Sendable </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"> id: </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"> 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"> price: Decimal</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



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



<p class="wp-block-paragraph">The service decodes it in the background and returns it, and the view model assigns it to its UI state. A struct with immutable properties is <code>Sendable</code>, so it crosses the boundary without complaints.</p>



<p class="wp-block-paragraph">For many apps, this is all you need.</p>



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



<h3 class="wp-block-heading">When the UI Modifies the Data: DTOs</h3>



<p class="wp-block-paragraph">Sometimes a struct isn&#8217;t enough. The user can rename a product or mark it as a favorite, and the views should update right away. For that, you want an <code>@Observable</code> class:</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>@MainActor
@Observable
final class Product: Identifiable {
    let id: String
    var name: String
    var isFavorite: Bool

    init(dto: ProductDTO) {
        id = dto.id
        name = dto.name
        isFavorite = dto.isFavorite
    }
}</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">@MainActor</span></span>
<span class="line"><span style="color: #F286C4">@Observable</span></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">Product</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">let</span><span style="color: #F6F6F4"> id: </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"> name: </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"> isFavorite: </span><span style="color: #97E1F1; font-style: italic">Bool</span></span>
<span class="line"></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">dto</span><span style="color: #F6F6F4">: ProductDTO) {</span></span>
<span class="line"><span style="color: #F6F6F4">        id </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> dto.id</span></span>
<span class="line"><span style="color: #F6F6F4">        name </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> dto.name</span></span>
<span class="line"><span style="color: #F6F6F4">        isFavorite </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> dto.isFavorite</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:30px" aria-hidden="true" class="wp-block-spacer"></div>



<p class="wp-block-paragraph">This class is UI state, so it&#8217;s isolated to the main actor. That means the service can&#8217;t create it in the background. Instead, the service returns a DTO, a data transfer object, which is a simple <code>Sendable</code> 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" 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>nonisolated struct ProductDTO: Codable, Sendable {
    let id: String
    let name: String
    let isFavorite: Bool
}</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">nonisolated</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ProductDTO</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">Codable</span><span style="color: #F6F6F4">, </span><span style="color: #97E1F1; font-style: italic">Sendable </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"> id: </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"> 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"> isFavorite: </span><span style="color: #97E1F1; font-style: italic">Bool</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<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>nonisolated struct RemoteProductService: ProductService {
    @concurrent
    func fetchProducts() async throws -> &#91;ProductDTO&#93; {
        let url = URL(string: "https://api.example.com/products")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode(&#91;ProductDTO&#93;.self, from: data)
    }
}</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">nonisolated</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">struct</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">RemoteProductService</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">ProductService </span><span style="color: #F6F6F4">{</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">@concurrent</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">fetchProducts</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 style="color: #F286C4">-&gt;</span><span style="color: #F6F6F4"> &#91;ProductDTO&#93; {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> url </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">URL</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">string</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">https://api.example.com/products</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span><span style="color: #F286C4">!</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> (data, </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"> </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> URLSession.shared.</span><span style="color: #97E1F1">data</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">from</span><span style="color: #F6F6F4">: url)</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">try</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">JSONDecoder</span><span style="color: #F6F6F4">().</span><span style="color: #97E1F1">decode</span><span style="color: #F6F6F4">(&#91;ProductDTO&#93;.</span><span style="color: #F286C4">self</span><span style="color: #F6F6F4">, </span><span style="color: #97E1F1">from</span><span style="color: #F6F6F4">: data)</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:30px" aria-hidden="true" class="wp-block-spacer"></div>



<p class="wp-block-paragraph">The DTO crosses the boundary, and the view model turns it into the observable model on the main actor:</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>func loadProducts() async {
    do {
        let dtos = try await service.fetchProducts()
        products = dtos.map(Product.init)
    } catch {
        errorMessage = "Could not load products."
    }
}</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">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">loadProducts</span><span style="color: #F6F6F4">() </span><span style="color: #F286C4">async</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"> dtos </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"> service.</span><span style="color: #97E1F1">fetchProducts</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        products </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> dtos.</span><span style="color: #97E1F1">map</span><span style="color: #F6F6F4">(Product.</span><span style="color: #F286C4">init</span><span style="color: #F6F6F4">)</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">        errorMessage </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Could not load products.</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:30px" aria-hidden="true" class="wp-block-spacer"></div>



<p class="wp-block-paragraph">So the work is split by isolation. Fetching and decoding happen in the background and produce Sendable DTOs. Creating the observable models happens on the main actor, where they&#8217;re used as UI state. When you save changes, it goes the other way: the view model creates a DTO from the model and hands it to the service.</p>



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



<h2 class="wp-block-heading">State the UI Never Sees Gets Its Own Actor</h2>



<p class="wp-block-paragraph">Some state isn&#8217;t UI state, but it&#8217;s still state. My favorite example is a token store. It holds the authentication token, refreshes it when it expires, and hands it to the network layer. No view ever displays the token.</p>



<p class="wp-block-paragraph">So it doesn&#8217;t belong in the view model, and it doesn&#8217;t belong on the main actor. But it&#8217;s shared mutable state: several requests might ask for the token at the same time, and two of them might try to refresh it at once. That&#8217;s what actors are for:</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>actor TokenStore {
    private var token: String?
    private var refreshTask: Task&lt;String, Error>?

    func validToken() async throws -> String {
        if let token { return token }

        if let refreshTask {
            return try await refreshTask.value
        }

        let task = Task { try await refreshToken() }
        refreshTask = task
        defer { refreshTask = nil }

        let newToken = try await task.value
        token = newToken
        return newToken
    }

    private func refreshToken() async throws -> String {
        // Call your auth endpoint here
        "new-token"
    }
}</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">actor</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">TokenStore</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"> token: </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F286C4">?</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"> refreshTask: Task&lt;</span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4">, </span><span style="color: #97E1F1; font-style: italic">Error</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">validToken</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 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: #F286C4">if</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> token { </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> token }</span></span>
<span class="line"></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"> refreshTask {</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #F286C4">return</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"> refreshTask.</span><span style="color: #BF9EEE">value</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"> task </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">Task</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: #97E1F1">refreshToken</span><span style="color: #F6F6F4">() }</span></span>
<span class="line"><span style="color: #F6F6F4">        refreshTask </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> task</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">defer</span><span style="color: #F6F6F4"> { refreshTask </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>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> newToken </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"> task.</span><span style="color: #BF9EEE">value</span></span>
<span class="line"><span style="color: #F6F6F4">        token </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> newToken</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> newToken</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">refreshToken</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 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: #7B7F8B">// Call your auth endpoint here</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">new-token</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:30px" aria-hidden="true" class="wp-block-spacer"></div>



<p class="wp-block-paragraph">The actor protects its state. If several requests ask for a token while a refresh is running, they all wait for the same refresh instead of starting their own. That kind of bug is really hard to find at runtime, and here the structure prevents it.</p>



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



<h2 class="wp-block-heading">My Cheat Sheet</h2>



<p class="wp-block-paragraph">This is how it all comes together:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>State</th><th>Layer</th><th>Isolation</th></tr></thead><tbody><tr><td>UI state</td><td>View, view model, coordinator</td><td><code>@MainActor</code></td></tr><tr><td>Non-UI state</td><td>Token store, cache</td><td>Its own <code>actor</code></td></tr><tr><td>No state</td><td>Service</td><td>Nonisolated struct, <code>@concurrent</code> functions for heavy work</td></tr><tr><td>Data passed between layers</td><td>Model</td><td><code>Sendable</code> value types</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">When I create a new type, I check what state it holds. That tells me which layer it belongs to and how it&#8217;s isolated, so I don&#8217;t have to fix concurrency errors afterward.</p>



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



<h2 class="wp-block-heading">A Note on Project Settings</h2>



<p class="wp-block-paragraph">The code in this post assumes a new Xcode 26 project, where default main actor isolation is turned on. In that setup, every type is on the main actor unless you opt out, which is why the services, protocols, and models are marked <code>nonisolated</code>. The view model is main actor either way, but I keep the explicit <code>@MainActor</code> so the intent is clear.</p>



<p class="wp-block-paragraph">If your project doesn&#8217;t use default main actor isolation, it&#8217;s the other way around. You don&#8217;t need the <code>nonisolated</code> markers, but you do need to add <code>@MainActor</code> to your view models yourself. <code>@Observable</code> doesn&#8217;t do that for you.</p>



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



<h2 class="wp-block-heading">My Advice</h2>



<p class="wp-block-paragraph">If you&#8217;re fighting Swift concurrency right now, here&#8217;s what I&#8217;d recommend:</p>



<ul class="wp-block-list">
<li><strong>Before you create a type, look at its state.</strong> UI state, non-UI state, or no state. That tells you where it belongs.</li>



<li><strong>Keep your view models for UI state.</strong> If the UI doesn&#8217;t read it, move it out.</li>



<li><strong>Make services stateless whenever you can.</strong> A stateless struct is the easiest thing to make Sendable.</li>



<li><strong>Use protocols that require <code>Sendable</code>.</strong> You get safe injection and easy mocks for previews and tests.</li>



<li><strong>Make your models value types.</strong> They cross every boundary without complaints.</li>



<li><strong>Give shared non-UI state its own actor.</strong> Don&#8217;t hide it in a view model or a singleton.</li>



<li><strong>When you hit a Sendable error, check where your state lives before adding annotations.</strong> Most of the time, the error is pointing at your structure.</li>
</ul>



<p class="wp-block-paragraph">The compiler is showing you exactly where your state is in the wrong place. Once your app is structured around state, it mostly stays quiet.</p>



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



<p class="wp-block-paragraph"></p>



<h2 class="wp-block-heading">Further Reading</h2>



<p class="wp-block-paragraph"><em>If you want to go deeper into how to separate your SwiftUI code, from views and view models to services and modules, check out my course.</em></p>



<p class="wp-block-paragraph"><strong>Apple&#8217;s WWDC25 sessions</strong></p>



<ul class="wp-block-list">
<li><strong><a href="https://developer.apple.com/videos/play/wwdc2025/266" target="_blank" rel="noopener">Explore concurrency in SwiftUI</a>.</strong>&nbsp;A good starting point if you&#8217;re wondering how SwiftUI and Swift concurrency fit together. It shows how SwiftUI runs on the main actor by default and hands work off to other actors.</li>



<li><strong><a href="https://developer.apple.com/videos/play/wwdc2025/268" target="_blank" rel="noopener">Embracing Swift concurrency</a>.</strong>&nbsp;The best overview of the core concepts. It walks through taking an app from single-threaded to concurrent, with chapters on value types, actor-isolated types, and actors that match the layers in this post.</li>



<li><strong><a href="https://developer.apple.com/videos/play/wwdc2025/270" target="_blank" rel="noopener">Code-along: Elevate an app with Swift concurrency</a>.</strong>&nbsp;A hands-on session that starts with a main-actor app, moves work to the background, and works through fixing data-race safety errors. It also covers the approachable concurrency settings.</li>
</ul>



<p class="wp-block-paragraph"><strong>Swift documentation</strong></p>



<ul class="wp-block-list">
<li><strong><a href="https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/" target="_blank" rel="noopener">The Swift Programming Language: Concurrency</a>.</strong>&nbsp;The official language guide chapter on async/await, actors, and Sendable.</li>



<li><strong><a href="https://www.swift.org/migration/documentation/migrationguide/" target="_blank" rel="noopener">Swift 6 Migration Guide</a>.</strong>&nbsp;Useful if you&#8217;re moving an existing project to Swift 6 and hitting data-race errors.</li>
</ul>



<p class="wp-block-paragraph"><strong>Swift Evolution proposals</strong></p>



<p class="wp-block-paragraph">For the details behind the code in this post:</p>



<ul class="wp-block-list">
<li><strong><a href="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0461-async-function-isolation.md" target="_blank" rel="noopener">SE-0461: Run nonisolated async functions on the caller&#8217;s actor by default</a>.</strong>&nbsp;Introduces&nbsp;<code>@concurrent</code>&nbsp;and explains why it&#8217;s needed for background work.</li>



<li><strong><a href="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0466-control-default-actor-isolation.md" target="_blank" rel="noopener">SE-0466: Control default actor isolation inference</a>.</strong>&nbsp;The proposal behind default main actor isolation in new Xcode projects.</li>



<li><strong><a href="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0449-nonisolated-for-global-actor-cutoff.md" target="_blank" rel="noopener">SE-0449: Allow nonisolated to prevent global actor inference</a>.</strong>&nbsp;Explains why you can mark types and protocols as&nbsp;<code>nonisolated</code>.</li>
</ul>



<p class="wp-block-paragraph"></p>
<p>The post <a rel="nofollow" href="https://www.swiftyplace.com/blog/swiftui-architecture-swift-concurrency">How I Architect SwiftUI Apps for Swift Concurrency &#8211; So the Compiler Stays Quiet</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-architecture-swift-concurrency/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>What Is Sendable in Swift? How Swift Concurrency Protects Your State</title>
		<link>https://www.swiftyplace.com/blog/sendable-swift-concurrency?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=sendable-swift-concurrency</link>
					<comments>https://www.swiftyplace.com/blog/sendable-swift-concurrency#respond</comments>
		
		<dc:creator><![CDATA[Karin Prater]]></dc:creator>
		<pubDate>Thu, 08 Oct 2026 11:26:03 +0000</pubDate>
				<category><![CDATA[Swift]]></category>
		<category><![CDATA[Swift Concurrency]]></category>
		<guid isPermaLink="false">https://www.swiftyplace.com/?p=1005731</guid>

					<description><![CDATA[<p>What is Sendable in Swift? A practical explanation of data races, isolation, and why structs, classes, and actors are Sendable or not, with real examples.</p>
<p>The post <a rel="nofollow" href="https://www.swiftyplace.com/blog/sendable-swift-concurrency">What Is Sendable in Swift? How Swift Concurrency Protects Your State</a> appeared first on <a rel="nofollow" href="https://www.swiftyplace.com">swiftyplace</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">If you&#8217;ve worked with Swift concurrency, you&#8217;ve probably run into Sendable errors. A lot of them. And if you&#8217;re like me, your first reaction was to wonder why the compiler suddenly cares so much about whether something is &#8220;Sendable,&#8221; and what that even means.</p>



<p class="wp-block-paragraph">To understand Sendable, you first need to understand the problem it solves. And that problem is one of the most painful kinds of bugs in app development: data races.</p>



<figure class="gb-block-image gb-block-image-93508376"><img loading="lazy" decoding="async" width="971" height="249" class="gb-image gb-image-93508376" src="https://www.swiftyplace.com/wp-content/uploads/2026/10/sendable-error-2.webp" alt="Sendable error in Xcode Swift 6" title="sendable error-2" srcset="https://www.swiftyplace.com/wp-content/uploads/2026/10/sendable-error-2.webp 971w, https://www.swiftyplace.com/wp-content/uploads/2026/10/sendable-error-2-300x77.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2026/10/sendable-error-2-768x197.webp 768w" sizes="auto, (max-width: 971px) 100vw, 971px" /></figure>



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



<h2 class="wp-block-heading">The Crash You Can&#8217;t Reproduce</h2>



<p class="wp-block-paragraph">Let&#8217;s start with some code that many of us have written at some point. Here&#8217;s a simple cache for image data:</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>final class ImageCache {
    private var images: &#91;URL: Data&#93; = &#91;:&#93;

    func image(for url: URL) -> Data? {
        images&#91;url&#93;
    }

    func store(_ data: Data, for url: URL) {
        images&#91;url&#93; = data
    }
}</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">final</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">class</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">ImageCache</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"> images: &#91;URL</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> Data&#93; </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> &#91;</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4">&#93;</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">image</span><span style="color: #F6F6F4">(</span><span style="color: #62E884">for</span><span style="color: #F6F6F4"> </span><span style="color: #FFB86C; font-style: italic">url</span><span style="color: #F6F6F4">: URL) </span><span style="color: #F286C4">-&gt;</span><span style="color: #F6F6F4"> Data</span><span style="color: #F286C4">?</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">        images&#91;url&#93;</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">store</span><span style="color: #F6F6F4">(</span><span style="color: #62E884">_</span><span style="color: #F6F6F4"> </span><span style="color: #FFB86C; font-style: italic">data</span><span style="color: #F6F6F4">: Data, </span><span style="color: #62E884">for</span><span style="color: #F6F6F4"> </span><span style="color: #FFB86C; font-style: italic">url</span><span style="color: #F6F6F4">: URL) {</span></span>
<span class="line"><span style="color: #F6F6F4">        images&#91;url&#93; </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> data</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:30px" aria-hidden="true" class="wp-block-spacer"></div>



<p class="wp-block-paragraph">Nothing special. Now say you show a list of products, and each row loads its image in the background. When a download finishes, it stores the data in the shared cache so you don&#8217;t have to download it again:</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>func loadImages(for urls: &#91;URL&#93;, cache: ImageCache) async {
    await withTaskGroup(of: Void.self) { group in
        for url in urls {
            group.addTask {
                guard let (data, _) = try? await URLSession.shared.data(from: url) else { return }
                cache.store(data, for: url)
            }
        }
    }
}</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">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">loadImages</span><span style="color: #F6F6F4">(</span><span style="color: #62E884">for</span><span style="color: #F6F6F4"> </span><span style="color: #FFB86C; font-style: italic">urls</span><span style="color: #F6F6F4">: &#91;URL&#93;, </span><span style="color: #62E884; font-style: italic">cache</span><span style="color: #F6F6F4">: ImageCache) </span><span style="color: #F286C4">async</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">withTaskGroup</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">of</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">Void</span><span style="color: #F6F6F4">.</span><span style="color: #F286C4">self</span><span style="color: #F6F6F4">) { group </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">for</span><span style="color: #F6F6F4"> url </span><span style="color: #F286C4">in</span><span style="color: #F6F6F4"> urls {</span></span>
<span class="line"><span style="color: #F6F6F4">            group.</span><span style="color: #97E1F1">addTask</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"> (data, </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"> </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> URLSession.shared.</span><span style="color: #97E1F1">data</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">from</span><span style="color: #F6F6F4">: url) </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">                cache.</span><span style="color: #97E1F1">store</span><span style="color: #F6F6F4">(data, </span><span style="color: #97E1F1">for</span><span style="color: #F6F6F4">: url)</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:30px" aria-hidden="true" class="wp-block-spacer"></div>



<p class="wp-block-paragraph">You test it, and everything works. Images load, scrolling is smooth, the cache does its job. You ship it.</p>



<p class="wp-block-paragraph">Then the crash reports come in. Not many, maybe one in a thousand sessions. The stack traces point somewhere deep inside <code>Dictionary</code>. You try to reproduce it on your device, and you can&#8217;t. You scroll the list a hundred times, and nothing happens.</p>



<p class="wp-block-paragraph">This is a data race. And in Swift 5, the compiler didn&#8217;t say a word about it.</p>



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



<h2 class="wp-block-heading">What a Data Race Is</h2>



<p class="wp-block-paragraph">A data race happens when two pieces of code access the same mutable state at the same time, and at least one of them writes to it.</p>



<p class="wp-block-paragraph">In the image cache, that&#8217;s exactly what happens. Two rows finish their downloads at the same moment, and both call <code>store</code> on the same cache. Both write to the same dictionary at once. If the dictionary is in the middle of growing its storage when the second write comes in, its internal memory ends up in a broken state, and the app crashes.</p>



<p class="wp-block-paragraph">You can make this visible with an even smaller example. Here&#8217;s a counter, incremented 1,000 times from concurrent tasks:</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>final class Counter {
    var value = 0

    func increment() {
        value += 1
    }
}

let counter = Counter()

await withTaskGroup(of: Void.self) { group in
    for _ in 0..&lt;1000 {
        group.addTask { counter.increment() }
    }
}

print(counter.value)</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">final</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">class</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">Counter</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"> value </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">func</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">increment</span><span style="color: #F6F6F4">() {</span></span>
<span class="line"><span style="color: #F6F6F4">        value </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>
<span class="line"><span style="color: #F6F6F4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> counter </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">Counter</span><span style="color: #F6F6F4">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">withTaskGroup</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">of</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">Void</span><span style="color: #F6F6F4">.</span><span style="color: #F286C4">self</span><span style="color: #F6F6F4">) { group </span><span style="color: #F286C4">in</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: #BF9EEE">1000</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">        group.</span><span style="color: #97E1F1">addTask</span><span style="color: #F6F6F4"> { counter.</span><span style="color: #97E1F1">increment</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: #97E1F1">print</span><span style="color: #F6F6F4">(counter.</span><span style="color: #BF9EEE">value</span><span style="color: #F6F6F4">)</span></span></code></pre></div>



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



<p class="wp-block-paragraph">You&#8217;d expect 1,000. But run it a few times, and you might get 973, then 988, then 1,000. The result changes every time. <code>value += 1</code> looks like one step, but it&#8217;s really three: read the value, add one, write it back. When two tasks read the same value at the same moment, both write back the same result, and one increment is lost.</p>



<figure class="gb-block-image gb-block-image-8aa316d4"><img loading="lazy" decoding="async" width="1536" height="1024" class="gb-image gb-image-8aa316d4" src="https://www.swiftyplace.com/wp-content/uploads/2026/10/Chaos-vs.-Order_-Serialized-Access.webp" alt="swift concurrency with sendable erros in swift. preventing data races with actors." title="Chaos vs. Order_ Serialized Access" srcset="https://www.swiftyplace.com/wp-content/uploads/2026/10/Chaos-vs.-Order_-Serialized-Access.webp 1536w, https://www.swiftyplace.com/wp-content/uploads/2026/10/Chaos-vs.-Order_-Serialized-Access-300x200.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2026/10/Chaos-vs.-Order_-Serialized-Access-1024x683.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2026/10/Chaos-vs.-Order_-Serialized-Access-768x512.webp 768w" sizes="auto, (max-width: 1536px) 100vw, 1536px" /></figure>



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



<h2 class="wp-block-heading">Why Data Races Are So Painful</h2>



<p class="wp-block-paragraph">Data races are some of the hardest bugs to deal with, for several reasons.</p>



<p class="wp-block-paragraph"><strong>They&#8217;re random.</strong> Whether a race happens depends on timing: which task runs first, how busy the device is, how fast the network responds. That&#8217;s why the cache worked perfectly on your device and crashed for one user in a thousand.</p>



<p class="wp-block-paragraph"><strong>They hide.</strong> The crash often happens far from the actual cause. The stack trace points into <code>Dictionary</code>, but the real problem is that two tasks shared the cache without protection.</p>



<p class="wp-block-paragraph"><strong>They corrupt data silently.</strong> Not every race crashes. The counter didn&#8217;t crash. It just gave the wrong result. In a real app, that could be a wrong total in a shopping cart or a lost user setting, and you might never notice.</p>



<p class="wp-block-paragraph"><strong>Preventing them was all on you.</strong> Before Swift concurrency, you protected shared state with locks or serial dispatch queues. That works, but only if you remember it everywhere, every time. Forget one lock, and the bug is back.</p>



<p class="wp-block-paragraph"><strong>The tools only helped at runtime.</strong> Thread Sanitizer can detect data races, but only if the race actually happens while it&#8217;s running. If the timing doesn&#8217;t line up during your test, it finds nothing.</p>



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



<h2 class="wp-block-heading">Swift Concurrency Moves This into the Compiler</h2>



<p class="wp-block-paragraph">This is the problem Swift concurrency was designed to solve. Instead of hoping you remember every lock, the compiler checks for data races before your app ever runs.</p>



<p class="wp-block-paragraph">To do that, Swift concurrency is built into the type system. Isolation is part of a type, just like its properties and functions. Every piece of code runs in an isolation domain, as described in the <a href="https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/" target="_blank" rel="noopener">concurrency chapter of The Swift Programming Language</a>:</p>



<ul class="wp-block-list">
<li><strong>The main actor</strong>, where your UI code runs.</li>



<li><strong>A specific actor</strong>, which protects its own state.</li>



<li><strong>Nonisolated</strong>, which doesn&#8217;t belong to any actor.</li>
</ul>



<p class="wp-block-paragraph">Inside one isolation domain, only one piece of code runs at a time, so accessing state there is safe. The danger is when values cross from one domain to another, like when the cache is captured by several tasks running at the same time.</p>



<p class="wp-block-paragraph">And this is where the compiler steps in. In Swift 6, the <code>loadImages</code> function from the beginning doesn&#8217;t compile anymore. The compiler sees that the cache is shared between tasks that can run concurrently, and that it isn&#8217;t safe to share. It tells you that <code>ImageCache</code> isn&#8217;t Sendable.</p>


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

<p class="wp-block-paragraph">The crash your users would have hit randomly in production is now a compiler error on your machine. This is what the <a href="https://www.swift.org/migration/documentation/migrationguide/" target="_blank" rel="noopener">Swift 6 migration guide</a> calls data race safety.</p>

</div>


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



<h2 class="wp-block-heading">What Sendable Means</h2>



<p class="wp-block-paragraph">Sendable is a promise that a value is safe to pass from one isolation domain to another. It was introduced in <a href="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0302-concurrent-value-and-concurrent-closures.md" target="_blank" rel="noopener">SE-0302</a>, and you can find the full rules in <a href="https://developer.apple.com/documentation/swift/sendable" target="_blank" rel="noopener">Apple&#8217;s <code>Sendable</code> documentation</a>.</p>



<p class="wp-block-paragraph">When a type is Sendable, the compiler knows that sharing it across tasks or actors can&#8217;t cause a data race. When it isn&#8217;t, the compiler stops you from passing it across.</p>



<p class="wp-block-paragraph">A few things are worth knowing:</p>



<ul class="wp-block-list">
<li><strong>It&#8217;s checked at compile time.</strong> Sendable has no runtime cost. It only exists so the compiler can check your code.</li>



<li><strong>It&#8217;s only checked at boundaries.</strong> Within one isolation domain, non-Sendable types are completely fine. The compiler only cares when a value crosses from one domain to another.</li>



<li><strong>It&#8217;s about state.</strong> Whether a type can be Sendable depends on what state it holds and how that state is protected.</li>
</ul>



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



<h2 class="wp-block-heading">Which Types Are Sendable</h2>



<h3 class="wp-block-heading">Structs and Enums</h3>



<p class="wp-block-paragraph">Structs and enums are value types. When you pass one to another isolation domain, it&#8217;s copied, so both sides have their own copy and nothing is shared.</p>



<p class="wp-block-paragraph">That&#8217;s why a struct can have <code>var</code> properties and still be Sendable. Each copy has its own mutable state, and changing one doesn&#8217;t affect the other.</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 Product: Sendable {
    let id: UUID
    var name: String    // fine, each copy has its own 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">Product</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">Sendable </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"> id: UUID</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 style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// fine, each copy has its own name</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



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



<p class="wp-block-paragraph">A struct or enum is Sendable automatically if all its stored properties are Sendable. Standard types like <code>Int</code>, <code>String</code>, <code>Data</code>, <code>Array</code>, <code>Dictionary</code>, and <code>Optional</code> are Sendable as long as their contents are. One exception: public types don&#8217;t get this automatically, so you need to declare <code>Sendable</code> explicitly.</p>



<p class="wp-block-paragraph">But a struct isn&#8217;t automatically safe just because it&#8217;s a struct. If it holds a class that isn&#8217;t Sendable, the copy still points to the same shared instance:</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 Order {
    var product: Product
    var cache: ImageCache   // a non-Sendable class, so Order isn't Sendable
}</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">Order</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"> product: Product</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> cache: ImageCache   </span><span style="color: #7B7F8B">// a non-Sendable class, so Order isn&#39;t Sendable</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



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



<h3 class="wp-block-heading">Classes Without <code>var</code></h3>



<p class="wp-block-paragraph">A class is a reference type. Passing it around doesn&#8217;t copy it. Everyone shares the same instance.</p>



<p class="wp-block-paragraph">But if nobody can change that instance, sharing it is safe. A class with only <code>let</code> properties of Sendable types can be Sendable. It has to be <code>final</code>, though, because a subclass could add mutable state. And you need to declare the conformance yourself:</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>final class Configuration: Sendable {
    let baseURL: URL
    let timeout: TimeInterval

    init(baseURL: URL, timeout: TimeInterval) {
        self.baseURL = baseURL
        self.timeout = timeout
    }
}</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">final</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">class</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">Configuration</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">Sendable </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"> baseURL: URL</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> timeout: TimeInterval</span></span>
<span class="line"></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">baseURL</span><span style="color: #F6F6F4">: URL, </span><span style="color: #62E884; font-style: italic">timeout</span><span style="color: #F6F6F4">: TimeInterval) {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #BF9EEE; font-style: italic">self</span><span style="color: #F6F6F4">.baseURL </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> baseURL</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #BF9EEE; font-style: italic">self</span><span style="color: #F6F6F4">.timeout </span><span style="color: #F286C4">=</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></code></pre></div>



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



<h3 class="wp-block-heading">Classes With <code>var</code></h3>



<p class="wp-block-paragraph">This is the classic problem, and it&#8217;s exactly what our image cache and counter are. A class with mutable state, shared between isolation domains, means two pieces of code can read and write the same state at the same time.</p>



<p class="wp-block-paragraph">A class like this can&#8217;t simply be Sendable. It needs protection, and there are a few ways to add it.</p>



<p class="wp-block-paragraph"><strong>Isolate it to the main actor.</strong> A <code>@MainActor</code> class is Sendable, because all access to its state happens on the main actor, one at a time. This is what your view models look 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" 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>@MainActor
final class CounterViewModel {
    var count = 0   // protected by the main actor
}</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">@MainActor</span></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">CounterViewModel</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"> count </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">0</span><span style="color: #F6F6F4">   </span><span style="color: #7B7F8B">// protected by the main actor</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



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



<p class="wp-block-paragraph"><strong>Turn it into an actor.</strong> That&#8217;s coming up next.</p>



<p class="wp-block-paragraph"><strong>Protect it with a lock.</strong> You can synchronize the state yourself, for example with <code>Mutex</code> from the Synchronization framework. With older locks like <code>NSLock</code>, you mark the class <code>@unchecked Sendable</code>, which tells the compiler to trust you. More on that later.</p>



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



<h3 class="wp-block-heading">Actors</h3>



<p class="wp-block-paragraph">Actors are always Sendable (WWDC session <a href="https://developer.apple.com/videos/play/wwdc2021/10133/" target="_blank" rel="noopener">Protect mutable state with Swift actors</a>.). They can have mutable state because they protect it themselves: only one caller accesses an actor&#8217;s state at a time. Everyone else waits for their turn, which is why you call an actor&#8217;s methods with <code>await</code> from outside.</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>actor TokenStore {
    private var token: String?

    func store(_ newToken: String) {
        token = newToken
    }
}</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">actor</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">TokenStore</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"> token: </span><span style="color: #97E1F1; font-style: italic">String</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">store</span><span style="color: #F6F6F4">(</span><span style="color: #62E884">_</span><span style="color: #F6F6F4"> </span><span style="color: #FFB86C; font-style: italic">newToken</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">        token </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> newToken</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:30px" aria-hidden="true" class="wp-block-spacer"></div>



<p class="wp-block-paragraph">One thing to keep in mind: an actor protects its own state, not the values you pass into it. The parameters and return values of its methods still need to be Sendable.</p>



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



<h3 class="wp-block-heading">Closures</h3>



<p class="wp-block-paragraph">Closures matter too, because tasks are closures. A closure that runs in another isolation domain needs to be <code>@Sendable</code>, which means it can only capture Sendable values and can&#8217;t capture mutable local variables.</p>



<p class="wp-block-paragraph">That&#8217;s exactly what went wrong in <code>loadImages</code>: the closure passed to <code>addTask</code> captured the non-Sendable cache. Many confusing errors with <code>Task</code> and callbacks come down to this rule.</p>



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



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



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Type</th><th>Sendable?</th><th>Why</th></tr></thead><tbody><tr><td>Struct or enum with Sendable properties</td><td>Yes, automatically</td><td>Copied when passed, nothing is shared</td></tr><tr><td>Struct with a non-Sendable class property</td><td>No</td><td>The copy still shares the class instance</td></tr><tr><td>Final class with only <code>let</code> properties</td><td>Yes, if declared</td><td>Shared, but nobody can change it</td></tr><tr><td>Class with <code>var</code> properties</td><td>No</td><td>Shared mutable state, so a data race risk</td></tr><tr><td><code>@MainActor</code> class</td><td>Yes</td><td>State is protected by the main actor</td></tr><tr><td>Actor</td><td>Yes, always</td><td>State is protected by the actor</td></tr><tr><td>Class with a lock, <code>@unchecked Sendable</code></td><td>Yes, on your word</td><td>You guarantee safety, the compiler doesn&#8217;t check</td></tr></tbody></table></figure>



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



<h2 class="wp-block-heading">Fixing the Image Cache</h2>



<p class="wp-block-paragraph">Back to the cache. Which option fits?</p>



<p class="wp-block-paragraph">The cache is shared mutable state. It needs to be shared, because every row should benefit from images that other rows already downloaded. And the UI doesn&#8217;t read the cache directly. So the right fit is an actor:</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>actor ImageCache {
    private var images: &#91;URL: Data&#93; = &#91;:&#93;

    func image(for url: URL) -> Data? {
        images&#91;url&#93;
    }

    func store(_ data: Data, for url: URL) {
        images&#91;url&#93; = data
    }
}</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">actor</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">ImageCache</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"> images: &#91;URL</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> Data&#93; </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> &#91;</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4">&#93;</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">image</span><span style="color: #F6F6F4">(</span><span style="color: #62E884">for</span><span style="color: #F6F6F4"> </span><span style="color: #FFB86C; font-style: italic">url</span><span style="color: #F6F6F4">: URL) </span><span style="color: #F286C4">-&gt;</span><span style="color: #F6F6F4"> Data</span><span style="color: #F286C4">?</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">        images&#91;url&#93;</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">store</span><span style="color: #F6F6F4">(</span><span style="color: #62E884">_</span><span style="color: #F6F6F4"> </span><span style="color: #FFB86C; font-style: italic">data</span><span style="color: #F6F6F4">: Data, </span><span style="color: #62E884">for</span><span style="color: #F6F6F4"> </span><span style="color: #FFB86C; font-style: italic">url</span><span style="color: #F6F6F4">: URL) {</span></span>
<span class="line"><span style="color: #F6F6F4">        images&#91;url&#93; </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> data</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:30px" aria-hidden="true" class="wp-block-spacer"></div>



<p class="wp-block-paragraph">The only change in <code>loadImages</code> is an <code>await</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>group.addTask {
    guard let (data, _) = try? await URLSession.shared.data(from: url) else { return }
    await cache.store(data, for: url)
}</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">group.</span><span style="color: #97E1F1">addTask</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"> (data, </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"> </span><span style="color: #F286C4">await</span><span style="color: #F6F6F4"> URLSession.shared.</span><span style="color: #97E1F1">data</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">from</span><span style="color: #F6F6F4">: url) </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"> cache.</span><span style="color: #97E1F1">store</span><span style="color: #F6F6F4">(data, </span><span style="color: #97E1F1">for</span><span style="color: #F6F6F4">: url)</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



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



<p class="wp-block-paragraph">Now only one task at a time can access the dictionary. When two downloads finish at the same moment, one waits for the other.</p>



<p class="wp-block-paragraph">Why not the other options? A struct would give every task its own copy, so it wouldn&#8217;t be a shared cache anymore. Putting the cache on the main actor would also be safe, but then every background task would have to hop over to the main actor just to store some data, competing with your UI for no reason.</p>



<p class="wp-block-paragraph">The crash is gone. And it&#8217;s gone not because you remembered a lock, but because the compiler won&#8217;t let you share the cache unsafely again.</p>



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



<h2 class="wp-block-heading">The Real Problem: Shared Mutable State Without Protection</h2>



<p class="wp-block-paragraph">If you take one thing from this post, let it be this: mutable state alone isn&#8217;t the problem. A struct can have <code>var</code> properties and be perfectly safe.</p>



<p class="wp-block-paragraph">The problem is state that is <strong>shared</strong> and <strong>mutable</strong> without <strong>protection</strong>. Take away any one of those three, and the data race is gone:</p>



<ul class="wp-block-list">
<li><strong>Value types</strong> avoid sharing, because every copy is separate.</li>



<li><strong>Immutable classes</strong> avoid mutation, because nobody can change them.</li>



<li><strong>Actors and the main actor</strong> add protection, because only one caller accesses the state at a time.</li>
</ul>



<p class="wp-block-paragraph">Sendable is how the compiler checks that one of these is true for every value crossing a boundary.</p>



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



<h2 class="wp-block-heading">What This Means for Structuring Your App</h2>



<p class="wp-block-paragraph">Once you see Sendable this way, it changes how you structure your code. The question for every type becomes: what kind of state does it hold?</p>



<ul class="wp-block-list">
<li><strong>UI state</strong> lives on the main actor. That&#8217;s your view models and coordinators.</li>



<li><strong>Non-UI state</strong> lives in its own actor. That&#8217;s your image cache or a token store.</li>



<li><strong>No state</strong> means nothing to protect. That&#8217;s stateless services and value-type models.</li>
</ul>



<p class="wp-block-paragraph">When your components are organized this way, most Sendable errors never show up in the first place. I go deeper into this in <a href="https://www.swiftyplace.com/swiftui-architecture-swift-concurrency" target="_blank" rel="noreferrer noopener">How I Structure SwiftUI Apps So the Compiler Stays Quiet</a>.</p>



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



<h2 class="wp-block-heading">What Sendable Errors Are Telling You</h2>



<p class="wp-block-paragraph">Sendable errors can be really annoying. But most of them say the same thing: mutable state is about to cross a boundary without protection.</p>



<p class="wp-block-paragraph">The most common causes I see are:</p>



<ul class="wp-block-list">
<li><strong>Class-based models.</strong> A model class with <code>var</code> properties passed from a service to a view model.</li>



<li><strong>Services with hidden state.</strong> A service class that keeps a cache or a counter inside.</li>



<li><strong>Closures capturing mutable variables.</strong> A <code>var</code> captured by a <code>Task</code> or a callback.</li>
</ul>



<p class="wp-block-paragraph">So before adding annotations to make an error go away, ask where this state should live. Often the fix is to turn a class into a struct, move state into an actor, or make a service stateless.</p>



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



<h2 class="wp-block-heading">The Escape Hatches</h2>



<p class="wp-block-paragraph">Swift gives you ways to turn off the checks: <code>@unchecked Sendable</code> for types and <code>nonisolated(unsafe)</code> for variables.</p>



<p class="wp-block-paragraph">They have legitimate uses. If you protect a class&#8217;s state with a lock yourself, <code>@unchecked Sendable</code> tells the compiler you&#8217;ve got it covered. With <code>Mutex</code> from the Synchronization framework, you often don&#8217;t even need that, because <code>Mutex</code> itself is Sendable, so a final class that stores its state in a <code>let</code> mutex can be checked by the compiler.</p>



<p class="wp-block-paragraph">But if you use these just to silence errors, you&#8217;re bringing back exactly the kind of crash from the beginning of this post. The compiler stops checking, and you&#8217;re back to random crashes you can&#8217;t reproduce.</p>



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



<h2 class="wp-block-heading">Why Some Non-Sendable Code Still Compiles</h2>



<p class="wp-block-paragraph">At some point, you&#8217;ll notice that a non-Sendable value crosses a boundary and the compiler doesn&#8217;t complain. That&#8217;s not a bug.</p>



<p class="wp-block-paragraph">Since Swift 6, the compiler can see when you pass a non-Sendable value to another isolation domain and never use it again in the original one. If you hand it off completely, nothing is shared, so there&#8217;s no risk of a data race. The rules haven&#8217;t changed: the compiler is just smart enough to see that the state isn&#8217;t actually shared.</p>



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



<h2 class="wp-block-heading">Wrap-Up</h2>



<p class="wp-block-paragraph">Data races used to be some of the hardest bugs to find. They were random, they hid behind misleading crash reports, and preventing them depended on never forgetting a single lock.</p>



<p class="wp-block-paragraph">Sendable changes that. It turns those runtime crashes into compiler errors, on your machine, before your users ever see them. The errors can be annoying, but every one of them points to state that could have crashed your app.</p>



<p class="wp-block-paragraph">Think about where your state lives, whether it&#8217;s shared, and how it&#8217;s protected. Once you do, Sendable stops feeling like a fight with the compiler.</p>



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



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<h3 class="wp-block-heading">Further Reading</h3>



<ul class="wp-block-list">
<li><a href="https://developer.apple.com/videos/play/wwdc2022/110351/" target="_blank" rel="noopener">Eliminate data races using Swift Concurrency</a> (WWDC22)</li>



<li><a href="https://developer.apple.com/videos/play/wwdc2021/10133/" target="_blank" rel="noopener">Protect mutable state with Swift actors</a> (WWDC21)</li>



<li><a href="https://developer.apple.com/videos/play/wwdc2024/10169/" target="_blank" rel="noopener">Migrate your app to Swift 6</a> (WWDC24)</li>



<li><a href="https://developer.apple.com/videos/play/wwdc2025/268/" target="_blank" rel="noopener">Embracing Swift concurrency</a> (WWDC25)</li>



<li><a href="https://www.swift.org/migration/documentation/migrationguide/" target="_blank" rel="noopener">Swift 6 migration guide</a></li>



<li><a href="https://www.swiftyplace.com/swiftui-architecture-swift-concurrency" target="_blank" rel="noreferrer noopener">How I Structure SwiftUI Apps So the Compiler Stays Quiet</a>.</li>
</ul>
</blockquote>



<p class="wp-block-paragraph"></p>
<p>The post <a rel="nofollow" href="https://www.swiftyplace.com/blog/sendable-swift-concurrency">What Is Sendable in Swift? How Swift Concurrency Protects Your State</a> appeared first on <a rel="nofollow" href="https://www.swiftyplace.com">swiftyplace</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.swiftyplace.com/blog/sendable-swift-concurrency/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Simple, Consistent, and a Little Repetitive</title>
		<link>https://www.swiftyplace.com/blog/simple-consistent-code?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=simple-consistent-code</link>
					<comments>https://www.swiftyplace.com/blog/simple-consistent-code#respond</comments>
		
		<dc:creator><![CDATA[Karin Prater]]></dc:creator>
		<pubDate>Tue, 06 Oct 2026 08:08:32 +0000</pubDate>
				<category><![CDATA[SwiftUI Architecture]]></category>
		<guid isPermaLink="false">https://www.swiftyplace.com/?p=1005720</guid>

					<description><![CDATA[<p>This might be slightly against the mainstream opinion, but I want to write code that works even if it looks boring. I don&#8217;t care if it&#8217;s clever. I don&#8217;t care if it uses every design ... <a title="Simple, Consistent, and a Little Repetitive" class="read-more" href="https://www.swiftyplace.com/blog/simple-consistent-code" aria-label="More on Simple, Consistent, and a Little Repetitive">Read more</a></p>
<p>The post <a rel="nofollow" href="https://www.swiftyplace.com/blog/simple-consistent-code">Simple, Consistent, and a Little Repetitive</a> appeared first on <a rel="nofollow" href="https://www.swiftyplace.com">swiftyplace</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">This might be slightly against the mainstream opinion, but I want to write code that works even if it looks boring. I don&#8217;t care if it&#8217;s clever. I don&#8217;t care if it uses every design pattern in the book. I care about one thing: what does it feel like to work with this code on a Tuesday afternoon when a ticket comes in and I need to change a tab icon?</p>



<p class="wp-block-paragraph">How many files do I open? How many hops before I find the right line? How confident am I that my change won&#8217;t break something three screens away?</p>



<p class="wp-block-paragraph">I&#8217;ve started measuring architecture by these small, boring moments. And what I&#8217;ve found — again and again — is that the codebases that feel best to work with share a few traits: they&#8217;re simple, they&#8217;re consistent, and they&#8217;re a little repetitive.</p>



<p class="wp-block-paragraph">That last one surprises people. We&#8217;ve been trained to think repetition is a code smell. I think it&#8217;s often the opposite.</p>



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



<h2 class="wp-block-heading">The Allure of Abstraction</h2>



<p class="wp-block-paragraph">Let me show you a pattern I keep running into in SwiftUI projects: the global router. You can see an <a href="https://github.com/Dimillian/IcySky" target="_blank" rel="noopener">example project on Github here</a> which is a BlueSky client.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="678" src="https://www.swiftyplace.com/wp-content/uploads/2026/10/bluesky-client-1024x678.webp" alt="BlueSky client app implemented with router pattern in SwiftUI" class="wp-image-1005724" srcset="https://www.swiftyplace.com/wp-content/uploads/2026/10/bluesky-client-1024x678.webp 1024w, https://www.swiftyplace.com/wp-content/uploads/2026/10/bluesky-client-300x199.webp 300w, https://www.swiftyplace.com/wp-content/uploads/2026/10/bluesky-client-768x508.webp 768w, https://www.swiftyplace.com/wp-content/uploads/2026/10/bluesky-client-1536x1017.webp 1536w, https://www.swiftyplace.com/wp-content/uploads/2026/10/bluesky-client.webp 1837w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">It always starts the same way. You have four tabs, each needs a title and an icon, so you model it as an enum:</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 AppTab: String, CaseIterable, Identifiable {
    case feed
    case notifications
    case profile
    case settings

    var id: String { rawValue }

    var title: String {
        switch self {
        case .feed: "Feed"
        case .notifications: "Notifications"
        case .profile: "Profile"
        case .settings: "Settings"
        }
    }

    var icon: String {
        switch self {
        case .feed: "house"
        case .notifications: "bell"
        case .profile: "person"
        case .settings: "gearshape.fill"
        }
    }
}</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">AppTab</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4">, </span><span style="color: #97E1F1; font-style: italic">CaseIterable</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">case</span><span style="color: #F6F6F4"> feed</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> notifications</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> profile</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> settings</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> id: </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4"> { rawValue }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> title: </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"> .feed</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Feed</span><span style="color: #DEE492">&quot;</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .notifications</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Notifications</span><span style="color: #DEE492">&quot;</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .profile</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Profile</span><span style="color: #DEE492">&quot;</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .settings</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Settings</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>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> icon: </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"> .feed</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">house</span><span style="color: #DEE492">&quot;</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .notifications</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">bell</span><span style="color: #DEE492">&quot;</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .profile</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">person</span><span style="color: #DEE492">&quot;</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .settings</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">gearshape.fill</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:30px" aria-hidden="true" class="wp-block-spacer"></div>



<p class="wp-block-paragraph">Then your tab view loops over the cases:</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 AppTabView: View {
    @State var selectedTab: AppTab = .feed

    var body: some View {
        TabView(selection: $selectedTab) {
            ForEach(AppTab.allCases) { tab in
                tab.rootView
                    .tabItem {
                        Label(tab.title, systemImage: tab.icon)
                    }
                    .tag(tab)
            }
        }
    }
}</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">AppTabView</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"> selectedTab: AppTab </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> .feed</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">TabView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">selection</span><span style="color: #F6F6F4">: $selectedTab) {</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">ForEach</span><span style="color: #F6F6F4">(AppTab.allCases) { tab </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">                tab.rootView</span></span>
<span class="line"><span style="color: #F6F6F4">                    .</span><span style="color: #97E1F1">tabItem</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">(tab.title, </span><span style="color: #97E1F1">systemImage</span><span style="color: #F6F6F4">: tab.icon)</span></span>
<span class="line"><span style="color: #F6F6F4">                    }</span></span>
<span class="line"><span style="color: #F6F6F4">                    .</span><span style="color: #97E1F1">tag</span><span style="color: #F6F6F4">(tab)</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:30px" aria-hidden="true" class="wp-block-spacer"></div>



<p class="wp-block-paragraph">Clean. No repetition. The enum drives everything. This feels right.</p>



<p class="wp-block-paragraph">Then the app grows. You need navigation destinations, so you add a route enum:</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>enum AppRoute: Hashable {
    case postDetail(Post)
    case userProfile(User)
    case settings
    case followers(User)
    case following(User)
    case editProfile
    // ... keeps growing
}</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">AppRoute</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">Hashable </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"> postDetail(Post)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> userProfile(User)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> settings</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> followers(User)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> following(User)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> editProfile</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// ... keeps growing</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



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



<p class="wp-block-paragraph">You need sheets, so you add another enum:</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>enum SheetDestination: Identifiable {
    case auth
    case newPost
    case mediaViewer(MediaItem)
    case editProfile
    case reportContent(Post)

    var id: String { /* ... */ }
}</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">SheetDestination</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">case</span><span style="color: #F6F6F4"> auth</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> newPost</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> mediaViewer(MediaItem)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> editProfile</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> reportContent(Post)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> id: </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4"> { </span><span style="color: #7B7F8B">/* ... */</span><span style="color: #F6F6F4"> }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



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



<p class="wp-block-paragraph">And a centralized modifier to wire it all up:</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 AppDestinationsModifier: ViewModifier {
    @Environment(Router.self) var router

    func body(content: Content) -> some View {
        content
            .navigationDestination(for: AppRoute.self) { route in
                switch route {
                case .postDetail(let post):
                    PostDetailView(post: post)
                case .userProfile(let user):
                    ProfileView(user: user)
                case .settings:
                    SettingsView()
                case .followers(let user):
                    FollowersListView(user: user)
                case .following(let user):
                    FollowingListView(user: user)
                case .editProfile:
                    EditProfileView()
                }
            }
            .sheet(item: $router.presentedSheet) { destination in
                switch destination {
                case .auth:
                    AuthenticationLoginView()
                case .newPost:
                    ComposerView()
                case .mediaViewer(let media):
                    MediaDetailView(media: media)
                case .editProfile:
                    EditProfileView()
                case .reportContent(let post):
                    ReportView(post: post)
                }
            }
    }
}</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">AppDestinationsModifier</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">ViewModifier </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">(Router.</span><span style="color: #F286C4">self</span><span style="color: #F6F6F4">) </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> router</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">body</span><span style="color: #F6F6F4">(</span><span style="color: #62E884; font-style: italic">content</span><span style="color: #F6F6F4">: Content) </span><span style="color: #F286C4">-&gt;</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        content</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">: AppRoute.</span><span style="color: #F286C4">self</span><span style="color: #F6F6F4">) { route </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #F286C4">switch</span><span style="color: #F6F6F4"> route {</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">postDetail</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> post)</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #97E1F1">PostDetailView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">post</span><span style="color: #F6F6F4">: post)</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">userProfile</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> user)</span><span style="color: #F286C4">:</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">user</span><span style="color: #F6F6F4">: user)</span></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .settings</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #97E1F1">SettingsView</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">followers</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> user)</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #97E1F1">FollowersListView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">user</span><span style="color: #F6F6F4">: user)</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">following</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> user)</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #97E1F1">FollowingListView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">user</span><span style="color: #F6F6F4">: user)</span></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .editProfile</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #97E1F1">EditProfileView</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">sheet</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">item</span><span style="color: #F6F6F4">: $router.presentedSheet) { destination </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #F286C4">switch</span><span style="color: #F6F6F4"> destination {</span></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .auth</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #97E1F1">AuthenticationLoginView</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"> .newPost</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #97E1F1">ComposerView</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">mediaViewer</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> media)</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #97E1F1">MediaDetailView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">media</span><span style="color: #F6F6F4">: media)</span></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .editProfile</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #97E1F1">EditProfileView</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">reportContent</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> post)</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #97E1F1">ReportView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">post</span><span style="color: #F6F6F4">: post)</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:30px" aria-hidden="true" class="wp-block-spacer"></div>



<p class="wp-block-paragraph">At this point, search your project for <code>Router</code> or <code>AppRoute</code>. You&#8217;ll find it in nearly every file. That innocent enum has become the most coupled dependency in your entire application.  A god object that everything needs and nothing can work without.</p>



<p class="wp-block-paragraph">Here&#8217;s the boring alternative. No enum. No router. Just tabs:</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>TabView(selection: $selectedTab) {
    FeedView()
        .tabItem { Label("Feed", systemImage: "house") }
        .tag(AppTab.feed)

    NotificationsView()
        .tabItem { Label("Notifications", systemImage: "bell") }
        .tag(AppTab.notifications)

    ProfileView()
        .tabItem { Label("Profile", systemImage: "person") }
        .tag(AppTab.profile)

    SettingsView()
        .tabItem { Label("Settings", systemImage: "gearshape.fill") }
        .tag(AppTab.settings)
}</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">TabView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">selection</span><span style="color: #F6F6F4">: $selectedTab) {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">FeedView</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        .</span><span style="color: #97E1F1">tabItem</span><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">Feed</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">house</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">tag</span><span style="color: #F6F6F4">(AppTab.feed)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">NotificationsView</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        .</span><span style="color: #97E1F1">tabItem</span><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">Notifications</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">bell</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">tag</span><span style="color: #F6F6F4">(AppTab.notifications)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">ProfileView</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        .</span><span style="color: #97E1F1">tabItem</span><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">Profile</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">person</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">tag</span><span style="color: #F6F6F4">(AppTab.profile)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">SettingsView</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        .</span><span style="color: #97E1F1">tabItem</span><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">Settings</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">gearshape.fill</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">tag</span><span style="color: #F6F6F4">(AppTab.settings)</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



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



<p class="wp-block-paragraph">I have to repeat the code to write <code>NavigaitonStack</code> and the destinations for each Tab:</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 FeedView: View {
   ...

    var body: some View {
        NavigationStack(path: $path) {
            FeedsListView()
                .navigationDestination(for: FeedDestination.self) { destination in
                    switch destination {
                        case .feed(let feedItem):
                            PostsFeedView(client: client, feedItem: feedItem)
                        case .post(let post):
                            PostDetailView(post: post)
                        case .timeline:
                            PostsTimelineView(client: client)
                        case .profile(let profile):
                            ProfileView(profile: profile)
                        case .profilePosts(let profile, let filter):
                            PostsProfileView(client: client, profile: profile, filter: filter)
                        case .profileLikes(let profile):
                            PostsLikesView(client: client, profile: profile)
                    }
                }
        }
    }
}</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">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">...</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">: $path) {</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">FeedsListView</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">: FeedDestination.</span><span style="color: #F286C4">self</span><span style="color: #F6F6F4">) { destination </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #F286C4">switch</span><span style="color: #F6F6F4"> destination {</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">feed</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> feedItem)</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">                            </span><span style="color: #97E1F1">PostsFeedView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">client</span><span style="color: #F6F6F4">: client, </span><span style="color: #97E1F1">feedItem</span><span style="color: #F6F6F4">: feedItem)</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">post</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> post)</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">                            </span><span style="color: #97E1F1">PostDetailView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">post</span><span style="color: #F6F6F4">: post)</span></span>
<span class="line"><span style="color: #F6F6F4">                        </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .timeline</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">                            </span><span style="color: #97E1F1">PostsTimelineView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">client</span><span style="color: #F6F6F4">: client)</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">profile</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> profile)</span><span style="color: #F286C4">:</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">profile</span><span style="color: #F6F6F4">: profile)</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">profilePosts</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> profile, </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> filter)</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">                            </span><span style="color: #97E1F1">PostsProfileView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">client</span><span style="color: #F6F6F4">: client, </span><span style="color: #97E1F1">profile</span><span style="color: #F6F6F4">: profile, </span><span style="color: #97E1F1">filter</span><span style="color: #F6F6F4">: filter)</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">profileLikes</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> profile)</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">                            </span><span style="color: #97E1F1">PostsLikesView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">client</span><span style="color: #F6F6F4">: client, </span><span style="color: #97E1F1">profile</span><span style="color: #F6F6F4">: profile)</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:30px" aria-hidden="true" class="wp-block-spacer"></div>



<p class="wp-block-paragraph">More repetitive? Sure. But now let me show you what happens when we start working with both.</p>



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



<h2 class="wp-block-heading">Ticket 1: Change a Tab Icon</h2>



<p class="wp-block-paragraph">First ticket of the day. The Settings tab uses a filled gear icon. Design wants the outline version instead. A one-line change.</p>



<p class="wp-block-paragraph"><strong>The direct version.</strong> I open the app file, I see the tabs, I see <code>"gearshape.fill"</code>, I change it to <code>"gearshape"</code>. One file. Done. The icon is right next to the view it belongs to. I didn&#8217;t have to think.</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>TabView(selection: $selectedTab) {
    FeedListView()
        .tabItem { Label("Feed", systemImage: "house") }
        .tag(AppTab.feed)

    ...

    SettingsView()
        .tabItem { Label("Settings", systemImage: "gearshape.fill") } // change this line
        .tag(AppTab.settings)
}</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">TabView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">selection</span><span style="color: #F6F6F4">: $selectedTab) {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">FeedListView</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        .</span><span style="color: #97E1F1">tabItem</span><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">Feed</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">house</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">tag</span><span style="color: #F6F6F4">(AppTab.feed)</span></span>
<span class="line"></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: #97E1F1">SettingsView</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        .</span><span style="color: #97E1F1">tabItem</span><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">Settings</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">gearshape.fill</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) } </span><span style="color: #7B7F8B">// change this line</span></span>
<span class="line"><span style="color: #F6F6F4">        .</span><span style="color: #97E1F1">tag</span><span style="color: #F6F6F4">(AppTab.settings)</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



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



<p class="wp-block-paragraph"><strong>The router version.</strong> I open the app file, I find the <code>TabView</code>, I see <code>ForEach(AppTab.allCases)</code>. The label references <code>tab.icon</code>. So I navigate to the <code>AppTab</code> enum, find the <code>icon</code> computed property, scroll to the <code>.settings</code> case, and change it there.</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 AppTab: String, CaseIterable, Identifiable {
    case feed
    case notifications
    case profile
    case settings

    var id: String { rawValue }

    var title: String {
       ...
    }

    var icon: String {
        switch self {
        case .feed: "house"
        case .notifications: "bell"
        case .profile: "person"
        case .settings: "gearshape.fill" // change this line
        }
    }
}</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">AppTab</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4">, </span><span style="color: #97E1F1; font-style: italic">CaseIterable</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">case</span><span style="color: #F6F6F4"> feed</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> notifications</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> profile</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> settings</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> id: </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4"> { rawValue }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> title: </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">...</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">var</span><span style="color: #F6F6F4"> icon: </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"> .feed</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">house</span><span style="color: #DEE492">&quot;</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .notifications</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">bell</span><span style="color: #DEE492">&quot;</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .profile</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">person</span><span style="color: #DEE492">&quot;</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .settings</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">gearshape.fill</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4"> </span><span style="color: #7B7F8B">// change this line</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:30px" aria-hidden="true" class="wp-block-spacer"></div>



<p class="wp-block-paragraph">Same result, but I had to open a second file and trace through a level of indirection to get there. The icon doesn&#8217;t live where the tab is. It lives on a property, on an enum case, in a model file.</p>



<p class="wp-block-paragraph">On its own, this is nothing. One extra file. But these &#8220;nothing&#8221; moments happen twenty times a day.</p>



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



<h2 class="wp-block-heading">Ticket 2: Reorder the Tabs</h2>



<p class="wp-block-paragraph">Product wants Profile before Notifications. The tab bar should read: Feed, Profile, Notifications, Settings.</p>



<p class="wp-block-paragraph"><strong>The direct version.</strong> I move the Profile code block above the Notifications block:</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>TabView(selection: $selectedTab) {
    FeedListView()
        .tabItem { Label("Feed", systemImage: "house") }
        .tag(AppTab.feed)

    ProfileView()                // moved up
        .tabItem { Label("Profile", systemImage: "person") }
        .tag(AppTab.profile)

    NotificationsView()          // moved down
        .tabItem { Label("Notifications", systemImage: "bell") }
        .tag(AppTab.notifications)

    SettingsView()
        .tabItem { Label("Settings", systemImage: "gearshape") }
        .tag(AppTab.settings)
}</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">TabView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">selection</span><span style="color: #F6F6F4">: $selectedTab) {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">FeedListView</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        .</span><span style="color: #97E1F1">tabItem</span><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">Feed</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">house</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">tag</span><span style="color: #F6F6F4">(AppTab.feed)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">ProfileView</span><span style="color: #F6F6F4">()                </span><span style="color: #7B7F8B">// moved up</span></span>
<span class="line"><span style="color: #F6F6F4">        .</span><span style="color: #97E1F1">tabItem</span><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">Profile</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">person</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">tag</span><span style="color: #F6F6F4">(AppTab.profile)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">NotificationsView</span><span style="color: #F6F6F4">()          </span><span style="color: #7B7F8B">// moved down</span></span>
<span class="line"><span style="color: #F6F6F4">        .</span><span style="color: #97E1F1">tabItem</span><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">Notifications</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">bell</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">tag</span><span style="color: #F6F6F4">(AppTab.notifications)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">SettingsView</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">        .</span><span style="color: #97E1F1">tabItem</span><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">Settings</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">gearshape</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">tag</span><span style="color: #F6F6F4">(AppTab.settings)</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



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



<p class="wp-block-paragraph">Declaration order is display order. It&#8217;s so obvious it barely requires thought.</p>



<p class="wp-block-paragraph"><strong>The router version.</strong> The <code>TabView</code> uses <code>ForEach(AppTab.allCases)</code>. The <code>allCases</code> property comes from the <code>CaseIterable</code> protocol, which returns cases in the order they&#8217;re declared in the enum. So to reorder the tabs on screen, I reorder the cases in the enum definition:</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>enum AppTab: String, CaseIterable, Identifiable {
    case feed
    case profile        // moved up
    case notifications  // moved down
    case settings
}</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">AppTab</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4">, </span><span style="color: #97E1F1; font-style: italic">CaseIterable</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">case</span><span style="color: #F6F6F4"> feed</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> profile        </span><span style="color: #7B7F8B">// moved up</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> notifications  </span><span style="color: #7B7F8B">// moved down</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> settings</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



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



<p class="wp-block-paragraph">Stop and think about what just happened. I changed a model definition. The declaration order of an enum&#8217;s cases to achieve a UI change. Nothing in the <code>TabView</code> tells you this. Nothing in the view layer hints at it. The connection between &#8220;what I see on screen&#8221; and &#8220;what I need to change in code&#8221; passes through a protocol conformance that&#8217;s completely invisible unless you already know how <code>CaseIterable</code> works.</p>



<p class="wp-block-paragraph">If you know this pattern, you&#8217;ll find it in five minutes. If you&#8217;re a junior developer or an intern picking up this ticket on a Friday afternoon, you might spend half an hour wondering how to reorder the views in the <code>TabView</code> with a <code>ForEach</code>.</p>



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



<h2 class="wp-block-heading">Ticket 3: Add a New Screen</h2>



<p class="wp-block-paragraph">Here&#8217;s where the real cost shows up. The Profile tab needs a new screen: a &#8220;Blocked Users&#8221; list, accessible from the user&#8217;s profile page. Straightforward feature work.</p>



<p class="wp-block-paragraph">But first, let me show you what the router pattern has grown into by this point. As the app added more screens, all navigation destinations were centralized into a single modifier applied to the entire tab 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>public struct AppDestinations: ViewModifier {
    public func body(content: Content) -> some View {
        content
            .navigationDestination(for: RouterDestination.self) { destination in
                switch destination {
                case .feed(let feedItem):
                    PostsFeedView(feedItem: feedItem)
                case .post(let post):
                    PostDetailView(post: post)
                case .timeline:
                    PostsTimelineView()
                case .profile(let profile):
                    ProfileView(profile: profile)
                case .profilePosts(let profile, let filter):
                    PostsProfileView(profile: profile, filter: filter)
                case .profileLikes(let profile):
                    PostsLikesView(profile: profile)
                }
            }
    }
}</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">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">AppDestinations</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">ViewModifier </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">body</span><span style="color: #F6F6F4">(</span><span style="color: #62E884; font-style: italic">content</span><span style="color: #F6F6F4">: Content) </span><span style="color: #F286C4">-&gt;</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">some</span><span style="color: #F6F6F4"> View {</span></span>
<span class="line"><span style="color: #F6F6F4">        content</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">: RouterDestination.</span><span style="color: #F286C4">self</span><span style="color: #F6F6F4">) { destination </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #F286C4">switch</span><span style="color: #F6F6F4"> destination {</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">feed</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> feedItem)</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #97E1F1">PostsFeedView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">feedItem</span><span style="color: #F6F6F4">: feedItem)</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">post</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> post)</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #97E1F1">PostDetailView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">post</span><span style="color: #F6F6F4">: post)</span></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .timeline</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #97E1F1">PostsTimelineView</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">profile</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> profile)</span><span style="color: #F286C4">:</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">profile</span><span style="color: #F6F6F4">: profile)</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">profilePosts</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> profile, </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> filter)</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #97E1F1">PostsProfileView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">profile</span><span style="color: #F6F6F4">: profile, </span><span style="color: #97E1F1">filter</span><span style="color: #F6F6F4">: filter)</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">profileLikes</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> profile)</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">                    </span><span style="color: #97E1F1">PostsLikesView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">profile</span><span style="color: #F6F6F4">: profile)</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:30px" aria-hidden="true" class="wp-block-spacer"></div>



<p class="wp-block-paragraph">Look at this switch statement for a moment. Six destinations. Which ones belong to the Feed tab? Which ones belong to the Profile tab? Can the Feed tab navigate to <code>profileLikes</code>? Can the Profile tab navigate to <code>timeline</code>? You can&#8217;t tell. Everything is flattened into one bucket with no indication of which tab uses what.</p>



<p class="wp-block-paragraph">So, back to the ticket. To add the Blocked Users screen, I need to:</p>



<ol class="wp-block-list">
<li>Add a new case to the <code>RouterDestination</code> enum</li>



<li>Add a new branch to this switch statement</li>



<li>Hope that I&#8217;m not accidentally making this screen reachable from tabs that shouldn&#8217;t have it</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" 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 RouterDestination: Hashable {
    case feed(FeedItem)
    case post(Post)
    case timeline
    case profile(Profile)
    case profilePosts(Profile, PostFilter)
    case profileLikes(Profile)
    case blockedUsers              // new
}</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">RouterDestination</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1; font-style: italic">Hashable </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"> feed(FeedItem)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> post(Post)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> timeline</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> profile(Profile)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> profilePosts(Profile, PostFilter)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> profileLikes(Profile)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> blockedUsers              </span><span style="color: #7B7F8B">// new</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<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>case .blockedUsers:
    BlockedUsersView()</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">case</span><span style="color: #F6F6F4"> .blockedUsers</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">BlockedUsersView</span><span style="color: #F6F6F4">()</span></span></code></pre></div>



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



<p class="wp-block-paragraph">Done? Technically yes. But think about what just happened. I added a Profile feature to a global modifier that the Feed tab, the Notifications tab, and the Settings tab all share. Nothing in this code says &#8220;this destination belongs to Profile.&#8221; It just sits in the pile with everything else.</p>



<p class="wp-block-paragraph">And think about what this file looks like two years from now. The app keeps growing. More screens, more destinations:</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>case .feed(let feedItem): PostsFeedView(feedItem: feedItem)
case .post(let post): PostDetailView(post: post)
case .timeline: PostsTimelineView()
case .profile(let profile): ProfileView(profile: profile)
case .profilePosts(let profile, let filter): PostsProfileView(...)
case .profileLikes(let profile): PostsLikesView(profile: profile)
case .blockedUsers: BlockedUsersView()
case .search(let query): SearchResultsView(query: query)
case .followers(let user): FollowersListView(user: user)
case .following(let user): FollowingListView(user: user)
case .editProfile: EditProfileView()
case .thread(let post): ThreadView(post: post)
case .hashtag(let tag): HashtagFeedView(tag: tag)
case .quotePosts(let post): QuotePostsView(post: post)
case .mutedWords: MutedWordsView()</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">case</span><span style="color: #F6F6F4"> .</span><span style="color: #97E1F1">feed</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> feedItem)</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">PostsFeedView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">feedItem</span><span style="color: #F6F6F4">: feedItem)</span></span>
<span class="line"><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .</span><span style="color: #97E1F1">post</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> post)</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">PostDetailView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">post</span><span style="color: #F6F6F4">: post)</span></span>
<span class="line"><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .timeline</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">PostsTimelineView</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .</span><span style="color: #97E1F1">profile</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> profile)</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">ProfileView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">profile</span><span style="color: #F6F6F4">: profile)</span></span>
<span class="line"><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .</span><span style="color: #97E1F1">profilePosts</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> profile, </span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> filter)</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">PostsProfileView</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">...</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .</span><span style="color: #97E1F1">profileLikes</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> profile)</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">PostsLikesView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">profile</span><span style="color: #F6F6F4">: profile)</span></span>
<span class="line"><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .blockedUsers</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">BlockedUsersView</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .</span><span style="color: #97E1F1">search</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> query)</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">SearchResultsView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">query</span><span style="color: #F6F6F4">: query)</span></span>
<span class="line"><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .</span><span style="color: #97E1F1">followers</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> user)</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">FollowersListView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">user</span><span style="color: #F6F6F4">: user)</span></span>
<span class="line"><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .</span><span style="color: #97E1F1">following</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> user)</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">FollowingListView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">user</span><span style="color: #F6F6F4">: user)</span></span>
<span class="line"><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .editProfile</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">EditProfileView</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .</span><span style="color: #97E1F1">thread</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> post)</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">ThreadView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">post</span><span style="color: #F6F6F4">: post)</span></span>
<span class="line"><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .</span><span style="color: #97E1F1">hashtag</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> tag)</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">HashtagFeedView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">tag</span><span style="color: #F6F6F4">: tag)</span></span>
<span class="line"><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .</span><span style="color: #97E1F1">quotePosts</span><span style="color: #F6F6F4">(</span><span style="color: #F286C4">let</span><span style="color: #F6F6F4"> post)</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">QuotePostsView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">post</span><span style="color: #F6F6F4">: post)</span></span>
<span class="line"><span style="color: #F286C4">case</span><span style="color: #F6F6F4"> .mutedWords</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">MutedWordsView</span><span style="color: #F6F6F4">()</span></span></code></pre></div>



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



<p class="wp-block-paragraph">Fifteen cases. All in one switch. Every developer on the team edits this same file whenever they add a screen, anywhere in the app. Profile developer and Feed developer and Settings developer all touching the same enum, the same switch, the same modifier. Merge conflicts become routine. And the worst part: a change to profile navigation could affect the feed tab, and you wouldn&#8217;t know until runtime.</p>



<p class="wp-block-paragraph">Now imagine a real production app. Not fifteen screens. Fifty. A hundred. This file becomes an archaeological dig site, and every new screen makes the dig deeper.</p>



<p class="wp-block-paragraph">The root cause is structural. You&#8217;ve taken navigation that is inherently local (&#8220;from this screen, you can go to these screens&#8221;) and made it global. Global things don&#8217;t scale.</p>



<p class="wp-block-paragraph"><strong>Now the same ticket with the boring approach.</strong> Each tab has its own <code>NavigationStack</code> and declares its own destinations:</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>// Feed tab
NavigationStack {
    FeedListView()
        .navigationDestination(for: Post.self) { post in
            PostDetailView(post: post)
        }
        .navigationDestination(for: FeedItem.self) { item in
            PostsFeedView(feedItem: item)
        }
}

// Profile tab
NavigationStack {
    ProfileView()
        .navigationDestination(for: PostFilter.self) { filter in
            PostsProfileView(filter: filter)
        }
        .navigationDestination(for: User.self) { user in
            UserDetailView(user: user)
        }
}</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">// Feed tab</span></span>
<span class="line"><span style="color: #97E1F1">NavigationStack</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">FeedListView</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">: Post.</span><span style="color: #F286C4">self</span><span style="color: #F6F6F4">) { post </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">PostDetailView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">post</span><span style="color: #F6F6F4">: post)</span></span>
<span class="line"><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">: FeedItem.</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">PostsFeedView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">feedItem</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>
<span class="line"><span style="color: #7B7F8B">// Profile tab</span></span>
<span class="line"><span style="color: #97E1F1">NavigationStack</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">ProfileView</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">: PostFilter.</span><span style="color: #F286C4">self</span><span style="color: #F6F6F4">) { filter </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">PostsProfileView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">filter</span><span style="color: #F6F6F4">: filter)</span></span>
<span class="line"><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">: User.</span><span style="color: #F286C4">self</span><span style="color: #F6F6F4">) { user </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">UserDetailView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">user</span><span style="color: #F6F6F4">: user)</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:30px" aria-hidden="true" class="wp-block-spacer"></div>



<p class="wp-block-paragraph">Adding the Blocked Users screen? I add a <code>.navigationDestination</code> to the Profile tab&#8217;s stack:</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>// Profile tab
NavigationStack {
    ProfileView()
        .navigationDestination(for: PostFilter.self) { filter in
            PostsProfileView(filter: filter)
        }
        .navigationDestination(for: User.self) { user in
            UserDetailView(user: user)
        }
        .navigationDestination(for: BlockedUsersRoute.self) { _ in
            BlockedUsersView()    // new, only here
        }
}</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">// Profile tab</span></span>
<span class="line"><span style="color: #97E1F1">NavigationStack</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">ProfileView</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">: PostFilter.</span><span style="color: #F286C4">self</span><span style="color: #F6F6F4">) { filter </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">PostsProfileView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">filter</span><span style="color: #F6F6F4">: filter)</span></span>
<span class="line"><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">: User.</span><span style="color: #F286C4">self</span><span style="color: #F6F6F4">) { user </span><span style="color: #F286C4">in</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1">UserDetailView</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">user</span><span style="color: #F6F6F4">: user)</span></span>
<span class="line"><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">: BlockedUsersRoute.</span><span style="color: #F286C4">self</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">BlockedUsersView</span><span style="color: #F6F6F4">()    </span><span style="color: #7B7F8B">// new, only 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:30px" aria-hidden="true" class="wp-block-spacer"></div>



<p class="wp-block-paragraph">That&#8217;s it. One file. One tab. The Feed tab doesn&#8217;t know about it. The Notifications tab isn&#8217;t affected. No shared enum to extend. No shared switch to edit. No merge conflict with the developer adding search to the Feed tab.</p>



<p class="wp-block-paragraph">I can look at the Profile tab&#8217;s code and see every screen it can navigate to. I can look at the Feed tab&#8217;s code and see every screen it can navigate to. The structure tells me the truth. I don&#8217;t need to cross-reference a central registry to figure out which destinations belong where.</p>



<p class="wp-block-paragraph">Yes, I wrote <code>NavigationStack</code> multiple times. Yes, if both Feed and Profile can navigate to <code>PostDetailView</code>, it shows up in two places. That&#8217;s more code. That&#8217;s more repetition. And I&#8217;ll take that trade every single time, because the alternative is a centralized switch statement that grows without limit and tells me nothing about which feature owns which screen.</p>



<p class="wp-block-paragraph">The router version scales by making one file bigger and bigger. The direct version scales by keeping each tab small and independent. One approach has a ceiling. The other doesn&#8217;t.</p>



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



<h2 class="wp-block-heading">The Abstraction Tax</h2>



<p class="wp-block-paragraph">There&#8217;s a pattern running through all three of these tickets. The router version always takes more hops. Always requires opening more files. Always demands that I understand the system before I can make a simple change: the enum, the protocol conformance, the centralized modifier, the destination switch.</p>



<p class="wp-block-paragraph">Each individual layer feels small and reasonable when you add it. &#8220;I&#8217;ll just put the icons on the enum.&#8221; &#8220;I&#8217;ll just centralize the destinations.&#8221; &#8220;I&#8217;ll just use <code>CaseIterable</code> for the tab order.&#8221; Each one is a defensible decision in isolation.</p>



<p class="wp-block-paragraph">But they stack. And the cost isn&#8217;t paid by the person who built the abstraction. They understand it perfectly, they just created it. The cost is paid by every person who reads the code afterward. The next developer on the team. The new hire trying to onboard. You, three months from now, after you&#8217;ve forgotten the details.</p>



<p class="wp-block-paragraph">And those three tickets? They&#8217;re not edge cases. They&#8217;re the most common kind of work in any codebase. Change an icon. Reorder some items. Add a new screen. This is what a normal week looks like. If your architecture makes normal work harder, it&#8217;s not helping you.</p>



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



<h2 class="wp-block-heading">Repetition Isn&#8217;t the Enemy</h2>



<p class="wp-block-paragraph">At this point you might be looking at the boring version with four <code>NavigationStack</code> declarations, <code>PostDetailView</code> showing up in two tabs, <code>.tabItem</code> written out four times, and thinking: isn&#8217;t this exactly what DRY tells me not to do?</p>



<p class="wp-block-paragraph">DRY stands for &#8220;Don&#8217;t Repeat Yourself.&#8221; The idea is straightforward: if the same piece of knowledge exists in two places, eventually one gets updated and the other doesn&#8217;t. You define a tax rate in your checkout flow and again in your invoice generator. Someone updates checkout to 10%. Nobody touches the invoice. It still says 8%. Customers get charged one thing and invoiced another. That&#8217;s a real bug, and DRY prevents it. Put the tax rate in one place, reference it everywhere.</p>



<p class="wp-block-paragraph">But a <code>NavigationStack</code> in the Feed tab and a <code>NavigationStack</code> in the Profile tab aren&#8217;t the same piece of knowledge defined twice. They&#8217;re two different things that happen to use the same API. Each one is scoped to its own tab. Each one owns different destinations. They&#8217;re configured independently and they change independently. There&#8217;s nothing to drift apart because they were never the same thing.</p>



<p class="wp-block-paragraph">Same with <code>PostDetailView</code> appearing as a destination in two tabs. That feels like duplication, but what would actually go wrong? If you change <code>PostDetailView(post:)</code> to <code>PostDetailView(post:showComments:)</code>, every call site that&#8217;s outdated fails to compile. You get a red error in both tabs. You fix them in thirty seconds. The compiler already prevents the drift that DRY is designed to prevent.</p>



<p class="wp-block-paragraph">The discomfort with the boring version is real, but it&#8217;s pattern recognition misfiring. We&#8217;ve trained ourselves to see any repetition as a problem. Sometimes repetition is just structure. Four tabs, four navigation stacks. That&#8217;s not duplication. That&#8217;s how tabs work.</p>



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



<h2 class="wp-block-heading">What &#8220;Simple, Consistent, and a Little Repetitive&#8221; Means</h2>



<p class="wp-block-paragraph">When I say I prefer boring code, I mean something specific.</p>



<p class="wp-block-paragraph"><strong>Simple</strong> means the path from &#8220;what I see on screen&#8221; to &#8220;what I change in code&#8221; is short. If I see a gear icon in the tab bar, I should find <code>"gearshape"</code> within one or two file hops. Not on a computed property on an enum case in a model file.</p>



<p class="wp-block-paragraph"><strong>Consistent</strong> means the same pattern is used the same way everywhere. Every tab has its own <code>NavigationStack</code>. Every tab declares its own destinations locally. You learn the pattern once, and you can predict where things live in any part of the app.</p>



<p class="wp-block-paragraph">New developer onboarding becomes a thirty-second conversation. &#8220;Each tab has a <code>NavigationStack</code>. Destinations are declared in the tab that uses them.&#8221; That&#8217;s the architecture. There&#8217;s no router to learn, no centralized modifier to memorize, no <code>CaseIterable</code> trick to discover.</p>



<p class="wp-block-paragraph"><strong>A little repetitive</strong> means I&#8217;ll write <code>NavigationStack</code> four times. I&#8217;ll write <code>.tabItem { Label(...) }</code> four times. I&#8217;ll declare destinations per tab instead of in a shared modifier. And I won&#8217;t feel bad about it, because each of those repetitions makes the code more local, more self-contained, and more obvious.</p>



<p class="wp-block-paragraph">Every time I&#8217;m tempted to extract a shared abstraction, I ask myself: does this repetition actually cause problems? Will these four <code>NavigationStack</code> declarations drift apart in a way that matters? Or am I just uncomfortable with code that looks a little verbose?</p>



<p class="wp-block-paragraph">Usually it&#8217;s the discomfort. And I&#8217;ve learned to sit with that discomfort, because the alternative, another layer of indirection, another file hop, another concept someone has to learn, is worse.</p>



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



<h2 class="wp-block-heading">The Friday Afternoon Test</h2>



<p class="wp-block-paragraph">Here&#8217;s the test I keep coming back to. It&#8217;s Friday afternoon. You&#8217;ve been coding all week. You&#8217;re tired. A ticket comes in. Change an icon. Reorder tabs. Add a new screen to the Profile tab.</p>



<p class="wp-block-paragraph">In the simple, boring codebase, you open the relevant file, you see the code laid out directly in front of you, you make the change. Ten minutes, including running the app to verify.</p>



<p class="wp-block-paragraph">In the clever, abstracted codebase, you open the file and the actual content is somewhere else. Driven by an enum. Routed through a modifier. Ordered by a protocol conformance. You start jumping. File to file, layer to layer, holding the mental thread of where you started. You find the right spot eventually, but it took three times as long and you&#8217;re still not sure your change won&#8217;t ripple into a tab you never meant to touch.</p>



<p class="wp-block-paragraph">Both get you to the same result. One of them will still work fine when the app has fifty screens and three new developers who&#8217;ve never seen the codebase. The other will have a <code>RouterDestination</code> enum with eighty cases and a <code>AppDestinations</code> modifier that nobody wants to touch.</p>



<p class="wp-block-paragraph">Don&#8217;t add abstractions you don&#8217;t need. Prefer code you can follow without a tour guide, even if it means writing <code>NavigationStack</code> four times. The app doesn&#8217;t care how clever your architecture is. It cares whether you can ship the next ticket without breaking the last one.</p>



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



<h2 class="wp-block-heading">Further Reading</h2>



<p class="wp-block-paragraph"><strong>Navigation in SwiftUI.</strong> Apple&#8217;s documentation on <code>NavigationStack</code> and <code>navigationDestination</code> is worth reading carefully. A lot of the router patterns people reach for exist because they carried habits over from other frameworks. SwiftUI&#8217;s declarative navigation already solves many of the problems routers were built for. Sometimes the best abstraction is the one the framework already gives you.</p>



<ul class="wp-block-list">
<li>WWDC22 Session 10054, <a href="https://developer.apple.com/videos/play/wwdc2022/10054/" target="_blank" rel="noopener">&#8220;The SwiftUI cookbook for navigation&#8221;</a> (shows <code>NavigationStack</code> and <code>navigationDestination</code> in practice, the API the &#8220;boring&#8221; version is built on)</li>



<li>Apple&#8217;s <code><a href="https://developer.apple.com/documentation/swiftui/navigationstack" target="_blank" rel="noopener">NavigationStack</a></code></li>
</ul>



<p class="wp-block-paragraph"><strong>Coordinator pattern.</strong> If your navigation decisions depend on logic that doesn&#8217;t belong in a view, coordinators are worth exploring. A background network request finishes and you need to present a specific sheet based on the result. A push notification arrives and you need to navigate somewhere. Who holds that logic? A coordinator sits above the view layer, owns the navigation state, and makes those decisions. The key: don&#8217;t build one global coordinator. Build small, scoped ones per feature. For most simple apps you won&#8217;t need them, but when navigation starts depending on async events and external triggers, they give you a clean home for that logic.</p>



<p class="wp-block-paragraph"><strong>Modularization with Swift packages.</strong> The global router is a modularization killer. It imports every screen in the app, which means it depends on every feature. You can&#8217;t extract your Profile feature into its own package without the router package also depending on it, and on Feed, and on Settings, and on everything else. The approach in this post avoids that entirely. Each tab already owns its navigation and only knows about its own screens. When the time comes to split into packages, the boundaries are already there.</p>
<p>The post <a rel="nofollow" href="https://www.swiftyplace.com/blog/simple-consistent-code">Simple, Consistent, and a Little Repetitive</a> appeared first on <a rel="nofollow" href="https://www.swiftyplace.com">swiftyplace</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.swiftyplace.com/blog/simple-consistent-code/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>What is @State actually doing in SwiftUI</title>
		<link>https://www.swiftyplace.com/blog/swiftui-state-property-wrapper?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=swiftui-state-property-wrapper</link>
					<comments>https://www.swiftyplace.com/blog/swiftui-state-property-wrapper#respond</comments>
		
		<dc:creator><![CDATA[Karin Prater]]></dc:creator>
		<pubDate>Tue, 22 Sep 2026 10:23:44 +0000</pubDate>
				<category><![CDATA[SwiftUI]]></category>
		<guid isPermaLink="false">https://www.swiftyplace.com/?p=1005694</guid>

					<description><![CDATA[<p>The SwiftUI property wrapper @State doesn't store values in your view struct. It creates a node in SwiftUI's AttributeGraph, and if you don't understand that, you'll write bugs you can't explain. Here's what's actually happening and how to fix the most common mistake.</p>
<p>The post <a rel="nofollow" href="https://www.swiftyplace.com/blog/swiftui-state-property-wrapper">What is @State actually doing in SwiftUI</a> appeared first on <a rel="nofollow" href="https://www.swiftyplace.com">swiftyplace</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">When you write <code>@State private var count = 0</code> in a SwiftUI view, it looks like a normal property declaration. It&#8217;s not. That line doesn&#8217;t store a value inside your view struct. It tells SwiftUI to create a node in something called the <a href="https://www.swiftyplace.com/blog/the-attributegraph-the-engine-behind-every-swiftui-view" data-type="link" data-id="https://www.swiftyplace.com/blog/the-attributegraph-the-engine-behind-every-swiftui-view">AttributeGraph</a>. A runtime data structure that holds your actual state and tracks every dependency in your view hierarchy.</p>



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



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



<p class="wp-block-paragraph">SwiftUI maintains an internal data structure called the AttributeGraph. It&#8217;s a directed graph where every node represents a value. A view&#8217;s body, a modifier, a piece of state, and the edges are the dependencies between them.</p>



<p class="wp-block-paragraph">When a state value changes, SwiftUI walks the edges to find exactly which nodes are affected and only reevaluates those. This is how SwiftUI decides whether your view&#8217;s body needs to run again. It&#8217;s not diffing a DOM. It&#8217;s tracking state dependencies.</p>



<p class="wp-block-paragraph">If you&#8217;ve ever opened Instruments and looked at the SwiftUI cause-and-effect graph, you&#8217;ve seen a slice of this. That&#8217;s the AttributeGraph. The nodes, the edges, the dependency chains. That&#8217;s what&#8217;s running your app.</p>



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



<h2 class="wp-block-heading">What does the @State Property Wrapper do?</h2>



<p class="wp-block-paragraph">When SwiftUI sees <code>@State</code>, it creates a node in the AttributeGraph to store this value. Here&#8217;s what that looks like in practice:</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 {
    // This creates a node in the AttributeGraph that holds the Int.
    // ParentView.body has a dependency edge to this node.
    @State private var counter = 0

    var body: some View {
        VStack {
            // `counter` flows into ChildView as an input — another edge in the graph.
            // When counter changes, SwiftUI walks the edges and knows
            // both ParentView.body and ChildView.body need reevaluation.
            ChildView(label: "You selected (counter)")
            Button("Increment: (counter)") { counter += 1 }
        }
    }
}

struct ChildView: View {
    let label: String  // an input edge from the parent's node
    var body: some View {
        Text(label)
    }
}</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: #7B7F8B">// This creates a node in the AttributeGraph that holds the Int.</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// ParentView.body has a dependency edge to this node.</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"> counter </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: #7B7F8B">// `counter` flows into ChildView as an input — another edge in the graph.</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #7B7F8B">// When counter changes, SwiftUI walks the edges and knows</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #7B7F8B">// both ParentView.body and ChildView.body need reevaluation.</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">label</span><span style="color: #F6F6F4">: </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">You selected (counter)</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">Increment: (counter)</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">) { counter </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>
<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">let</span><span style="color: #F6F6F4"> label: </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4">  </span><span style="color: #7B7F8B">// an input edge from the parent&#39;s 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">Text</span><span style="color: #F6F6F4">(label)</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 class="wp-block-paragraph"><code>@State</code> doesn&#8217;t store the value in <code>ParentView</code>. <code>ParentView</code> is a struct. A short-lived value type. It gets created, body gets evaluated, and it goes away. The actual <code>counter</code> value persists in the AttributeGraph across those struct lifetimes. The struct dies, the node in the graph doesn&#8217;t.</p>



<p class="wp-block-paragraph">If you&#8217;ve opened Instruments and looked at the cause and effect graph, you&#8217;ve seen a slice of this. That&#8217;s the AttributeGraph. The nodes, the edges, the dependency chains. That&#8217;s what&#8217;s running your app.</p>



<figure class="gb-block-image gb-block-image-5de8c37d"><img loading="lazy" decoding="async" width="1702" height="818" class="gb-image gb-image-5de8c37d" src="https://www.swiftyplace.com/wp-content/uploads/2026/09/swiftui-instrument-example-2-3.jpg" alt="" title="swiftui-instrument-example-2-3" srcset="https://www.swiftyplace.com/wp-content/uploads/2026/09/swiftui-instrument-example-2-3.jpg 1702w, https://www.swiftyplace.com/wp-content/uploads/2026/09/swiftui-instrument-example-2-3-300x144.jpg 300w, https://www.swiftyplace.com/wp-content/uploads/2026/09/swiftui-instrument-example-2-3-1024x492.jpg 1024w, https://www.swiftyplace.com/wp-content/uploads/2026/09/swiftui-instrument-example-2-3-768x369.jpg 768w, https://www.swiftyplace.com/wp-content/uploads/2026/09/swiftui-instrument-example-2-3-1536x738.jpg 1536w" sizes="auto, (max-width: 1702px) 100vw, 1702px" /></figure>



<p class="wp-block-paragraph">In the right sidebar, inside Instruments, you can see more details from the AttributeGraph and follow&nbsp;the updating process.</p>



<p class="wp-block-paragraph">(A) This is updating an existing view</p>



<p class="wp-block-paragraph">(B) The source node is a state node from ParentView._counter to</p>



<p class="wp-block-paragraph">(C) Destination ParentView.body</p>



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



<h2 class="wp-block-heading">What happens when you ignore this</h2>



<p class="wp-block-paragraph">This matters the moment you try to initialize <code>@State</code> from a parent value. You have a <code>NavigationSplitView</code>. The sidebar shows a list of users. When someone selects one, the detail pane loads that user&#8217;s profile. The detail view needs a <code>userID</code> from the parent to get started.</p>



<p class="wp-block-paragraph">The natural instinct is to initialize the view model in <code>init</code> and pass the value there:</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 DetailView: View {
    @State private var viewModel: ViewModel

    init(userID: UUID) {
        _viewModel = State(wrappedValue: ViewModel(id: userID))
    }
}</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">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">private</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">var</span><span style="color: #F6F6F4"> viewModel: ViewModel</span></span>
<span class="line"></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">userID</span><span style="color: #F6F6F4">: UUID) {</span></span>
<span class="line"><span style="color: #F6F6F4">        _viewModel </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">State</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">wrappedValue</span><span style="color: #F6F6F4">: </span><span style="color: #97E1F1">ViewModel</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">id</span><span style="color: #F6F6F4">: userID))</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 class="wp-block-paragraph">It works the first time. You select a user, the detail pane loads. Then you select a different user. The detail pane doesn&#8217;t update. It&#8217;s still showing the first user.</p>



<figure class="gb-block-image gb-block-image-3e77882a"><img loading="lazy" decoding="async" width="1544" height="396" class="gb-image gb-image-3e77882a" src="https://www.swiftyplace.com/wp-content/uploads/2026/09/state-init-pass-value-1.jpg" alt="" title="state-init-pass-value-1" srcset="https://www.swiftyplace.com/wp-content/uploads/2026/09/state-init-pass-value-1.jpg 1544w, https://www.swiftyplace.com/wp-content/uploads/2026/09/state-init-pass-value-1-300x77.jpg 300w, https://www.swiftyplace.com/wp-content/uploads/2026/09/state-init-pass-value-1-1024x263.jpg 1024w, https://www.swiftyplace.com/wp-content/uploads/2026/09/state-init-pass-value-1-768x197.jpg 768w, https://www.swiftyplace.com/wp-content/uploads/2026/09/state-init-pass-value-1-1536x394.jpg 1536w" sizes="auto, (max-width: 1544px) 100vw, 1544px" /></figure>



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



<h2 class="wp-block-heading">Why it breaks</h2>



<p class="wp-block-paragraph">Let`s see, how the AttributeGraph looks behind these flow. First the selection is nil and no detail is shown:</p>



<figure class="gb-block-image gb-block-image-737fdeb1"><img loading="lazy" decoding="async" width="818" height="402" class="gb-image gb-image-737fdeb1" src="https://www.swiftyplace.com/wp-content/uploads/2026/09/Screenshot-2026-09-22-at-12.23.28-PM.png" alt="" title="Screenshot 2026-09-22 at 12.23.28 PM" srcset="https://www.swiftyplace.com/wp-content/uploads/2026/09/Screenshot-2026-09-22-at-12.23.28-PM.png 818w, https://www.swiftyplace.com/wp-content/uploads/2026/09/Screenshot-2026-09-22-at-12.23.28-PM-300x147.png 300w, https://www.swiftyplace.com/wp-content/uploads/2026/09/Screenshot-2026-09-22-at-12.23.28-PM-768x377.png 768w" sizes="auto, (max-width: 818px) 100vw, 818px" /></figure>



<p class="wp-block-paragraph">Next selecting an item from the sidebar. The AttributeGraph adds a node for the new DetailView and stores the asscoated state with it:</p>



<figure class="gb-block-image gb-block-image-f92acc2c"><img loading="lazy" decoding="async" width="885" height="418" class="gb-image gb-image-f92acc2c" src="https://www.swiftyplace.com/wp-content/uploads/2026/09/Screenshot-2026-09-22-at-12.26.06-PM.png" alt="" title="Screenshot 2026-09-22 at 12.26.06 PM" srcset="https://www.swiftyplace.com/wp-content/uploads/2026/09/Screenshot-2026-09-22-at-12.26.06-PM.png 885w, https://www.swiftyplace.com/wp-content/uploads/2026/09/Screenshot-2026-09-22-at-12.26.06-PM-300x142.png 300w, https://www.swiftyplace.com/wp-content/uploads/2026/09/Screenshot-2026-09-22-at-12.26.06-PM-768x363.png 768w" sizes="auto, (max-width: 885px) 100vw, 885px" /></figure>



<p class="wp-block-paragraph"><code>@State</code> creates a node in the AttributeGraph. SwiftUI reads the underscore syntax once. When that view identity is born. After that, the storage lives in the graph.</p>



<p class="wp-block-paragraph">When the parent passes a new <code>userID</code>, <code>init</code> runs again. <code>_viewModel = State(wrappedValue: ViewModel(id: newUserID))</code> executes. And SwiftUI throws the new value away. It already has a node for this identity. It doesn&#8217;t need a new one.</p>



<p class="wp-block-paragraph"><strong>You now have two sources of truth.</strong> The parent knows the selection changed. The child&#8217;s view model is still working with the old <code>userID</code>. They&#8217;re out of sync.</p>



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



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



<p class="wp-block-paragraph">Stop trying to re-initialize. React to the change instead. Pass <code>userID</code> as a plain property and let the view model own the response:</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 DetailView: View {
    let userID: UUID
    @State private var viewModel: ViewModel?

    var body: some View {
        UserDetailContent(viewModel: viewModel)
            .task(id: userID) {
                if viewModel == nil {
                    viewModel = ViewModel()
                }
                await viewModel?.load(for: userID)
            }
    }
}</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">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"> userID: UUID</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"> viewModel: ViewModel</span><span style="color: #F286C4">?</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">UserDetailContent</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 style="color: #97E1F1">task</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">id</span><span style="color: #F6F6F4">: userID) {</span></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #F286C4">if</span><span style="color: #F6F6F4"> viewModel </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">                    viewModel </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">ViewModel</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">await</span><span style="color: #F6F6F4"> viewModel</span><span style="color: #F286C4">?</span><span style="color: #F6F6F4">.</span><span style="color: #97E1F1">load</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1">for</span><span style="color: #F6F6F4">: userID)</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 class="wp-block-paragraph"><code>userID</code> is a plain property. An input edge in the graph. <code>@State</code> holds the view model as a node. <code>.task(id:)</code> fires whenever <code>userID</code> changes, including the first time the view appears. The view model is created once, and <code>load(for:)</code> handles every subsequent change.</p>



<p class="wp-block-paragraph">The parent owns the selected ID. The view model owns the loaded data. One source of truth on each side.</p>



<figure class="gb-block-image gb-block-image-37c02728"><img loading="lazy" decoding="async" width="1544" height="396" class="gb-image gb-image-37c02728" src="https://www.swiftyplace.com/wp-content/uploads/2026/09/state-init-pass-value-2.jpg" alt="" title="state-init-pass-value-2" srcset="https://www.swiftyplace.com/wp-content/uploads/2026/09/state-init-pass-value-2.jpg 1544w, https://www.swiftyplace.com/wp-content/uploads/2026/09/state-init-pass-value-2-300x77.jpg 300w, https://www.swiftyplace.com/wp-content/uploads/2026/09/state-init-pass-value-2-1024x263.jpg 1024w, https://www.swiftyplace.com/wp-content/uploads/2026/09/state-init-pass-value-2-768x197.jpg 768w, https://www.swiftyplace.com/wp-content/uploads/2026/09/state-init-pass-value-2-1536x394.jpg 1536w" sizes="auto, (max-width: 1544px) 100vw, 1544px" /></figure>



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



<h2 class="wp-block-heading">Going deeper</h2>



<p class="wp-block-paragraph">This example is from chapter 3 of <a href="https://school.swiftyplace.com/courses/swiftui-handbook-data-flow?coupon=SEPTEMBERRESET" data-type="link" data-id="https://school.swiftyplace.com/courses/swiftui-handbook-data-flow?coupon=SEPTEMBERRESET" target="_blank" rel="noopener">my book, SwiftUI Data Flow.</a> The whole book is built around understanding how the AttributeGraph, view identity, and state ownership actually work. Ao you can reason about your SwiftUI code instead of guessing.</p>



<p class="wp-block-paragraph">30% off until the end of September.</p>



<p class="wp-block-paragraph">Read about the <a href="https://www.swiftyplace.com/blog/the-attributegraph-the-engine-behind-every-swiftui-view" data-type="link" data-id="https://www.swiftyplace.com/blog/the-attributegraph-the-engine-behind-every-swiftui-view">AttributeGraph in this post</a>. </p>
<p>The post <a rel="nofollow" href="https://www.swiftyplace.com/blog/swiftui-state-property-wrapper">What is @State actually doing in SwiftUI</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-state-property-wrapper/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<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-fac2cdea wp-block-group-is-layout-flex">
<p class="wp-block-paragraph">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 class="wp-block-paragraph">Then you use <code>TabView</code>.</p>



<p class="wp-block-paragraph">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 class="wp-block-paragraph"><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 class="wp-block-paragraph">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 class="wp-block-paragraph">The word is <strong>&#8220;appears.&#8221;</strong></p>



<p class="wp-block-paragraph"><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-fb128502 wp-block-group-is-layout-flex">
<h2 class="wp-block-heading">View Node Lifetime and Visibility</h2>



<p class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph"><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 class="wp-block-paragraph"><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-fb128502 wp-block-group-is-layout-flex">
<h2 class="wp-block-heading">The Simple Case: When onAppear fires with conditional views</h2>



<p class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph"><strong>Your text is gone.</strong></p>



<p class="wp-block-paragraph">Here&#8217;s what happened:</p>



<p class="wp-block-paragraph"><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 class="wp-block-paragraph"><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 class="wp-block-paragraph">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-fb128502 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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 loading="lazy" 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="auto, (max-width: 1546px) 100vw, 1546px" /></figure>



<p class="wp-block-paragraph">Now tap the second tab:</p>



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



<p class="wp-block-paragraph"><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 loading="lazy" 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="auto, (max-width: 1546px) 100vw, 1546px" /></figure>



<p class="wp-block-paragraph">Tap back to the first tab:</p>



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



<p class="wp-block-paragraph"><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 loading="lazy" 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="auto, (max-width: 1546px) 100vw, 1546px" /></figure>



<p class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph"><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-fb128502 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 class="wp-block-paragraph">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 class="wp-block-paragraph"><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 class="wp-block-paragraph"><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 class="wp-block-paragraph"><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 class="wp-block-paragraph">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-fb128502 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 class="wp-block-paragraph"><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 class="wp-block-paragraph">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-fb128502 wp-block-group-is-layout-flex">
<h2 class="wp-block-heading">Where init Fits (It Doesn&#8217;t)</h2>



<p class="wp-block-paragraph">One more thing worth clarifying: init is not a lifecycle event.</p>



<p class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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-fb128502 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 class="wp-block-paragraph"><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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph"><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 class="wp-block-paragraph">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-fb128502 wp-block-group-is-layout-flex">
<h2 class="wp-block-heading">Container Cheat Sheet</h2>



<p class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph"></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-fb128502 wp-block-group-is-layout-flex">
<p class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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-fac2cdea wp-block-group-is-layout-flex">
<h2 class="gb-headline gb-headline-d5ed2bd9 gb-headline-text">What Is the AttributeGraph?</h2>



<p class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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-fb128502 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 class="wp-block-paragraph">This is the most important mental shift coming from UIKit.</p>



<p class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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-fb128502 wp-block-group-is-layout-flex">
<h2 class="gb-headline gb-headline-2c3b3666 gb-headline-text">Where @State Actually Lives</h2>



<p class="wp-block-paragraph">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 class="wp-block-paragraph"><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 class="wp-block-paragraph">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 class="wp-block-paragraph">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-fb128502 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 class="wp-block-paragraph">You&#8217;ve heard this phrase a hundred times. Here&#8217;s what it means in terms of the graph.</p>

</div>


<p class="wp-block-paragraph"><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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph"><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 class="wp-block-paragraph">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-fb128502 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 wp-block-paragraph">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 wp-block-paragraph">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 wp-block-paragraph">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 class="wp-block-paragraph">@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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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-fb128502 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 wp-block-paragraph">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 wp-block-paragraph">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 wp-block-paragraph"><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 wp-block-paragraph">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-fb128502 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 class="wp-block-paragraph">The graph needs a way to match your view struct to the right attribute. It does this through <strong>Identity</strong>.</p>



<p class="wp-block-paragraph">There are two kinds:</p>



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



<p class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph"><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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 wp-block-paragraph">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 wp-block-paragraph">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 wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph"><em>“Is this code running on the main thread?”</em></p>
</blockquote>



<p class="wp-block-paragraph">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 class="wp-block-paragraph">&#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 class="wp-block-paragraph"> <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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph"><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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">If you ever call this function from the wrong actor, it stops you <em>immediately</em> during development.</p>



<p class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">It works just like <code>MainActor.assertIsolated</code>.</p>



<p class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph"><a href="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqbXhIOElMNXhydmlxN3VCdzdyNUJrRTI3ejNpUXxBQ3Jtc0tubWxHQ29UZjBQVi1oTFJqMVVRbzQ2cnNCUHlQNEt6ZzJiN3k3Ym1mV29OYUR4OU9UVlNOeFlsUGVuamVVNFhoYnJzVHFZS3JYNHNaV0N6a2tsUURQdTlhejlFY3Y1LWd1ZVQ0NTBPM290MldIemRUYw&amp;q=https%3A%2F%2Fround-lake.dustinice.workers.dev%3A443%2Fhttps%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%2Fround-lake.dustinice.workers.dev%3A443%2Fhttps%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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph"><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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph"><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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph"><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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph"><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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph"><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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph"><a href="https://www.youtube.com/redirect?event=video_description&amp;redir_token=QUFFLUhqbXhIOElMNXhydmlxN3VCdzdyNUJrRTI3ejNpUXxBQ3Jtc0tubWxHQ29UZjBQVi1oTFJqMVVRbzQ2cnNCUHlQNEt6ZzJiN3k3Ym1mV29OYUR4OU9UVlNOeFlsUGVuamVVNFhoYnJzVHFZS3JYNHNaV0N6a2tsUURQdTlhejlFY3Y1LWd1ZVQ0NTBPM290MldIemRUYw&amp;q=https%3A%2F%2Fround-lake.dustinice.workers.dev%3A443%2Fhttps%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%2Fround-lake.dustinice.workers.dev%3A443%2Fhttps%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 class="wp-block-paragraph"><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 class="wp-block-paragraph"></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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph"><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 class="wp-block-paragraph">For now, we’re focusing on <strong>unit testing</strong>.</p>



<p class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">This allows your test code to access <code>internal</code> stuff—functions, classes, etc.—without changing your access levels just for testing.</p>



<p class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">XCTest provides a bunch of <code>XCTAssert*</code> functions to check if your code does what you expect.</p>



<p class="wp-block-paragraph">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 class="wp-block-paragraph"><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 class="wp-block-paragraph">If any of these assertions fail, the test fails, and you get clear feedback inside Xcode.</p>



<p class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">It also exposes a flag <code>deleteDisabled</code> which reflects whether deletion should be possible.</p>



<p class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">We want to test that calling <code>addItem(name:)</code> correctly updates the <code>items</code> array.</p>



<p class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">We want to ensure that <code>deleteLastItem()</code> removes exactly one item and that the correct one is removed.</p>



<p class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">You don’t need to test everything. You need to test what can <strong>hurt you</strong>.</p>



<p class="wp-block-paragraph">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 class="wp-block-paragraph"><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 class="wp-block-paragraph">This is logic that the <strong>user or product owner deeply cares about</strong>. If it breaks, the app loses trust.</p>



<p class="wp-block-paragraph"><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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph"><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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph"><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 class="wp-block-paragraph"><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 class="wp-block-paragraph"><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 class="wp-block-paragraph"><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 class="wp-block-paragraph"><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 class="wp-block-paragraph"><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 class="wp-block-paragraph"><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 class="wp-block-paragraph"><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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">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 class="wp-block-paragraph">Don’t aim for perfect tests. &#8211; Aim for <strong>useful tests</strong> that protect you from regressions and crashes.</p>



<p class="wp-block-paragraph">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>
	</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-10-18 22:36:57 by W3 Total Cache
-->