<?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>Fri, 06 Nov 2026 07:15:09 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[Code Review in the Age of AI-Generated Code]]></title><description><![CDATA[The Review Bottleneck]]></description><link>https://swiftwright.dev/p/code-review-in-the-age-of-ai-generated</link><guid isPermaLink="false">https://swiftwright.dev/p/code-review-in-the-age-of-ai-generated</guid><dc:creator><![CDATA[John]]></dc:creator><pubDate>Sun, 27 Sep 2026 03:58:28 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[<h2>The Review Bottleneck</h2><p>The situation around agent-assisted software development reminds me of the passage in Code Complete where McConnell discusses the importance of being aware of your location in a given &#8220;technology wave.&#8221; We are still at the early edge of this AI-driven wave, and we are experiencing many of its symptoms: rapidly changing tools, new workflows without settled conventions, and engineers spending time figuring out the tooling rather than simply using it.</p><p>A lot has changed in the last year or so. We have all seen the surge in plausible-looking, agent-generated code. Pull requests have grown much larger and arrive much faster. A 2,000-line change can now be put up for review after a few hours. On top of that, polish is no longer a proxy for code quality. The problems in code are not typos, but unnecessary abstractions or mistaken assumptions made early in the design. The PR author may not have read every line they are putting up for review, never mind carefully reasoning through it as they would hand-written code. The reviewer may be the first human to read the code closely.</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><p>Unfortunately, code review has not evolved at the same pace as code generation. Human review is becoming a bottleneck&#8212;but that gives us an opportunity to reconsider what code review is actually for.</p><p>Even prior to agents, I found a disconnect between what engineers thought the purpose of code review was and what actually ended up being valuable. Teams expect reviews to be about finding defects, but they really serve to facilitate knowledge transfer and allow alternative solutions to be considered (Bacchelli and Bird, 2013). In that context, maybe we are automating the smaller benefit and risking the larger ones. Automated review may already be more effective at catching local or mechanical issues than a hurried human, but it quietly eats away at learning until knowledge transfer goes to zero. After all, if we see a green check on a PR, do we really expect to consistently dedicate time to deeply understand it at the expense of our own work?</p><h2>What Human Review Is For</h2><blockquote><p>The question we ought to ask ourselves in the middle of all this development acceleration is how do we reimagine many things we took for granted&#8230; If you ask me, the way we do code review today makes less and less sense, and the same is true for pull requests. &#8212; Venkat Venkataramani, VP @ OpenAI</p></blockquote><p>The software development lifecycle is evolving so quickly that predicting its eventual shape is probably ill-advised. But there is no question that PR review needs a redesign. My sense is that much of today&#8217;s review process will eventually become infrastructure, much as type systems and CI have offloaded work. I can see engineers being a different kind of QA, one step up stream. Like QA, we are increasingly reviewing work done by another party to ensure it meets certain criteria. In the current environment, and possibly for many years, there are a few principles we can keep in mind.</p><p>Below I&#8217;ll outline some things that I believe matter in human review including ownership, conceptual integrity, and learning, as well as ideas for structuring the changes so that a person can realistically achieve these goals. To start here are some questions I keep in mind as I review code changes.</p><h4>Does this duplicate an existing abstraction?</h4><p>A human who has worked in the codebase for years may recognize where an abstraction already exists. With heavily agent-generated code, that is less likely to be caught. Agent-generated PRs show almost twice the semantic redundancy (the same logic expressed through different code) as human-authored ones (Huang et al., 2026). Reviewers are also more lenient and less likely to question this redundancy. Duplication is one of the cardinal sins in programming, so it&#8217;s easy to see how this bloat could lead to issues over time.</p><blockquote><p>If you want a system to have conceptual integrity, then somebody has to be looking at the whole system. &#8212; Martin Fowler</p></blockquote><h4>Is someone (ideally the author) able to defend the changes?</h4><p>Agents aren&#8217;t yet on the hook for responding to incidents, so we as programmers must be able to debug the changes. Using an agent changes who typed the code, not who owns it. Saying &#8220;the agent wrote it&#8221; in a postmortem means we delegated too much. If we aren&#8217;t confident that we understand the changes, they should not be merged. Questions raised during review may be evidence that a change is not yet understandable enough to merge, so they are important to raise.</p><h4>Does the change fit well in the codebase?</h4><p>If we take a rubber-stamp approach to code review, we forfeit the ability to control the conceptual integrity of the codebase. Agents are great at producing many independent and uncoordinated changes. Without human oversight, the codebase can quickly devolve into a patchwork of duplicative and incoherent code. Code review is one of the places where we can course-correct individual changes and keep the system&#8217;s design intentional.</p><blockquote><p>I will contend that conceptual integrity is the most important consideration in system design. &#8212; Fred Brooks, The Mythical Man-Month</p></blockquote><h4>What can the team learn from this change?</h4><p>The reasoning behind a design is often missing from the final diff. These choices are especially important for junior engineers and new hires to learn from. When engineers are handed only the final output, they have no way to learn how to evaluate tradeoffs and grow their expertise. Deliberately discussing those tradeoffs also prevents others from having to repeat the same decision-making process later.</p><h4>Are the tests sufficient?</h4><p>If we are reading and writing less of the code, we need high-quality tests to mitigate the risks. Tests have always been the safety net that lets us make changes without breaking things, and at the scale code can now be changed, they&#8217;re even more important. Thankfully, tests are also much easier to write with AI. We should review them as carefully as the production code to make sure the agent isn&#8217;t simply maximizing coverage. I still find some tests that feel &#8220;tautological&#8221;&#8212;they simply assert the implementation itself rather than verifying any meaningful behavior (e.g. a test that checks that a constant defined in a method has the value it was assigned).  Another common failure is heavily mocked tests that are unable to exercise any real behavior.</p><blockquote><p>With tests, we can change the behavior of our code quickly and verifiably. Without them, we really don&#8217;t know if our code is getting better or worse. &#8212; Michael Feathers, Working Effectively with Legacy Code</p></blockquote><h2>Sizing and Scoping Changes</h2><p>Of course, none of this works if the change is too large to understand.</p><h4>How can we reduce the size of changes?</h4><p>Research conducted well before the current generation of coding agents found that defect detection declined as reviews grew beyond roughly 400 lines (Cohen, Best Kept Secrets of Peer Code Review, SmartBear/Cisco). We&#8217;re now asking more reviewers to do more than just scan for bugs, so I think 400 lines is still a good threshold to aim for. I exclude whitespace, generated files, comments, and tests from this informal count.</p><p>But splitting large features into reviewable chunks has always been a challenge. Feature work doesn&#8217;t get done in clean, independently reviewable pieces. Agents actually can help with this problem. We can continue iterating until the work is functionally ready in whatever process we prefer, then ask an agent to help organize it into manageable chunks instead of publishing one large PR with a messy history. There are a few ways to do this.</p><h4>Atomic Commits</h4><p>Once the work is done, ask the agent to rewrite the commit history on the feature branch (before anyone has pulled and built off of it) so that each commit focuses on one aspect of the change. For instance, model or API changes could be in one commit, UI behavior in another, and tests alongside the behavior they verify. These can be put up together so the reviewer sees the whole picture while retaining the ability to view each commit in isolation. This optimizes for comprehension by preventing the reviewer from having to scroll back and forth in the code review tool past unrelated changes. It also requires very little effort from the author and lets them keep their own workflow, with just a simple cleanup step added.</p><p>The end result is that the commits are grouped conceptually rather than chronologically. Instead of showing what order the changes were made in, they are organized into coherent sets that better explain what is being changed.</p><h4>Stacked Diffs</h4><p>If your team or workflow fits better with decomposing changes so that each can actually be reviewed and merged independently, then stacks are an alternative approach. Companies like Uber and Meta have used these for years. GitHub added its own <code>gh stack</code> tooling on July 2026. There are many other offerings available such as Graphite. They all help with managing the branches, but more importantly also with all the relationships between them, rebasing/restacking, and merging. <a href="https://graphite.com/docs/cli-quick-start?utm_source=chatgpt.com">This guide</a> has a good overview of how these work.</p><h4>Independent PRs</h4><p>If you can divide a change into entirely independent sets of work that can land in isolation, that can often be the best approach. This may involve making preparatory PRs that refactor existing code to be open to extension (the &#8220;O&#8221; in SOLID), followed by architectural changes and then the behavioral change itself. The final PR can contain the interesting, unique functionality while the earlier PRs contain the more mechanical changes.</p><h2>Offloading Cognitive Load</h2><p>There are also steps beyond organizing the change sets themselves that we can take to help out our coworkers as they try to get a handle on changes we put up for review. The goal should be to reduce the amount of context that the reviewer has to mentally reconstruct in order to understand the change.</p><h4>Automate the First Pass</h4><p>Having a standardized team protocol that is followed before asking for human review is helpful for keeping routine issues out of the way. I recommend using a shared set of role-specific agents focused on specific tracks such as concurrency and testing to catch mistakes a general-purpose coding agent may overlook. Paul Hudson hosts some nice ones <a href="https://github.com/twostraws/Swift-Agent-Skills">here</a>. If you have access to multiple families of models for reviews, differences in their training data can mean they find slightly different problems too. I like having all of this run via a single pre-review skill that can be invoked just before pushing changes. Agent review pipelines are a topic in their own right that I will cover in a future post, but sharing a process across the team helps raise the bar for the code and makes our reviews more efficient.</p><h4>Give Reviewers a Map</h4><p>PR descriptions can add a lot of value for readers. One of my goals is to transfer the context that I gathered through iterating on the implementation so that each reviewer picking it up from scratch doesn&#8217;t have to struggle to recreate it (or worse, skip trying).  I like to include a reading guide that explains which files matter, which are mechanical piping, how they fit together, and what order to read them in. Any additional narration of the mental model behind the change can only help the reviewers learn. This shouldn&#8217;t just be more generated text, but a short, thoughtful, ideally human-written or edited introduction.</p><p>Including other modalities like images and diagrams can also be extremely effective. For UI changes, before-and-after screenshots or recordings communicate the high-level intent immediately, making the details easier to understand. For non-UI changes, simple Mermaid diagrams can be very helpful and can generally be rendered directly. For those unfamiliar, Mermaid is essentially Markdown for diagrams. I like adding flowcharts or decision tables showing how behavior changes across scenarios (e.g. to model online and offline states, errors, or retries). They are almost always much easier to understand than nested conditionals or paragraphs of commentary.</p><h3>In Closing</h3><p>AI has made it much cheaper to produce code. It has not made it cheaper to understand a system, preserve its conceptual integrity, or take responsibility when something goes wrong. Who knows whether that will continue to be true forever, but for now the goal is still more useful software&#8212;not merely more code.</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[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 buttonBase-GK1x3M"><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" class="icon-noB79L"><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 buttonBase-GK1x3M"><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 icon-noB79L"><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">6 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">6 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>