<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[SwiftWright]]></title><description><![CDATA[Subscribe for deep dives into Swift, SwiftUI, iOS internals, concurrency, performance, and Apple platform engineering.]]></description><link>https://swiftwright.dev</link><image><url>https://substackcdn.com/image/fetch/$s_!cSw4!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59075462-3aa3-4185-ab25-4ae020813b48_1254x1254.png</url><title>SwiftWright</title><link>https://swiftwright.dev</link></image><generator>Substack</generator><lastBuildDate>Mon, 03 Aug 2026 12:02:00 GMT</lastBuildDate><atom:link href="https://swiftwright.dev/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[John Baker Lent]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[jbakerlent@gmail.com]]></webMaster><itunes:owner><itunes:email><![CDATA[jbakerlent@gmail.com]]></itunes:email><itunes:name><![CDATA[John]]></itunes:name></itunes:owner><itunes:author><![CDATA[John]]></itunes:author><googleplay:owner><![CDATA[jbakerlent@gmail.com]]></googleplay:owner><googleplay:email><![CDATA[jbakerlent@gmail.com]]></googleplay:email><googleplay:author><![CDATA[John]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Understanding Swift’s Synchronization Models]]></title><description><![CDATA[One problem. Four different concurrency models.]]></description><link>https://swiftwright.dev/p/understanding-swifts-synchronization</link><guid isPermaLink="false">https://swiftwright.dev/p/understanding-swifts-synchronization</guid><dc:creator><![CDATA[John]]></dc:creator><pubDate>Sun, 12 Jul 2026 23:13:09 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!1S8R!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d652d05-7783-48ff-8ba4-2d94f8087907_864x650.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Over the years Swift has accumulated many different ways of synchronizing data access &#8212; some are so old that newer developers have never used them, others so new that many people aren&#8217;t even sure why they were added. I&#8217;d like to try to address both situations by looking at how four of these systems can help us prevent data races in a simple bank account example. We&#8217;ll see that each has its own mental model, strengths, and trade-offs. If you&#8217;re unfamiliar with data races or what causes them, I cover the topic in more detail here.</p><div class="comment" data-attrs="{&quot;url&quot;:&quot;https://open.substack.com/&quot;,&quot;commentId&quot;:252290270,&quot;comment&quot;:{&quot;id&quot;:252290270,&quot;date&quot;:&quot;2026-05-01T21:03:54.802Z&quot;,&quot;edited_at&quot;:null,&quot;body&quot;:&quot;I&#8217;ve been working with Swift Concurrency more lately and noticed that much of the time engineers use the terms &#8220;data race&#8221; and &#8220;race condition&#8221; interchangeably.\n\nData races are often what we mean when we use both terms&#8212;they refer to the undefined behavior (crashes and incorrect values) that come from accessing the same value simultaneously from multiple threads, when at least one of them is modifying that value.\n\nIn Swift Concurrency, most data races are surfaced at compile time, which is an amazing improvement over GCD which left them to the programmer to catch and fix. But this does not extend to race conditions.\n\nThat&#8217;s because race conditions are not memory level problems, but rather higher level design problems related to temporal coupling (a type of code smell). An example might be firing off a deposit and a withdraw method on a bank account. Because the execution is non-deterministic, the withdrawal could be incorrectly rejected if your API prevents negative balances.\n\nDifferentiating the two terms more clearly has helped me in the way I think about concurrency problems, and I plan on using &#8220;data race&#8221; more regularly instead of imprecisely calling everything a race condition.&quot;,&quot;body_json&quot;:{&quot;type&quot;:&quot;doc&quot;,&quot;attrs&quot;:{&quot;schemaVersion&quot;:&quot;v1&quot;,&quot;title&quot;:null},&quot;content&quot;:[{&quot;type&quot;:&quot;paragraph&quot;,&quot;content&quot;:[{&quot;type&quot;:&quot;text&quot;,&quot;text&quot;:&quot;I&#8217;ve been working with Swift Concurrency more lately and noticed that much of the time engineers use the terms &#8220;&quot;},{&quot;type&quot;:&quot;text&quot;,&quot;marks&quot;:[{&quot;type&quot;:&quot;bold&quot;}],&quot;text&quot;:&quot;data race&quot;},{&quot;type&quot;:&quot;text&quot;,&quot;text&quot;:&quot;&#8221; and &#8220;&quot;},{&quot;type&quot;:&quot;text&quot;,&quot;marks&quot;:[{&quot;type&quot;:&quot;bold&quot;}],&quot;text&quot;:&quot;race condition&quot;},{&quot;type&quot;:&quot;text&quot;,&quot;text&quot;:&quot;&#8221; interchangeably.&quot;}]},{&quot;type&quot;:&quot;paragraph&quot;,&quot;content&quot;:[{&quot;type&quot;:&quot;text&quot;,&quot;marks&quot;:[{&quot;type&quot;:&quot;bold&quot;}],&quot;text&quot;:&quot;Data races&quot;},{&quot;type&quot;:&quot;text&quot;,&quot;text&quot;:&quot; are often what we mean when we use both terms&#8212;they refer to the undefined behavior (crashes and incorrect values) that come from accessing the same value simultaneously from multiple threads, when at least one of them is modifying that value.&quot;}]},{&quot;type&quot;:&quot;paragraph&quot;,&quot;content&quot;:[{&quot;type&quot;:&quot;text&quot;,&quot;text&quot;:&quot;In Swift Concurrency, most data races are surfaced at compile time, which is an amazing improvement over GCD which left them to the programmer to catch and fix. But this does not extend to race conditions.&quot;}]},{&quot;type&quot;:&quot;paragraph&quot;,&quot;content&quot;:[{&quot;type&quot;:&quot;text&quot;,&quot;text&quot;:&quot;That&#8217;s because &quot;},{&quot;type&quot;:&quot;text&quot;,&quot;marks&quot;:[{&quot;type&quot;:&quot;bold&quot;}],&quot;text&quot;:&quot;race conditions&quot;},{&quot;type&quot;:&quot;text&quot;,&quot;text&quot;:&quot; are not memory level problems, but rather higher level design problems related to temporal coupling (a type of code smell). An example might be firing off a deposit and a withdraw method on a bank account. Because the execution is non-deterministic, the withdrawal could be incorrectly rejected if your API prevents negative balances.&quot;}]},{&quot;type&quot;:&quot;paragraph&quot;,&quot;content&quot;:[{&quot;type&quot;:&quot;text&quot;,&quot;text&quot;:&quot;Differentiating the two terms more clearly has helped me in the way I think about concurrency problems, and I plan on using &#8220;data race&#8221; more regularly instead of imprecisely calling everything a race condition.&quot;}]}]},&quot;restacks&quot;:0,&quot;reaction_count&quot;:0,&quot;children_count&quot;:0,&quot;attachments&quot;:[],&quot;name&quot;:&quot;John&quot;,&quot;user_id&quot;:501053456,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5da67900-9527-42d4-9007-fb6997cf3f39_1455x1455.jpeg&quot;,&quot;user_bestseller_tier&quot;:null,&quot;userStatus&quot;:null},&quot;source&quot;:null,&quot;forumChannel&quot;:null}" data-component-name="CommentPlaceholder"></div><p>Let&#8217;s look at our single-threaded bank account implementation and see what goes wrong when it is instead accessed concurrently. To keep our focus on the concurrency models rather than the banking domain, we&#8217;ll keep it super simple and skip things like depositing and handling currency values. I&#8217;m also omitting Swift 6 strict concurrency compliance for the same reasons.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!1S8R!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d652d05-7783-48ff-8ba4-2d94f8087907_864x650.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!1S8R!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d652d05-7783-48ff-8ba4-2d94f8087907_864x650.jpeg 424w, https://substackcdn.com/image/fetch/$s_!1S8R!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d652d05-7783-48ff-8ba4-2d94f8087907_864x650.jpeg 848w, https://substackcdn.com/image/fetch/$s_!1S8R!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d652d05-7783-48ff-8ba4-2d94f8087907_864x650.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!1S8R!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d652d05-7783-48ff-8ba4-2d94f8087907_864x650.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!1S8R!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d652d05-7783-48ff-8ba4-2d94f8087907_864x650.jpeg" width="449" height="337.78935185185185" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5d652d05-7783-48ff-8ba4-2d94f8087907_864x650.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:650,&quot;width&quot;:864,&quot;resizeWidth&quot;:449,&quot;bytes&quot;:49875,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://swiftwright.dev/i/206718714?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d652d05-7783-48ff-8ba4-2d94f8087907_864x650.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!1S8R!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d652d05-7783-48ff-8ba4-2d94f8087907_864x650.jpeg 424w, https://substackcdn.com/image/fetch/$s_!1S8R!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d652d05-7783-48ff-8ba4-2d94f8087907_864x650.jpeg 848w, https://substackcdn.com/image/fetch/$s_!1S8R!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d652d05-7783-48ff-8ba4-2d94f8087907_864x650.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!1S8R!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d652d05-7783-48ff-8ba4-2d94f8087907_864x650.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://swiftwright.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Swiftwright! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h3>Basic Bank Account Implementation</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;bb3217c7-f01d-4ac3-a766-ff277c39b491&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">final class BankAccount {
    private var balance: Int

    init(balance: Int) {
        self.balance = balance
    }

    func withdraw(_ amount: Int) -&gt; Bool {
        guard currentBalance() &gt;= amount else {
            return false
        }

        balance -= amount
        return true
    }

    func currentBalance() -&gt; Int {
        balance
    }
}</code></pre></div><p>Let&#8217;s begin by defining the invariants our implementation should maintain.</p><ol><li><p>A withdrawal must succeed only when sufficient money is available</p></li></ol><ol start="2"><li><p>The balance must never become negative.</p></li></ol><p>And now let&#8217;s see if these hold when our bank account is accessed concurrently. Say there are two account owners and they decide to withdraw some money from an ATM at the same time.</p><p>For a refresher on async let and TaskGroups check out this article.</p><div class="embedded-post-wrap" data-attrs="{&quot;id&quot;:196699862,&quot;url&quot;:&quot;https://swiftwright.dev/p/the-fork-join-pattern-in-swift-concurrency&quot;,&quot;publication_id&quot;:8805172,&quot;embedding_publication_id&quot;:null,&quot;publication_name&quot;:&quot;Swiftwright&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!cSw4!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59075462-3aa3-4185-ab25-4ae020813b48_1254x1254.png&quot;,&quot;title&quot;:&quot;The Fork-Join Pattern in Swift Concurrency&quot;,&quot;truncated_body_text&quot;:&quot;Swift Concurrency gives us two APIs for running work in parallel: async let and TaskGroup. They look quite different at first, but they're both expressions of the same underlying pattern, and I find that understanding that pattern makes the relationship between them much clearer.&quot;,&quot;date&quot;:&quot;2026-05-07T20:47:36.110Z&quot;,&quot;like_count&quot;:1,&quot;comment_count&quot;:1,&quot;bylines&quot;:[{&quot;id&quot;:501053456,&quot;name&quot;:&quot;John&quot;,&quot;handle&quot;:&quot;jbakerlent&quot;,&quot;previous_name&quot;:null,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5da67900-9527-42d4-9007-fb6997cf3f39_1455x1455.jpeg&quot;,&quot;bio&quot;:&quot;Staff iOS Engineer writing about interesting technical topics &#8212; much more to come :)&quot;,&quot;profile_set_up_at&quot;:&quot;2026-04-25T15:53:04.739Z&quot;,&quot;reader_installed_at&quot;:&quot;2026-05-01T23:42:28.252Z&quot;,&quot;publicationUsers&quot;:[{&quot;id&quot;:9023706,&quot;user_id&quot;:501053456,&quot;publication_id&quot;:8805172,&quot;role&quot;:&quot;admin&quot;,&quot;public&quot;:true,&quot;is_primary&quot;:true,&quot;publication&quot;:{&quot;id&quot;:8805172,&quot;name&quot;:&quot;Swiftwright&quot;,&quot;subdomain&quot;:&quot;jbakerlent&quot;,&quot;custom_domain&quot;:&quot;swiftwright.dev&quot;,&quot;custom_domain_optional&quot;:false,&quot;hero_text&quot;:&quot;Subscribe for deep dives into Swift, SwiftUI, iOS internals, concurrency, performance, and Apple platform engineering.&quot;,&quot;logo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/59075462-3aa3-4185-ab25-4ae020813b48_1254x1254.png&quot;,&quot;author_id&quot;:501053456,&quot;primary_user_id&quot;:501053456,&quot;theme_var_background_pop&quot;:&quot;#FF6719&quot;,&quot;created_at&quot;:&quot;2026-04-25T15:55:26.639Z&quot;,&quot;email_from_name&quot;:null,&quot;copyright&quot;:&quot;John Baker Lent&quot;,&quot;founding_plan_name&quot;:null,&quot;community_enabled&quot;:true,&quot;invite_only&quot;:false,&quot;payments_state&quot;:&quot;disabled&quot;,&quot;language&quot;:null,&quot;explicit&quot;:false,&quot;homepage_type&quot;:&quot;magaziney&quot;,&quot;is_personal_mode&quot;:false,&quot;logo_url_wide&quot;:null}}],&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null,&quot;status&quot;:null}],&quot;utm_campaign&quot;:null,&quot;belowTheFold&quot;:true,&quot;type&quot;:&quot;newsletter&quot;,&quot;language&quot;:&quot;en&quot;,&quot;source&quot;:null}" data-component-name="EmbeddedPostToDOM"><a class="embedded-post" native="true" href="https://swiftwright.dev/p/the-fork-join-pattern-in-swift-concurrency?utm_source=substack&amp;utm_campaign=post_embed&amp;utm_medium=web"><div class="embedded-post-header"><img class="embedded-post-publication-logo" src="https://substackcdn.com/image/fetch/$s_!cSw4!,w_56,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59075462-3aa3-4185-ab25-4ae020813b48_1254x1254.png" loading="lazy"><span class="embedded-post-publication-name">Swiftwright</span></div><div class="embedded-post-title-wrapper"><div class="embedded-post-title">The Fork-Join Pattern in Swift Concurrency</div></div><div class="embedded-post-body">Swift Concurrency gives us two APIs for running work in parallel: async let and TaskGroup. They look quite different at first, but they're both expressions of the same underlying pattern, and I find that understanding that pattern makes the relationship between them much clearer&#8230;</div><div class="embedded-post-cta-wrapper"><span class="embedded-post-cta">Read more</span></div><div class="embedded-post-meta">3 months ago &#183; 1 like &#183; 1 comment &#183; John</div></a></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;95018ed1-9461-47dd-985b-7fafbec5f0ed&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">let account = BankAccount(balance: 100)

async let taskA = account.withdraw(80)
async let taskB = account.withdraw(50)

let results = await [taskA, taskB]

Starting balance: 100

Task A reads starting balance = 100
Task B reads starting balance = 100

Task A writes new balance = 20
Task B writes new balance = 50

Total withdrawn: 130
Ending balance: 50</code></pre></div><p>Because the operations may interleave, one possible execution is shown above, but many different interleavings can land us in a pretty bad state. Here we have violated our invariants and allowed a total of $130 to be withdrawn from an account containing only $100, even though the final balance still appears to be positive.</p><p>The issue is that the operations are not actually atomic (i.e. indivisible). Notice that even though <code>balance -= amount</code> looks like one statement in Swift, it is not one indivisible machine operation. It is actually three separate steps: load, decrement, and store. When different threads try to withdraw their execution of the three steps can get interleaved and the state gets corrupted. We need to prevent different threads from modifying the state simultaneously.</p><p>To prevent this, we need a synchronization mechanism that makes the critical section effectively atomic again. Swift gives us several different ways to accomplish this: NSLock, Mutex, Serial Dispatch Queues, and Actors. We&#8217;ll go through them one by one and dig into the details.</p><h3>NSLock</h3><p>We&#8217;ll start each section by previewing the full implementation.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;630bc7c8-0c6d-4453-aa28-b71638f0b8c2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">import Foundation
final class BankAccount {
    private let lock = NSLock()
    private var balance: Int

    init(balance: Int) {
        self.balance = balance
    }

    func withdraw(_ amount: Int) -&gt; Bool {
        lock.withLock {
            guard balance &gt;= amount else {
                return false
            }

            balance -= amount
            return true
        }
    }

    func currentBalance() -&gt; Int {
        lock.withLock {
            balance
        }
    }
}</code></pre></div><p>Note that the closure-based withLock{} syntax is equivalent to the older style of manually calling lock() and unlock(), often in conjunction with a defer{} block to avoid missing early exits. The closure approach is generally clearer and also prevents us from accidentally forgetting to release the lock.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;f5825cc1-2082-431e-bc82-f4d6f6690d66&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">    func currentBalance() -&gt; Int {
        lock.lock()
        defer { lock.unlock() }
        return balance
    }</code></pre></div><p>The purpose of a lock is to create a region of code that only one thread may execute at a time. In our demo above, we noted that &#8220;A withdrawal must succeed only when sufficient money is available.&#8221; We can use that to define a <strong>critical section</strong> of our code that should not overlap and surround it with a lock. We have to be careful that the critical section covers the entire invariant, but no more than necessary. The process a thread will follow is:</p><ol><li><p>Acquire the lock</p></li><li><p>Execute the critical section</p></li><li><p>Release the lock</p></li></ol><p>While one thread owns the lock, other threads are blocked and must wait to acquire the lock before entering the protected region. This is called <strong>mutual exclusion</strong> and is what the term mutex stands for. NSLock is Foundation&#8217;s implementation of a traditional mutual exclusion lock.</p><p>So, we have successfully protected the balance with a lock and avoided data races. But, there are some weaknesses and nuances to using NSLock that we will examine next.</p><p>First, notice that inside our withdraw method I am directly reading the balance property, not using the currentBalance() method. If we ever need to add additional logic to the method, we would need to duplicate it inside withdraw(). Why not just use the method?</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;e2d0b5ba-e2d6-4af6-ac3a-ed7da0297c4d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">guard currentBalance() &gt;= amount else {
  return false
}</code></pre></div><p>Well, that would deadlock at runtime with no warning to the programmer writing it. </p><p>When currentBalance() attempts to acquire the lock, it discovers that the current thread already owns it (from inside withdraw()). So it blocks and waits for the lock to be released. However, withdraw() cannot release the lock until currentBalance() finishes executing. Neither can make progress.</p><p>A second class of issues relates to the fact that the relationship between the state and the lock exists only in the programmer&#8217;s head and the compiler has no visibility into it. This means that correctness must depend on the programmer always correctly understanding when and where locking is needed. Let&#8217;s say we later add a method to add interest to the balance.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;eea292f9-729a-4bbf-9bd6-fa851126b554&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">func applyInterest(_ rate: Double) {
  balance += Int(Double(balance) * rate)
}</code></pre></div><p>We&#8217;re back to having data races. The compiler cannot verify that every access is properly synchronized, so correctness depends entirely on programmer discipline.</p><p>Overall, NSLock is a manual, low-overhead, widely understood option for making a type thread-safe by acting as a gate allowing only one thread through at a time. Its main weakness is that the gating is not directly tied to the underlying data needing protection. We&#8217;ll look at Mutex next and see how it addresses this.</p><h3>Mutex</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;dda50bbb-e630-4859-bf4f-e27de6ff597b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">import Synchronization
final class BankAccount {
    private let balance: Mutex&lt;Int&gt;

    init(balance: Int) {
        self.balance = Mutex(balance)
    }

    func withdraw(_ amount: Int) -&gt; Bool {
        balance.withLock { balance in
            guard balance &gt;= amount else {
                return false
            }

            balance -= amount
            return true
        }
    }

    func currentBalance() -&gt; Int {
        balance.withLock { $0 }
    }
}</code></pre></div><p>Instead of importing Foundation to access NSLock, we are importing Synchronization to access Mutex. The Synchronization framework was introduced in the 2024 OS releases (iOS 18/macOS 15). As the name suggests, Mutex still provides mutual exclusion and operates very similarly to NSLock.</p><p><strong>The improvement is that Mutex puts the state </strong><em><strong>inside</strong></em><strong> the lock</strong>. We no longer need to track balance and the lock separately. In fact, there is no independently accessible Int at all and thus no way to modify it and accidentally forget to apply locking.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;ff7313c0-a6a0-4966-9998-31e86c2f5691&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">func currentBalance() -&gt; Int {
  balance
}
Compiler Error: Cannot convert value of type &#8216;Mutex&lt;Int&gt;&#8217; to expected argument type &#8216;Int&#8217;</code></pre></div><p>The implementation uses Swift&#8217;s newer ownership and isolation features (borrowing, sending, etc.) which allows the issues to be surfaced as errors at compile time. The only way to access the value is to temporarily borrow it through withLock(). Because the compiler understands the synchronization guarantees provided by Mutex, BankAccount can also safely conform to Sendable.</p><p>Two small notes regarding the syntax. Balance is declared as a let constant to prevent the mutex itself from being replaced, but the balance state it manages is still mutable. Second, withLock takes a closure with a parameter for the value stored inside the mutex which is modified via inout, so we reference it via $0 or a named parameter.</p><p>With Mutex we&#8217;re able to improve how state is represented and accessed. The data is directly tied to the fact that it must be accessed via a lock rather than being an undocumented convention. But we still have many of the same potential risks &#8212; deadlock is still possible and we still have to carefully establish the granularity of the lock to cover only the critical section.</p><h3>Serial Queue</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;fa5de535-07f9-40a3-b02e-81ae35ef5fc2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">final class BankAccount {
    private let queue = DispatchQueue(label: &#8220;BankAccount&#8221;)
    private var balance: Int

    init(balance: Int) {
        self.balance = balance
    }

    func withdraw(_ amount: Int) -&gt; Bool {
        queue.sync {
            guard balance &gt;= amount else {
                return false
            }

            balance -= amount
            return true
        }
    }

    func currentBalance() -&gt; Int {
        queue.sync {
            balance
        }
    }
}</code></pre></div><p>Here we have switched to using a serial queue (unless we add the .concurrent attribute, the queue is serial). This is a completely different model for serializing access compared to the locks we&#8217;ve looked at above. Instead of structuring the code to allow a single caller into a critical section we are saying &#8220;Every operation touching this state must be submitted to the same ordered executor.&#8221; <strong>The queue itself becomes the synchronization primitive.</strong> Unlike locks, callers never directly coordinate with one another&#8212;they simply enqueue work.</p><p>The dispatch queue tracks work submitted to it and arranges it so that it always completes in order.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;238ecc87-51b9-46cf-b724-bd281d1c2d74&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">submitted:  A &#8594; B &#8594; C
executed:   A &#8594; B &#8594; C</code></pre></div><p>By dispatching via sync we ensure that work does not continue until the closure completes. As with locks, the queue must enclose the full invariant &#8212; forgetting to wrap currentBalance() in a sync dispatch would again cause data races. And as with NSLock, nothing prevents a future developer from breaking enforcement like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;2bc6f7b0-82db-4438-bfe8-c99ce551c420&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">func reset() {
    balance = 0
}</code></pre></div><p>Beyond the mutual exclusion that NSLock and Mutex provide, queues also provide work scheduling. So they are a good fit when work operations need to be received and processed in order.</p><h3>Actor</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;349205a5-b51c-493f-8dd9-f5e6d9bc3606&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">actor BankAccount {
    private var balance: Int

    init(balance: Int) {
        self.balance = balance
    }

    func withdraw(_ amount: Int) -&gt; Bool {
        guard currentBalance() &gt;= amount else {
            return false
        }

        balance -= amount
        return true
    }

    func currentBalance() -&gt; Int {
        balance
    }
}</code></pre></div><p>The actor version is the smallest implementation so far. There is no explicit lock or queue logic because the actor declaration establishes an <em>isolation boundary</em>. This isolation means that the actor&#8217;s state can only be accessed directly by code running on that actor. So, within BankAccount, we do not need to do any manual synchronization. Code outside the actor interacts with it asynchronously by awaiting actor-isolated methods. It&#8217;s worth noting that having the async API cascade outward is not always a desired effect &#8212; callers must all either already be in an asynchronous context or create one.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;671fd4c3-adcb-492f-9a32-0dff076c263b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">Calls outside the actor:
Task {
  let balance = await account.currentBalance()
  let succeeded = await account.withdraw(80)
}</code></pre></div><p>I&#8217;ve covered suspension points and continuations in more detail in this article.</p><div class="embedded-post-wrap" data-attrs="{&quot;id&quot;:196269526,&quot;url&quot;:&quot;https://swiftwright.dev/p/why-asyncawait-isnt-just-prettier&quot;,&quot;publication_id&quot;:8805172,&quot;embedding_publication_id&quot;:null,&quot;publication_name&quot;:&quot;Swiftwright&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!cSw4!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59075462-3aa3-4185-ab25-4ae020813b48_1254x1254.png&quot;,&quot;title&quot;:&quot;Why Async/Await Isn&#8217;t Just Prettier GCD&quot;,&quot;truncated_body_text&quot;:&quot;When writing asynchronous code using async/await, the most obvious improvements are syntactical ones &#8212; being able to read the code top-to-bottom and see the execution order, not dealing with masses of completion handlers or complex error handling, etc.&quot;,&quot;date&quot;:&quot;2026-05-02T23:57:23.906Z&quot;,&quot;like_count&quot;:1,&quot;comment_count&quot;:1,&quot;bylines&quot;:[{&quot;id&quot;:501053456,&quot;name&quot;:&quot;John&quot;,&quot;handle&quot;:&quot;jbakerlent&quot;,&quot;previous_name&quot;:null,&quot;photo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5da67900-9527-42d4-9007-fb6997cf3f39_1455x1455.jpeg&quot;,&quot;bio&quot;:&quot;Staff iOS Engineer writing about interesting technical topics &#8212; much more to come :)&quot;,&quot;profile_set_up_at&quot;:&quot;2026-04-25T15:53:04.739Z&quot;,&quot;reader_installed_at&quot;:&quot;2026-05-01T23:42:28.252Z&quot;,&quot;publicationUsers&quot;:[{&quot;id&quot;:9023706,&quot;user_id&quot;:501053456,&quot;publication_id&quot;:8805172,&quot;role&quot;:&quot;admin&quot;,&quot;public&quot;:true,&quot;is_primary&quot;:true,&quot;publication&quot;:{&quot;id&quot;:8805172,&quot;name&quot;:&quot;Swiftwright&quot;,&quot;subdomain&quot;:&quot;jbakerlent&quot;,&quot;custom_domain&quot;:&quot;swiftwright.dev&quot;,&quot;custom_domain_optional&quot;:false,&quot;hero_text&quot;:&quot;Subscribe for deep dives into Swift, SwiftUI, iOS internals, concurrency, performance, and Apple platform engineering.&quot;,&quot;logo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/59075462-3aa3-4185-ab25-4ae020813b48_1254x1254.png&quot;,&quot;author_id&quot;:501053456,&quot;primary_user_id&quot;:501053456,&quot;theme_var_background_pop&quot;:&quot;#FF6719&quot;,&quot;created_at&quot;:&quot;2026-04-25T15:55:26.639Z&quot;,&quot;email_from_name&quot;:null,&quot;copyright&quot;:&quot;John Baker Lent&quot;,&quot;founding_plan_name&quot;:null,&quot;community_enabled&quot;:true,&quot;invite_only&quot;:false,&quot;payments_state&quot;:&quot;disabled&quot;,&quot;language&quot;:null,&quot;explicit&quot;:false,&quot;homepage_type&quot;:&quot;magaziney&quot;,&quot;is_personal_mode&quot;:false,&quot;logo_url_wide&quot;:null}}],&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:null,&quot;status&quot;:null}],&quot;utm_campaign&quot;:null,&quot;belowTheFold&quot;:true,&quot;type&quot;:&quot;newsletter&quot;,&quot;language&quot;:&quot;en&quot;,&quot;source&quot;:null}" data-component-name="EmbeddedPostToDOM"><a class="embedded-post" native="true" href="https://swiftwright.dev/p/why-asyncawait-isnt-just-prettier?utm_source=substack&amp;utm_campaign=post_embed&amp;utm_medium=web"><div class="embedded-post-header"><img class="embedded-post-publication-logo" src="https://substackcdn.com/image/fetch/$s_!cSw4!,w_56,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59075462-3aa3-4185-ab25-4ae020813b48_1254x1254.png" loading="lazy"><span class="embedded-post-publication-name">Swiftwright</span></div><div class="embedded-post-title-wrapper"><div class="embedded-post-title">Why Async/Await Isn&#8217;t Just Prettier GCD</div></div><div class="embedded-post-body">When writing asynchronous code using async/await, the most obvious improvements are syntactical ones &#8212; being able to read the code top-to-bottom and see the execution order, not dealing with masses of completion handlers or complex error handling, etc&#8230;</div><div class="embedded-post-cta-wrapper"><span class="embedded-post-cta">Read more</span></div><div class="embedded-post-meta">3 months ago &#183; 1 like &#183; 1 comment &#183; John</div></a></div><p>But the key concept here is that await marks a potential suspension point. The actor may already be processing an operation and so the caller may need to suspend until the actor can execute it. We are relying on the compiler to fully enforce isolation rather than relying on convention.</p><p>Note that we are able to safely call the currentBalance() method directly without issue. Because these are both synchronous and actor-isolated, this is totally safe. That reasoning changes when an actor method contains await, though. Suppose we need to add a remote authorization before withdrawing:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;7c66e895-61d2-40cf-82a9-de846f0ca031&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">func withdraw(_ amount: Int) async -&gt; Bool {
    guard currentBalance() &gt;= amount else {
        return false
    }

    await authorize(amount)

    balance -= amount
    return true
}</code></pre></div><p>BankAccount is an actor so this seems safe at first glance. <strong>Every await divides the method into independent execution segments.</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;56419290-1b93-4583-b7d5-29f28a35669f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Segment 1: check balance
await authorization
Segment 2: subtract amount</code></pre></div><p>While the actor is awaiting authorization it is allowed to process another operation. In some ways, this is a welcome ability &#8212; the actor can remain productive and complete other work while slow operations wait. But here it means that the balance could be changed by a second operation started while the first withdraw call is suspended waiting on the authorization check to complete. This situation is called <strong>reentrancy</strong>.</p><p>The balance must be checked again after any async operations complete.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;31b4c00d-5a64-46b9-b77a-cdd224cf77b7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">func withdraw(_ amount: Int) async -&gt; Bool {
    guard currentBalance() &gt;= amount else {
        return false
    }

    guard await authorize(amount) else {
        return false
    }

    guard currentBalance() &gt;= amount else {
        return false
    }

    balance -= amount
    return true
}</code></pre></div><p>The first balance check is an optimization to avoid an unnecessary authorization; the second is the correctness check. Or, if we wanted to avoid directly checking the balance twice, we could restructure it like this.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;c9da1453-cb16-4992-9eb1-f2d042258b53&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">func withdraw(_ amount: Int) async -&gt; Bool {
    guard await authorize(amount) else {
        return false
    }

    guard currentBalance() &gt;= amount else {
        return false
    }

    balance -= amount
    return true
}</code></pre></div><p>Actor isolation guarantees exclusive access only while an actor-isolated segment is executing. <strong>Isolation does not extend across suspension points</strong>. In other words, every await inside an actor is a signal to check any assumptions established before the await and recheck them after resuming. </p><p>Actors move synchronization into the language itself so that the compiler can prevent unsynchronized access and eliminate the manual auditing required by locks and queues. But they replace reasoning about lock ownership with reasoning about suspension points. We are freed from data races, but still need to make sure we avoid higher-level logical race conditions due to reentrancy.</p><h3>Conclusion</h3><p>So when should we use each?</p><p>All four approaches solve similar underlying problems&#8212;making a critical section effectively atomic&#8212;but they fit best in slightly different situations:</p><ul><li><p><strong>Actors</strong> &#8212; Default choice for new asynchronous Swift code. The compiler enforces actor isolation, eliminating unsynchronized access to actor state. The main thing to watch for is reentrancy: every await inside an actor is a point where assumptions may need to be revalidated.</p></li><li><p><strong>Mutex</strong> &#8212; An excellent choice for protecting synchronous shared state on modern OS releases. By putting the protected value inside the mutex, the compiler helps enforce correct access rather than relying entirely on programmer discipline. Like any lock, though, you still need to define the correct critical section and avoid deadlocks.</p></li><li><p><strong>NSLock</strong> &#8212; A solid option when supporting older operating systems or working in existing Foundation-based code. It&#8217;s lightweight and widely understood, but the relationship between the lock and the data it protects exists only by convention, so correctness depends on consistently applying the locking discipline.</p></li><li><p><strong>Serial Dispatch Queue</strong> &#8212; Best when the problem is naturally expressed as a sequence of ordered work rather than simply protecting shared state. Queues provide both mutual exclusion and scheduling, making them a great fit when the order of operations matters.</p></li></ul><p>From a performance perspective, these primitives all have different costs, but synchronization overhead is rarely the bottleneck in real applications. In most cases, you&#8217;ll get better results by choosing the primitive whose <strong>mental model</strong> best matches your problem than by optimizing for small differences in synchronization performance.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://swiftwright.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Swiftwright! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Body, Identity, and Lifetime in SwiftUI]]></title><description><![CDATA[An exploration of SwiftUI view updates]]></description><link>https://swiftwright.dev/p/body-identity-and-lifetime-in-swiftui</link><guid isPermaLink="false">https://swiftwright.dev/p/body-identity-and-lifetime-in-swiftui</guid><dc:creator><![CDATA[John]]></dc:creator><pubDate>Wed, 03 Jun 2026 23:13:53 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!cSw4!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59075462-3aa3-4185-ab25-4ae020813b48_1254x1254.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In SwiftUI, the phrase &#8220;the view updated&#8221; is overloaded. View updates can involve many things &#8212; body recomputation, structural identity comparison, state lookup, and more. </p><p>We know that SwiftUI views are ephemeral value types that can be reconstructed frequently, but just because the <em>body</em> property is reevaluated does not mean that an entire new view with a different identity and new state is created. These two events are easily conflated since they may both involve the same SwiftUI code being executed again, but they have very different consequences. So let&#8217;s first understand how to distinguish simple changes to a view&#8217;s description from changes to its identity.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://swiftwright.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading jbakerlent! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>One caveat that I feel obligated to add is that SwiftUI's reconciliation rules aren't documented and have shifted across releases. So the description below reflects the current model, but not necessarily a contractual guarantee.</p><h3>View Descriptions</h3><p>A description change is a new answer to &#8220;what should this view look like now?&#8221; A description-level change occurs when SwiftUI reevaluates a view&#8217;s <em>body</em> and produces a new description of the interface. The view still occupies the same structural position in the view tree, but some aspect of its rendered output has changed.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;4ccb1525-0e94-428a-ad0f-c49e4ccd805e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">struct CounterView: View {
    @State private var count = 0

    var body: some View {
        VStack {
            Text("Count: \(count)")

            Button("Increment") {
                count += 1
            }
        }
    }
}</code></pre></div><p>In the above snippet, changes to <em>count</em> cause <em>body</em> to be recomputed, producing a new description of the interface. But SwiftUI can still reconcile that description against the same structural identity, so the state associated with the counter remains intact.</p><h3>View Identity</h3><p>An identity change is a new answer to &#8220;is this still the same conceptual view?&#8221; Identity is closely tied to structure. When the shape of the produced view tree changes &#8212; a conditional branch is taken, an element appears or disappears, a view moves between containers &#8212; SwiftUI must determine how elements in the new tree correspond to elements in the previous tree. Not every change qualifies &#8212; changing a modifier value usually leaves identity intact, for instance &#8212; but when correspondence genuinely breaks, new identities and new lifetimes are established.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:&quot;9fe38ce8-ef66-4713-af28-7b437ea21e2d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">if isHighlighted {
    Text(title)
        .font(.headline)
} else {
    Text(title)
        .font(.body)
}</code></pre></div><p>Here, the change is no longer purely descriptive. Each branch of the conditional has its own identity. When isHighlighted flips, SwiftUI tears down the Text from the true branch and creates the Text from the false branch &#8212; these are two distinct identities, not one view receiving a new description. That said, toggling back and forth doesn&#8217;t spawn endless new identities; it will alternate between two stable ones, each with its own lifetime.</p><p>This example is particularly surprising because both branches are just Text(title) with a different font, so it&#8217;s tempting to expect SwiftUI to recognize them as the same view and simply update the font. It doesn&#8217;t. The if/else is a structural fork, and structure determines identity. Because Text holds no state, the identity change here costs you nothing observable &#8212; but the font change won&#8217;t animate across the branch boundary, and if either branch held a stateful child view, that state would be discarded on every toggle. (You can collapse this into a single identity by changing it to <em>Text(title).font(isHighlighted ? .headline : .body)</em>.)</p><p>For a deeper look at structural identity and how it is represented, see <a href="https://jbakerlent.substack.com/p/how-result-builders-and-opaque-return">this article</a>. </p><h3>State Storage</h3><p>One of the reasons the distinction between identity and description matters is that ordinary let and var properties are stored directly on the View struct and are therefore recreated with the view value. @State is different. Although a @State property is declared inside the view, its storage is managed externally by SwiftUI and tied to the view&#8217;s identity in the view tree.</p><p>As a result, if some upstream event causes CounterView (in the above example) to be recreated, count is not necessarily reinitialized. As long as SwiftUI can reconcile the new description against the same identity, the existing state storage can be looked up and reused.</p><h3>Performance Implications</h3><p>This is why recomputing body is usually less consequential. SwiftUI is designed to repeatedly ask views for new descriptions of the interface. As long as those descriptions can be reconciled with the same identity, the lifetime associated with that identity continues, and SwiftUI can preserve the state attached to it.</p><p>Identity changes are more significant. When a view moves to a different position, is replaced by a different branch, or otherwise no longer corresponds to the same place in the view tree, SwiftUI may establish a new lifetime. At that point, any state associated with the old identity is not associated with the new view.</p><p>In summary, the body property is ephemeral and corresponds to the view&#8217;s description, while state corresponds to its identity and is continuous. All the long-lived pieces are stored elsewhere, which allows SwiftUI to cheaply recompute the body frequently.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://swiftwright.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading jbakerlent! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[How Result Builders and Opaque Return Types Make SwiftUI Possible]]></title><description><![CDATA[The line var body: some View is likely the most-written line in SwiftUI.]]></description><link>https://swiftwright.dev/p/how-result-builders-and-opaque-return</link><guid isPermaLink="false">https://swiftwright.dev/p/how-result-builders-and-opaque-return</guid><dc:creator><![CDATA[John]]></dc:creator><pubDate>Sat, 16 May 2026 23:02:06 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!cSw4!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59075462-3aa3-4185-ab25-4ae020813b48_1254x1254.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The line <em>var body: some View</em> is likely the most-written line in SwiftUI. We have a sense of the purpose it serves, but likely don&#8217;t stop to examine the Swift language features that make it possible. Examining the usage of <strong>result builders</strong> and <strong>opaque return types</strong> in detail helps illuminate SwiftUI's identity and diffing behavior, and gives a clearer understanding of those language features themselves.</p><h4>Structural Identity in SwiftUI</h4><p>Because views are values and not objects &#8212; views are descriptions of UI, not instances of it &#8212; SwiftUI needs to be able to efficiently compare them to determine when to update the view tree. Unlike frameworks such as React, which primarily reconcile UI structure at runtime using a virtual DOM, SwiftUI pushes much more structural information into the type system at compile time. Achieving this requires a mechanism capable of assigning concrete static types to deeply nested hierarchies containing different view types and control flow such as <code>if</code> statements.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://swiftwright.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading jbakerlent! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>SwiftUI heavily relies on structural identity &#8212; the position and static type information of views in the hierarchy &#8212; to determine how updates should be applied efficiently. This means that erasing the type to something like<code> </code><em>AnyView</em> removes structural type information SwiftUI could otherwise use during diffing and update propagation. It would also force SwiftUI to rely more heavily on dynamic dispatch.</p><h4>Encoding View Hierarchies with <code>@ViewBuilder</code></h4><p>SwiftUI&#8217;s solution for encoding the structure of a view is to use a result builder. The content closure of each SwiftUI view is implicitly annotated with one: @ViewBuilder. It transforms the body into a concrete nested generic type at compile time. Let&#8217;s look at a simple view definition as an example.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">struct ExampleView: View {
    var showImage: Bool

    var body: some View {
        HStack {
            Text("Hello")
            if showImage {
                Image(systemName: "star")
            } else {
                Text("World")
            }
            Button("Tap me") { }
        }
    }
}</code></pre></div><p>The compiler relies on @ViewBuilder to desugar it to look roughly like this.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">ViewBuilder.buildBlock(
    Text("Hello"),
    ViewBuilder.buildEither(
        first: Image(systemName: "star")
    ),
    Button("Tap me") { }
)</code></pre></div><p>The <em>else</em> branch would similarly use <em>buildEither(second:)</em>. </p><p>That yields this fully specified type.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;swift&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-swift">HStack&lt;TupleView&lt;(Text, _ConditionalContent&lt;Image, Text&gt;, Button&lt;Text&gt;)&gt;&gt;</code></pre></div><p>You can see how beautifully this fits into SwiftUI&#8217;s declarative style; the type itself is a compile-time map of the hierarchy &#8212; every branch of control flow and every container is encoded structurally.</p><h4>Hiding Complexity with Opaque Return Types</h4><p>This is only half of the solution, though. If every view were exposed as a highly nested generic type, every place a view was referenced would need to include its fully specified type and cascading updates would be needed for simple changes to a single view in a hierarchy. This would create a very brittle structure. Swift addresses this with another language feature: opaque return types. Opaque return types are sometimes described as the opposite of generics because instead of the caller choosing the type, the implementation chooses it. This lets the type be hidden to callers &#8212; hence the word &#8220;opaque&#8221;. The compiler knows the full concrete type, while callers only know that it conforms to the <code>View</code> protocol.</p><h4>The Takeaway</h4><p>SwiftUI&#8217;s design works because these features complement each other perfectly: <em>@ViewBuilder</em> preserves the complete structural shape of the hierarchy for the framework and compiler, while <em>some View</em> hides that complexity from API boundaries. The result is a system that remains declarative and ergonomic while still retaining compile-time knowledge about the UI tree. It&#8217;s impressive how much complexity this design shifts into the compiler and type system. Understanding that flow gives a much clearer picture of SwiftUI&#8217;s architecture and many of its behavioral nuances.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://swiftwright.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading jbakerlent! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[The Fork-Join Pattern in Swift Concurrency]]></title><description><![CDATA[Swift Concurrency gives us two APIs for running work in parallel: async let and TaskGroup. They look quite different at first, but they're both expressions of the same underlying pattern, and I find that understanding that pattern makes the relationship between them much clearer.]]></description><link>https://swiftwright.dev/p/the-fork-join-pattern-in-swift-concurrency</link><guid isPermaLink="false">https://swiftwright.dev/p/the-fork-join-pattern-in-swift-concurrency</guid><dc:creator><![CDATA[John]]></dc:creator><pubDate>Thu, 07 May 2026 20:47:36 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!cSw4!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59075462-3aa3-4185-ab25-4ae020813b48_1254x1254.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Swift Concurrency gives us two APIs for running work in parallel: <em>async let</em> and <em>TaskGroup</em>. They look quite different at first, but they're both expressions of the same underlying pattern, and I find that understanding that pattern makes the relationship between them much clearer.</p><h3>Concurrency vs. parallelism</h3><p>First, a useful starting distinction around terms (from Rob Pike): <em>concurrency</em> is about structure, <em>parallelism</em> is about execution.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://swiftwright.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading jbakerlent! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p><em>Concurrency</em> is a way of structuring a program so multiple logically independent operations can make progress independently &#8212; whether interleaved on one core or executing simultaneously on many.</p><p>On the other hand, <em>parallelism</em> is multiple operations physically executing at the same instant on separate hardware (multiple cores, GPUs). It&#8217;s a property of how the program runs.</p><h3>The trouble with callbacks and promises</h3><p>With those definitions in hand: the question for any concurrent language is <em>how</em> it lets you express that structure. Most languages give you callbacks or futures and promises. In their raw form, callbacks and promises tend to behave like a kind of cross-thread goto: control flow escapes lexical structure, and lifetimes become difficult to reason about locally. You may be familiar with goto's downfall with the advent of structured programming, thanks to some opinion pieces by Dijkstra (pronounced Deek-strah, by the way). This is, not coincidentally, where the "Structured" in Structured Concurrency comes from &#8212; it's the same idea of respecting lexical scope applied to a new domain and helps simplify a lot of the pain points of GCD.</p><p>If you're curious to explore this more, <a href="https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/">this article</a> is a great walkthrough, particularly the breakdown of how each of these are semantically equivalent to <em>go</em> statements:</p><blockquote><ul><li><p>Registering a callback is semantically equivalent to starting a background thread that (a) blocks until some event occurs, and then (b) runs the callback. (Though obviously the implementation is different.) So in terms of high-level control flow, registering a callback is essentially a go statement.</p></li><li><p>Futures and promises are the same too: when you call a function and it returns a promise, that means it&#8217;s scheduled the work to happen in the background, and then given you a handle object to join the work later (if you want). In terms of control flow semantics, this is just like spawning a thread. Then you register callbacks on the promise, so see the previous bullet point.</p></li></ul></blockquote><h3>The fork-join pattern</h3><p>Swift's structured concurrency takes a different approach: a pattern called <strong>fork-join</strong>. The idea is simple &#8212; multiple operations start running concurrently (the <em>fork</em>), and then their results are combined once they all complete (the <em>join</em>). <em>async let</em> and <em>TaskGroup</em> are the two APIs that implement this pattern, and they differ along one key axis: whether the number of operations you want to fork is known at compile time (<em>static width</em>) or determined at runtime (<em>dynamic width</em>).</p><h4><code>async let</code> &#8212; static width</h4><p><em>async let</em> is fork-join for a fixed, known number of operations. If you have a predefined number of operations that always need to be performed together, you simply declare them one after the other. Each <em>async let</em> declaration is itself a fork point &#8212; the right-hand side begins executing as a child task the moment you write <em>async let</em>. The join happens when you <em>await</em> the binding:</p><p>swift</p><pre><code><code>func loadProfile() async -&gt; Profile {
    async let user = fetchUser()        // fork
    async let posts = fetchPosts()      // fork
    async let avatar = fetchAvatar()    // fork

    return await Profile(               // join
        user: user,
        posts: posts,
        avatar: avatar
    )
}</code></code></pre><p>All three fetches are in flight by the time we reach the <em>return</em>. The <em>await</em> is the join site &#8212; it&#8217;s where we wait for the children to finish and collect their results. The width here is static: there are exactly three child tasks, hardcoded into the source. You couldn&#8217;t write this with a variable number of fetches without restructuring.</p><h4><code>TaskGroup</code> &#8212; dynamic width</h4><p><em>TaskGroup</em> is fork-join for a runtime-determined number of operations. When the count or the work depends on values you only have at runtime, you open a group and add a child task for each unit of work you want to run. Each <em>addTask</em> call is a fork; the join occurs as you iterate over the group's results:</p><p>swift</p><pre><code><code>func loadPosts(for ids: [UUID]) async -&gt; [Post] {
    await withTaskGroup(of: Post.self) { group in
        for id in ids {
            group.addTask { await fetchPost(id: id) }   // fork
        }

        var posts: [Post] = []
        for await post in group {                       // join
            posts.append(post)
        }
        return posts
    }
}</code></code></pre><p>The shape is the same &#8212; fork some children, join their results &#8212; but the count is determined by <em>ids.count</em> at runtime rather than baked into the source. If <em>ids</em> has three elements you get three child tasks; if it has three hundred, you get three hundred.</p><h3><code>Conclusion</code></h3><p>The static/dynamic distinction is usually the deciding factor on which to use. If you know at the call site exactly which operations you want to run concurrently, use <em>async let</em> &#8212; each child gets its own typed binding, and you read the results back by name. If the count or the work depends on runtime data, use <em>TaskGroup</em>, at the cost of homogeneous child result types and a bit more setup boilerplate.</p><p>Conceptually, though, they&#8217;re much closer than they first appear. Both APIs implement the same fork-join structure: child tasks are forked from a parent task, execute concurrently within that parent&#8217;s scope, and are joined before the scope completes. The difference is not in the underlying concurrency model, but in how the number of forks is expressed. Either way, you&#8217;re expressing the same idea: fork some work to run concurrently, then join the results when you need them.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://swiftwright.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading jbakerlent! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Why Async/Await Isn’t Just Prettier GCD]]></title><description><![CDATA[Continuations]]></description><link>https://swiftwright.dev/p/why-asyncawait-isnt-just-prettier</link><guid isPermaLink="false">https://swiftwright.dev/p/why-asyncawait-isnt-just-prettier</guid><dc:creator><![CDATA[John]]></dc:creator><pubDate>Sat, 02 May 2026 23:57:23 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!cSw4!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F59075462-3aa3-4185-ab25-4ae020813b48_1254x1254.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When writing asynchronous code using async/await, the most obvious improvements are syntactical ones &#8212; being able to read the code top-to-bottom and see the execution order, not dealing with masses of completion handlers or complex error handling, etc.</p><p>But the benefits go much deeper on the performance side.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://swiftwright.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading John's Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><h3>The GCD Model</h3><p>When GCD was introduced way back with Snow Leopard in 2009, thread pools were the right answer (or so I&#8217;m told haha). When a thread is busy or blocked, GCD simply spawns another thread to pick up later work.</p><p>In practice, this leads to two significant problems: an explosion in the number of active threads and a related slowdown from having to hop between them. Remember that devices have a fixed number of actual CPU cores &#8212; 6 on recent iPhones &#8212; so having to time slice between, say, 20 threads to ensure they can all progress doesn&#8217;t give you the performance gain you might imagine.</p><p>Each time execution shifts between threads, it must perform a <strong>context switch</strong>, which is a kernel-level operation involving saving all of the current thread&#8217;s state and loading and resuming the new thread. On top of this, pulling in the new state pollutes the CPU&#8217;s cache (L1, L2), causing further slowdowns by forcing values to be fetched from slower locations.</p><h3>Enter Continuations</h3><p>With async/await (and Swift Concurrency more broadly) these problems are avoided by adopting some <a href="https://gist.github.com/lattner/429b9070918248274f25b714dcfc7619">prior art from C#</a> (among other sources).</p><p>Instead of spawning an unbounded number of new threads, the runtime uses a fixed pool of threads roughly equal to the number of CPU cores. And to avoid the slowdown from context switches, it uses a mechanism known as a <strong>continuation</strong>.</p><p>Continuations are essentially portable stack frames &#8212; a snapshot of the function&#8217;s state and local variables that can be paused and resumed elsewhere. Instead of blocking a thread (which forces a context switch to keep work flowing), the function suspends by saving its state into a continuation and simply returns. The thread is immediately free to run other work &#8212; no kernel involvement, no context switch, no cache wipe. As threads are freed up, they can cheaply pick up any available continuations and continue execution.</p><h3>A Restaurant Analogy</h3><p>Hopefully this helps visualize what we&#8217;ve been talking about.</p><p><strong>GCD:</strong> If a waiter (thread) is waiting for food (an API call), they just stand at the window doing nothing. If more orders come in, the manager hires more waiters (thread explosion). Soon the kitchen is too crowded to move (context switching).</p><p><strong>Async/Await:</strong> When the food isn&#8217;t ready, the waiter puts a &#8220;post-it note&#8221; (continuation) on the counter and goes to help another table. The manager never hires more waiters than there are tables.</p><h3>The Takeaway</h3><p>Async/await isn&#8217;t just nicer syntax than GCD &#8212; it&#8217;s a fundamentally different concurrency primitive. The readability is a bonus.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://swiftwright.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading John's Substack! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item></channel></rss>