<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mike Salari</title>
    <description>The latest articles on DEV Community by Mike Salari (@salari_dev).</description>
    <link>https://dev.to/salari_dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4100087%2F0ab13e53-fc98-4f69-823a-51d3a60ad5a2.png</url>
      <title>DEV Community: Mike Salari</title>
      <link>https://dev.to/salari_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/salari_dev"/>
    <language>en</language>
    <item>
      <title>Your SwiftUI app works. Production is where it starts lying to you.</title>
      <dc:creator>Mike Salari</dc:creator>
      <pubDate>Tue, 27 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/salari_dev/your-swiftui-app-works-production-is-where-it-starts-lying-to-you-2j55</link>
      <guid>https://dev.to/salari_dev/your-swiftui-app-works-production-is-where-it-starts-lying-to-you-2j55</guid>
      <description>&lt;p&gt;A SwiftUI screen can be completely correct in development and still have the wrong production model.&lt;/p&gt;

&lt;p&gt;The request returned. The list rendered. The state changed when it was supposed to change. Everything worked, on the simulator, with fixture data, with one person tapping in a predictable order. Then real timing arrived, then real data, then real users, and the screen began reporting things that were no longer true.&lt;/p&gt;

&lt;p&gt;None of the failures below start with a missing SwiftUI modifier. They are not the kind of bug a component reference can catch, because the UI already works when they appear. They show up when the system underneath the view has to answer questions the happy path never asked: which result still owns the screen, where the expensive work actually happens, and who owns this state and for how long.&lt;/p&gt;

&lt;p&gt;Three cases. Each is small enough to reproduce in an afternoon and common enough that you have probably shipped at least one of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The latest request is not necessarily the latest result
&lt;/h2&gt;

&lt;p&gt;Take a search field backed by an async API. Every keystroke calls &lt;code&gt;search&lt;/code&gt;, every call starts a task, and every task writes its results when it finishes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;@MainActor&lt;/span&gt;
&lt;span class="kd"&gt;@Observable&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;SearchModel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;SearchResult&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;SearchAPI&lt;/span&gt;

    &lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;SearchAPI&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;Task&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// Real failure policy goes here.&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user types &lt;code&gt;swift&lt;/code&gt;, then a moment later &lt;code&gt;swiftui&lt;/code&gt;. Two requests are in flight. The second one hits a warm cache, or gets a faster connection, or simply has less to return, and it completes first. The screen shows results for &lt;code&gt;swiftui&lt;/code&gt;. Then the older request for &lt;code&gt;swift&lt;/code&gt; completes and overwrites them. The user is now looking at results for a query they are no longer asking.&lt;/p&gt;

&lt;p&gt;Nothing in this sequence failed. Both requests succeeded. Both writes to &lt;code&gt;results&lt;/code&gt; happened on the main actor, one at a time, so there is no data race for the compiler or the sanitizer to catch. Every mutation was legal. The UI is still wrong.&lt;/p&gt;

&lt;p&gt;It is tempting to file this under concurrency and reach for cancellation. Cancellation helps, but it is cooperative: a request that has already returned cannot be un-returned, and a task that checks for cancellation late will still publish. &lt;code&gt;async/await&lt;/code&gt; orders the steps inside one task. It says nothing about which of several completed tasks still has the right to publish state. That is a question about intent, and only the model can answer it, because only the model knows what the user asked for most recently.&lt;/p&gt;

&lt;p&gt;The smallest correction that proves this diagnosis is to give each request an identity and let only the newest one write.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;@MainActor&lt;/span&gt;
&lt;span class="kd"&gt;@Observable&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;SearchModel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;SearchResult&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;SearchAPI&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;generation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;SearchAPI&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;generation&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;requestGeneration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;generation&lt;/span&gt;

        &lt;span class="kt"&gt;Task&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="n"&gt;requestGeneration&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;generation&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;

                &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// Real failure policy goes here.&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;generation&lt;/code&gt; is incremented synchronously, on the main actor, before the task starts, so the comparison after the &lt;code&gt;await&lt;/code&gt; is against whatever value the newest call left behind. A stale result is not an error. It is a result that has lost the right to be shown, and it is dropped. If the API supports it, cancelling the previous task on top of this saves work, but the guard is what makes the screen correct. Cancellation is an optimisation of it, not a substitute for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Request lifetime and user intent are not the same thing.&lt;/strong&gt; A request lives until it returns. The user's intent lives until the next keystroke. The state has to follow the intent.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. "It is lazy" is not "it is fast"
&lt;/h2&gt;

&lt;p&gt;The second failure arrives with scale, and it usually arrives with a sentence: it is fine, the list is lazy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kt"&gt;ScrollView&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;LazyVStack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;ForEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="kt"&gt;ItemRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is correct, and for a few hundred items it is the whole story. &lt;code&gt;LazyVStack&lt;/code&gt; constructs its children on demand, so rows far below the fold cost nothing until they scroll in. Then the feature grows. &lt;code&gt;items&lt;/code&gt; becomes a feed of 10,000 rows with a search field above it, and every keystroke freezes the screen for long enough that the keyboard visibly stalls. The instinct is to blame SwiftUI, or to blame the list, and to start swapping containers.&lt;/p&gt;

&lt;p&gt;The container is not where the time goes. The lazy container answers exactly one question: which rows need to exist right now. It has no opinion about what &lt;code&gt;items&lt;/code&gt; is or how it was produced. In the typical version of this screen, a keystroke normalises the query, filters all 10,000 items against it, sorts the survivors, groups them into sections and builds a fresh array. Because the array is new, SwiftUI then has to work out which rows changed, and if the ids are unstable the answer is all of them. Rows that survived are reconstructed, each row looks up an image, some of those images are decoded and resized on demand, and layout runs for whatever is visible. All of that happens before laziness is even consulted, and most of it happens on the main actor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lazy container solved the one problem it solves. It did not make the pipeline lazy.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It helps to see the whole route, because the freeze can live in any stage of it.&lt;/p&gt;

&lt;p&gt;Data to pixels&lt;/p&gt;

&lt;p&gt;1.&lt;/p&gt;

&lt;p&gt;User input&lt;/p&gt;

&lt;p&gt;keystroke, scroll, refresh&lt;br&gt;
2.&lt;/p&gt;

&lt;p&gt;Normalization&lt;/p&gt;

&lt;p&gt;trim, lowercase, diacritics&lt;br&gt;
3.&lt;/p&gt;

&lt;p&gt;Filtering&lt;/p&gt;

&lt;p&gt;every row, every keystroke&lt;br&gt;
4.&lt;/p&gt;

&lt;p&gt;Sorting / grouping&lt;br&gt;
5.&lt;/p&gt;

&lt;p&gt;Snapshot&lt;/p&gt;

&lt;p&gt;the array the view sees&lt;br&gt;
6.&lt;/p&gt;

&lt;p&gt;Image lookup&lt;/p&gt;

&lt;p&gt;cache hit or miss&lt;br&gt;
7.&lt;/p&gt;

&lt;p&gt;Decode / resize&lt;/p&gt;

&lt;p&gt;full-size image on the main thread&lt;br&gt;
8.&lt;/p&gt;

&lt;p&gt;Row construction&lt;br&gt;
9.&lt;/p&gt;

&lt;p&gt;Layout&lt;br&gt;
10.&lt;/p&gt;

&lt;p&gt;Pixels&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A large SwiftUI list is not merely a view hierarchy. It is a data-to-pixels pipeline.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once it is drawn this way, the question changes. Not "is SwiftUI slow", but "which stage is slow, and why is it running on this keystroke at all". Every stage has its own fix and its own evidence. Filtering and sorting can move off the main actor and sit behind a request generation, the same mechanism as the first case, so that a keystroke never publishes a snapshot the user has already typed past. Snapshots can keep stable identity, so a new array does not read as 10,000 replacements. Image decoding can leave the row and be cached at display size. Invalidation can be narrowed, so a change in one row does not rebuild the section around it. None of these is the answer until measurement says which stage is the bottleneck.&lt;/p&gt;

&lt;p&gt;That evidence comes from tools, not from intuition. A Time Profiler trace recorded while typing shows where the main actor spends each keystroke. Instruments' SwiftUI template shows whether view bodies are being evaluated broadly or narrowly, and a counter in a body will tell you the same thing more crudely. Allocation and memory traces show whether images are being decoded at scroll time and at what size. Row identity is worth checking by hand: if the model's &lt;code&gt;id&lt;/code&gt; changes when the content does not, every diff is a replacement. There are deliberately no timings in this article. They depend on the device, the dataset and the row, and the point of the exercise is to produce your own.&lt;/p&gt;

&lt;p&gt;The lesson is a discipline rather than a technique: do not diagnose "SwiftUI is slow" before you have found the slowest stage. The framework is rarely that stage. The pipeline built in front of it usually is.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Your state model worked until everybody needed it
&lt;/h2&gt;

&lt;p&gt;The third failure is the slowest to appear and the hardest to name, because nothing crashes and nothing measurably stalls. It is what happens to a state model that was designed for one screen and then adopted by the whole app.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;@Observable&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;AppModel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;User&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;notifications&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;Notification&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;searchResults&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;SearchResult&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a reasonable starting point, and it is worth being precise about what is and is not wrong with it. It is not a rendering problem. Observation tracks property access, so a view that reads &lt;code&gt;feed&lt;/code&gt; is not re-evaluated when &lt;code&gt;notifications&lt;/code&gt; changes. The claim that one large &lt;code&gt;@Observable&lt;/code&gt; model redraws every view on every change is not true, and if you split this model for that reason you will spend the effort and keep the real problem.&lt;/p&gt;

&lt;p&gt;The real problem is ownership. Ask the questions the type does not answer. Who owns &lt;code&gt;user&lt;/code&gt;, and what happens to &lt;code&gt;feed&lt;/code&gt; and &lt;code&gt;searchResults&lt;/code&gt; when the user signs out? How long does &lt;code&gt;searchResults&lt;/code&gt; live: as long as the search screen, or as long as the app? Which async work belongs to this object, and who cancels it? If notifications are removed as a feature next quarter, what else in this class disappears with them? Can the feed logic be tested without constructing a user, a settings object and a search? Every property here has a different answer, and the type has flattened them into one.&lt;/p&gt;

&lt;p&gt;Different answers are the reason to draw boundaries. A &lt;code&gt;SessionState&lt;/code&gt; that owns the user and the sign-out sequence. A &lt;code&gt;FeedState&lt;/code&gt; that lives while the feed is on screen and owns its own pagination and refresh tasks. A &lt;code&gt;SearchState&lt;/code&gt; that owns the request generation from the first case and nothing else. A &lt;code&gt;SettingsState&lt;/code&gt; that persists across sessions. These are not four models because four is a better number than one. They are four because they have four different lifetimes, four different mutation policies and four different sets of async work to cancel.&lt;/p&gt;

&lt;p&gt;Splitting is not automatically better. A model with three properties that always change together, live for the same time and are mutated from the same place should stay one model; splitting it adds plumbing and nothing else. The test is not size. It is whether the parts give different answers to the ownership questions. When they do, the single model is hiding a design decision that will be made anyway, later, by whoever has to add the next feature under a deadline.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;SwiftUI architecture is often state ownership disguised as view architecture.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  These are not SwiftUI syntax problems
&lt;/h2&gt;

&lt;p&gt;Put the three cases next to each other and the pattern is the same. The happy path worked. The failure appeared when the system had to answer a question the happy path never asked: which result still owns the UI, where the expensive work actually happens, and who owns this state and for how long. In each case the correction was not a modifier. It was a decision about ownership, made explicit in code.&lt;/p&gt;

&lt;p&gt;The way through them is the same as well. Reproduce the failure first, in the smallest project that still shows it, because a failure you cannot trigger on demand is one you cannot prove fixed. Then find the mechanism, which means naming the exact step that produces the wrong state rather than the general area: a stale write after an &lt;code&gt;await&lt;/code&gt;, a full sort on the main actor per keystroke, a lifetime that outlives its screen. Apply the smallest correction that proves the diagnosis, a generation guard, a moved filter, a split model. If the failure disappears, the diagnosis was right; if it does not, you have learned that before building anything on top of it. Only then design the boundary that prevents the whole class of failure rather than this one instance, and prove the correction with something you can run again, a test, a trace or a counter, so the next change cannot quietly bring the failure back.&lt;/p&gt;

&lt;h2&gt;
  
  
  SwiftUI gets harder after you know SwiftUI
&lt;/h2&gt;

&lt;p&gt;Early SwiftUI is about getting the screen to work: the layout, the binding, the modifier that does what you meant. That stage ends, and it ends sooner than it feels like it should. What comes after is production: real latency, real data volumes, real images, state that has to live for a specific time and then go away, cancellation that is cooperative rather than immediate, migrations away from the code that was there before, and real users who do things in an order nobody sketched.&lt;/p&gt;

&lt;p&gt;At that stage, building the view is no longer the hard part. Keeping the system underneath it truthful is.&lt;/p&gt;

&lt;p&gt;That is the stage I wrote SwiftUI Under Load for. I did not want another component reference or another collection of isolated SwiftUI recipes. The material starts with failures you can reproduce, then works through the mechanism, the smallest correction, the design that holds, and the evidence that proves it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;First published on &lt;a href="https://salari.dev/writing/swiftui-production-failures/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=swiftui-production-failures" rel="noopener noreferrer"&gt;salari.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>swiftui</category>
      <category>ios</category>
      <category>swift</category>
      <category>mobile</category>
    </item>
    <item>
      <title>You cancelled the caller. Why is the shared Task still running?</title>
      <dc:creator>Mike Salari</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/salari_dev/you-cancelled-the-caller-why-is-the-shared-task-still-running-3m2k</link>
      <guid>https://dev.to/salari_dev/you-cancelled-the-caller-why-is-the-shared-task-still-running-3m2k</guid>
      <description>&lt;p&gt;Here is a Swift concurrency question that looks easier than it is.&lt;/p&gt;

&lt;p&gt;A caller is waiting for the value of a shared &lt;code&gt;Task&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The caller gets cancelled.&lt;/p&gt;

&lt;p&gt;What happens next?&lt;/p&gt;

&lt;p&gt;A very tempting answer is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;await&lt;/code&gt; throws &lt;code&gt;CancellationError&lt;/code&gt;, so the caller leaves immediately.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is not what the shared-task case guarantees.&lt;/p&gt;

&lt;p&gt;If the task you are awaiting is a separate unstructured &lt;code&gt;Task&lt;/code&gt;, cancelling the caller does not automatically cancel that task. Accessing the task's &lt;code&gt;value&lt;/code&gt; waits for the task to complete and then returns or propagates that task's result.&lt;/p&gt;

&lt;p&gt;That distinction matters because Swift cancellation is not a general-purpose interrupt.&lt;/p&gt;

&lt;p&gt;It is a signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  The small example
&lt;/h2&gt;

&lt;p&gt;Consider a service that deduplicates refresh work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;actor&lt;/span&gt; &lt;span class="kt"&gt;RefreshStore&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;refreshTask&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Task&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;throws&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;refreshTask&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;refreshTask&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Task&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;performRefresh&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;refreshTask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;

        &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;refreshTask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;performRefresh&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;throws&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="kt"&gt;Task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"fresh"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is not the actor.&lt;/p&gt;

&lt;p&gt;It is that &lt;code&gt;task&lt;/code&gt; is its own task.&lt;/p&gt;

&lt;p&gt;Several callers may end up awaiting the same handle.&lt;/p&gt;

&lt;p&gt;Now imagine one of those callers is cancelled while this line is suspended:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is easy to read &lt;code&gt;try await&lt;/code&gt; and mentally substitute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;caller cancelled
↓
await throws CancellationError
↓
shared task stops
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But those are separate events.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cancelling a Task is cooperative
&lt;/h2&gt;

&lt;p&gt;Swift's &lt;code&gt;Task.cancel()&lt;/code&gt; does three important things: it marks the task as cancelled, triggers its cancellation handlers, and propagates cancellation through its structured child tasks.&lt;/p&gt;

&lt;p&gt;What it does &lt;strong&gt;not&lt;/strong&gt; do is force arbitrary asynchronous work to stop.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.swift.org/latest/documentation/swift/task/cancel%28%29/" rel="noopener noreferrer"&gt;Swift documentation&lt;/a&gt; is explicit about this: cancellation is cooperative. Code has to observe cancellation and decide how to react.&lt;/p&gt;

&lt;p&gt;That can happen through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kt"&gt;Task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isCancelled&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="kt"&gt;Task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkCancellation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or because an operation you call is itself cancellation-aware.&lt;/p&gt;

&lt;p&gt;A cancelled task can continue running if its code never responds to the cancellation signal.&lt;/p&gt;

&lt;p&gt;That is the first mental model to keep.&lt;/p&gt;

&lt;h2&gt;
  
  
  The caller and the shared task are not the same task
&lt;/h2&gt;

&lt;p&gt;Now return to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The current task is the caller.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;task&lt;/code&gt; is another task represented by a &lt;code&gt;Task&lt;/code&gt; handle.&lt;/p&gt;

&lt;p&gt;Cancelling the current task marks the &lt;strong&gt;current task&lt;/strong&gt; as cancelled.&lt;/p&gt;

&lt;p&gt;It does not mean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;was called on the shared task.&lt;/p&gt;

&lt;p&gt;And it does not mean the shared task must inherit that cancellation just because another task is awaiting its value.&lt;/p&gt;

&lt;p&gt;That is the key difference between waiting on an unstructured task handle and cancellation propagation inside a structured task tree.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Task.value actually promises
&lt;/h2&gt;

&lt;p&gt;For a throwing task, &lt;code&gt;value&lt;/code&gt; is an &lt;a href="https://developer.apple.com/documentation/swift/task/value-60t02" rel="noopener noreferrer"&gt;async throwing property&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If the task has not completed, accessing &lt;code&gt;value&lt;/code&gt; waits for it.&lt;/p&gt;

&lt;p&gt;If that task eventually throws, &lt;code&gt;value&lt;/code&gt; propagates that error.&lt;/p&gt;

&lt;p&gt;If that task responds to its own cancellation by throwing &lt;code&gt;CancellationError&lt;/code&gt;, then awaiting its value propagates that error.&lt;/p&gt;

&lt;p&gt;Notice which task those statements are about.&lt;/p&gt;

&lt;p&gt;They are about the task represented by the handle.&lt;/p&gt;

&lt;p&gt;That is not the same as saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the caller waiting for &lt;code&gt;value&lt;/code&gt; becomes cancelled, &lt;code&gt;value&lt;/code&gt; automatically throws &lt;code&gt;CancellationError&lt;/code&gt; from the caller's cancellation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That assumption is exactly where the bug in the mental model starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structured cancellation is different
&lt;/h2&gt;

&lt;p&gt;Swift does propagate cancellation through structured concurrency.&lt;/p&gt;

&lt;p&gt;If you cancel a task that owns child tasks created through a &lt;a href="https://docs.swift.org/latest/documentation/swift/taskgroup/" rel="noopener noreferrer"&gt;task group&lt;/a&gt; or another structured construct, cancellation is propagated down that hierarchy.&lt;/p&gt;

&lt;p&gt;But an unstructured &lt;code&gt;Task { ... }&lt;/code&gt; stored in a property and shared among callers is a different shape.&lt;/p&gt;

&lt;p&gt;You now have independent task lifetimes.&lt;/p&gt;

&lt;p&gt;That can be useful.&lt;/p&gt;

&lt;p&gt;For example, you may intentionally want one network refresh to continue even if one screen disappears, because another caller is still waiting for the same refresh.&lt;/p&gt;

&lt;p&gt;But once you make that choice, you have to decide explicitly what caller cancellation should mean.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sometimes you do want the caller to stop waiting
&lt;/h2&gt;

&lt;p&gt;Suppose the shared refresh should continue for other clients, but a cancelled caller should stop caring about its result.&lt;/p&gt;

&lt;p&gt;Those are two different cancellation decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Should the underlying shared operation be cancelled?&lt;/li&gt;
&lt;li&gt;Should this particular caller continue waiting for it?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not collapse them into one boolean.&lt;/p&gt;

&lt;p&gt;If you need the waiting caller to react immediately, you need to design that behaviour explicitly. A &lt;a href="https://docs.swift.org/latest/documentation/swift/withtaskcancellationhandler%28operation%3Aoncancel%3A%29/" rel="noopener noreferrer"&gt;cancellation handler&lt;/a&gt; can observe cancellation immediately, but you still need a safe mechanism for racing the shared result against the caller's cancellation without accidentally cancelling work needed by other consumers.&lt;/p&gt;

&lt;p&gt;The correct design depends on the ownership model of the operation.&lt;/p&gt;

&lt;p&gt;That is why "just cancel the task" is usually too vague.&lt;/p&gt;

&lt;h2&gt;
  
  
  The inverse problem is just as dangerous
&lt;/h2&gt;

&lt;p&gt;There is another common mistake:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;withTaskCancellationHandler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nv"&gt;onCancel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This may be correct if the caller owns the task.&lt;/p&gt;

&lt;p&gt;It can be very wrong if &lt;code&gt;task&lt;/code&gt; is shared.&lt;/p&gt;

&lt;p&gt;Imagine three callers waiting on the same refresh:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Caller A ─┐
Caller B ─┼──&amp;gt; Shared Task
Caller C ─┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Caller A goes away.&lt;/p&gt;

&lt;p&gt;If A's cancellation handler blindly calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A has now changed the operation for B and C too.&lt;/p&gt;

&lt;p&gt;That may be exactly what you did not want.&lt;/p&gt;

&lt;p&gt;Shared work needs shared ownership semantics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cancellation is not ownership
&lt;/h2&gt;

&lt;p&gt;This is the broader lesson.&lt;/p&gt;

&lt;p&gt;Cancellation is often discussed as if it were a universal "stop" command.&lt;/p&gt;

&lt;p&gt;It is not.&lt;/p&gt;

&lt;p&gt;In Swift it is cooperative, and propagation depends on the task relationship you built.&lt;/p&gt;

&lt;p&gt;Before writing cancellation code, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which task was cancelled?&lt;/li&gt;
&lt;li&gt;Which task owns the work?&lt;/li&gt;
&lt;li&gt;Is the operation structured or unstructured?&lt;/li&gt;
&lt;li&gt;Is the task handle shared?&lt;/li&gt;
&lt;li&gt;Should one caller be able to cancel work used by another caller?&lt;/li&gt;
&lt;li&gt;Does the operation check cancellation itself?&lt;/li&gt;
&lt;li&gt;Do I want to cancel the work, stop waiting, or both?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those questions are much more useful than simply asking whether something is &lt;code&gt;async throws&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters in interviews
&lt;/h2&gt;

&lt;p&gt;A senior Swift concurrency answer should not stop at:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Cancellation throws &lt;code&gt;CancellationError&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is too broad.&lt;/p&gt;

&lt;p&gt;A stronger answer separates the pieces:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Cancelling a Swift task sets cancellation state and propagates through structured children. It does not forcibly stop arbitrary work. If I am awaiting a separate shared &lt;code&gt;Task&lt;/code&gt; handle, cancelling my caller does not by itself cancel that shared task. I need to decide explicitly whether the caller should stop waiting, whether the shared work should be cancelled, or both.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the answer I would want to hear before we start editing code.&lt;/p&gt;

&lt;h2&gt;
  
  
  This was corrected in Share Your Screen 1.3
&lt;/h2&gt;

&lt;p&gt;An earlier version of Share Your Screen described this shared-task case incorrectly. It said that a cancelled caller would leave the &lt;code&gt;await&lt;/code&gt; with &lt;code&gt;CancellationError&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Version 1.3 corrects that explanation.&lt;/p&gt;

&lt;p&gt;The point of the correction is not that cancellation "never throws". It absolutely can. Swift provides &lt;code&gt;Task.checkCancellation()&lt;/code&gt;, cancellation-aware APIs can throw, and a task can propagate its own cancellation error through &lt;code&gt;value&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The important correction is narrower:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Cancelling the caller is not, by itself, a rule that makes an await on a separate shared task handle immediately throw &lt;code&gt;CancellationError&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once you separate the caller from the shared work, the behaviour becomes much easier to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Swift, &lt;a href="https://docs.swift.org/latest/documentation/swift/task/cancel%28%29/" rel="noopener noreferrer"&gt;Task.cancel()&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Apple, &lt;a href="https://developer.apple.com/documentation/swift/task/value-60t02" rel="noopener noreferrer"&gt;Task.value&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Swift, &lt;a href="https://docs.swift.org/latest/documentation/swift/withtaskcancellationhandler%28operation%3Aoncancel%3A%29/" rel="noopener noreferrer"&gt;withTaskCancellationHandler(operation:onCancel:)&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Swift, &lt;a href="https://docs.swift.org/latest/documentation/swift/taskgroup/" rel="noopener noreferrer"&gt;TaskGroup&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;First published on &lt;a href="https://salari.dev/writing/swift-shared-task-cancellation/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=swift-shared-task-cancellation" rel="noopener noreferrer"&gt;salari.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>swift</category>
      <category>ios</category>
      <category>concurrency</category>
      <category>programming</category>
    </item>
    <item>
      <title>An actor protects state. It does not own every retain and release.</title>
      <dc:creator>Mike Salari</dc:creator>
      <pubDate>Tue, 13 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/salari_dev/an-actor-protects-state-it-does-not-own-every-retain-and-release-27ba</link>
      <guid>https://dev.to/salari_dev/an-actor-protects-state-it-does-not-own-every-retain-and-release-27ba</guid>
      <description>&lt;p&gt;Swift actors make an important promise.&lt;/p&gt;

&lt;p&gt;They isolate mutable state.&lt;/p&gt;

&lt;p&gt;That promise is strong enough for Swift 6 to reject programs that cross concurrency boundaries unsafely, and it gives us a much better language for reasoning about data races.&lt;/p&gt;

&lt;p&gt;But there is a tempting shortcut hiding inside that model:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If an object is actor-isolated, its references must be isolated too.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;From there it is easy to jump again:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If those references are isolated, ARC should not need atomic reference counting.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sounds reasonable.&lt;/p&gt;

&lt;p&gt;It is also more than actor isolation actually promises.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the guarantee actors do make
&lt;/h2&gt;

&lt;p&gt;An &lt;code&gt;actor&lt;/code&gt; protects its &lt;a href="https://docs.swift.org/main/documentation/swift/actor/" rel="noopener noreferrer"&gt;actor-isolated state&lt;/a&gt; from concurrent access that violates Swift's isolation rules.&lt;/p&gt;

&lt;p&gt;An actor also conforms to &lt;code&gt;Sendable&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Those are language-level concurrency properties.&lt;/p&gt;

&lt;p&gt;They help answer questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can this mutable state be accessed from here?&lt;/li&gt;
&lt;li&gt;Does this value cross an isolation boundary legally?&lt;/li&gt;
&lt;li&gt;Does this code need &lt;code&gt;await&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Is the compiler able to prove that a particular access is safe?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are valuable guarantees.&lt;/p&gt;

&lt;p&gt;But notice what is missing.&lt;/p&gt;

&lt;p&gt;Actor isolation does not say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every reference-count operation associated with every object reachable from this actor happens only while executing on this actor.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That would be a much broader lifetime guarantee.&lt;/p&gt;

&lt;h2&gt;
  
  
  A reference can participate in more than one mechanism
&lt;/h2&gt;

&lt;p&gt;Consider an actor that stores a class instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;Payload&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;

    &lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;actor&lt;/span&gt; &lt;span class="kt"&gt;Store&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Payload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;currentValue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actor isolates access to &lt;code&gt;payload&lt;/code&gt; as actor state.&lt;/p&gt;

&lt;p&gt;That tells you something important about how the property can be accessed.&lt;/p&gt;

&lt;p&gt;But ARC is not merely a record of source-level property accesses.&lt;/p&gt;

&lt;p&gt;Reference lifetime can be affected by generated temporaries, function arguments, return values, captures, ownership conventions, optimizer decisions and other lowering details.&lt;/p&gt;

&lt;p&gt;So there are two different questions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Who may access this state?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;How is this object's lifetime represented and updated in generated code?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first is where actor isolation operates.&lt;/p&gt;

&lt;p&gt;The second belongs to ownership, ARC and compiler lowering.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is exactly what the ARC experiment tested
&lt;/h2&gt;

&lt;p&gt;I built a &lt;a href="https://github.com/salari-dev/swift6-arc-atomicity-lab" rel="noopener noreferrer"&gt;small Swift 6 lab&lt;/a&gt; around this distinction.&lt;/p&gt;

&lt;p&gt;The negative control was a deliberately illegal case: non-Sendable mutable reference state crossing a concurrency boundary.&lt;/p&gt;

&lt;p&gt;Swift 6 rejected it.&lt;/p&gt;

&lt;p&gt;Then I tested valid cases using actor isolation, &lt;code&gt;Sendable&lt;/code&gt;, and &lt;code&gt;@unchecked Sendable&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Those cases compiled.&lt;/p&gt;

&lt;p&gt;If concurrency isolation also acted as a direct ARC atomicity selector, this would be a reasonable place to expect the generated reference-counting path to change.&lt;/p&gt;

&lt;p&gt;In the Apple Swift 6.3.3 cases I tested, it did not.&lt;/p&gt;

&lt;p&gt;Ordinary builds still emitted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="n"&gt;swift_retain&lt;/span&gt;
&lt;span class="n"&gt;swift_release&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actor-isolated and Sendable cases did not switch those observed calls to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="n"&gt;swift_nonatomic_retain&lt;/span&gt;
&lt;span class="n"&gt;swift_nonatomic_release&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I changed a different assumption.&lt;/p&gt;

&lt;p&gt;I compiled with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-Xfrontend -assume-single-threaded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the observed calls changed to the non-atomic variants.&lt;/p&gt;

&lt;p&gt;That is a much cleaner signal than trying to infer ARC behaviour from the presence of &lt;code&gt;actor&lt;/code&gt; or &lt;code&gt;Sendable&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The A/B
&lt;/h2&gt;

&lt;p&gt;For the controlled retain/release case in the lab:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Build&lt;/th&gt;
&lt;th&gt;Retain&lt;/th&gt;
&lt;th&gt;Release&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;normal &lt;code&gt;-Onone&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swift_retain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swift_release&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;single-threaded &lt;code&gt;-Onone&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swift_nonatomic_retain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swift_nonatomic_release&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;normal &lt;code&gt;-O&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swift_retain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swift_release&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;single-threaded &lt;code&gt;-O&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swift_nonatomic_retain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swift_nonatomic_release&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The result is deliberately narrow.&lt;/p&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; prove that Swift ARC is always atomic.&lt;/p&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; prove that actor isolation can never participate in an ARC optimization.&lt;/p&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; prove that &lt;code&gt;Sendable&lt;/code&gt; can never influence optimization.&lt;/p&gt;

&lt;p&gt;It shows that in these tested Apple Swift 6.3.3 cases, changing the concurrency validity of the program did not change the observed ARC runtime entry-point selection, while the explicit single-threaded compiler assumption did.&lt;/p&gt;

&lt;p&gt;That is compiler/runtime implementation evidence for the tested toolchain, not a Swift language guarantee.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Sendable does not close the gap
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Sendable&lt;/code&gt; is another place where the same mental shortcut appears.&lt;/p&gt;

&lt;p&gt;A type conforming to &lt;code&gt;Sendable&lt;/code&gt; is saying something about crossing concurrency boundaries safely under Swift's concurrency model.&lt;/p&gt;

&lt;p&gt;For unchecked conformance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;@unchecked&lt;/span&gt; &lt;span class="kt"&gt;Sendable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// You are taking responsibility for the safety contract.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the compiler is not proving your synchronization strategy for you.&lt;/p&gt;

&lt;p&gt;You are asserting that the type can be used safely across those boundaries.&lt;/p&gt;

&lt;p&gt;That still does not mean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sendable
↓
non-atomic refcount
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Sendable&lt;/code&gt; is not an ARC annotation.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.swift.org/latest/documentation/swift/sendable/" rel="noopener noreferrer"&gt;Swift documentation&lt;/a&gt; describes &lt;code&gt;@unchecked Sendable&lt;/code&gt; in exactly that spirit: you assume responsibility for correctness when the compiler cannot enforce the conformance itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Isolation is about access, not exclusive lifetime ownership
&lt;/h2&gt;

&lt;p&gt;The easiest way to remember the distinction is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Actor isolation controls access to isolated state. It does not mean the actor is the only place where all lifetime bookkeeping for every related reference can occur.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That difference becomes more visible when values move across APIs.&lt;/p&gt;

&lt;p&gt;An actor method can return a Sendable value.&lt;/p&gt;

&lt;p&gt;A reference may be captured by generated code.&lt;/p&gt;

&lt;p&gt;A function call may introduce ownership traffic that is not interesting at the source level.&lt;/p&gt;

&lt;p&gt;Optimization may remove, combine or reposition retain/release operations.&lt;/p&gt;

&lt;p&gt;None of that means actor isolation is broken.&lt;/p&gt;

&lt;p&gt;It means source-level isolation and runtime lifetime accounting are different layers of the implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for performance reasoning
&lt;/h2&gt;

&lt;p&gt;This is where developers can accidentally turn a correctness feature into an imagined performance guarantee.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I moved this behind an actor, so ARC can be cheaper now.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That conclusion does not follow from the actor model.&lt;/p&gt;

&lt;p&gt;The actor may absolutely improve the correctness and architecture of the code.&lt;/p&gt;

&lt;p&gt;It may also change performance in many ways, including scheduling and serialization costs.&lt;/p&gt;

&lt;p&gt;But if your performance claim is specifically about reference-count atomicity, you need evidence from generated code or runtime behaviour.&lt;/p&gt;

&lt;p&gt;Do not infer it from &lt;code&gt;actor&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Do not infer it from &lt;code&gt;Sendable&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Measure the layer you are making the claim about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why SIL alone is not enough
&lt;/h2&gt;

&lt;p&gt;This also explains why looking at one intermediate representation can be misleading.&lt;/p&gt;

&lt;p&gt;SIL exposes ownership operations, but the question in the ARC experiment was narrower:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which runtime ARC entry points survive into the generated program?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For that, LLVM IR and assembly are more directly useful.&lt;/p&gt;

&lt;p&gt;Optimization can eliminate or combine ARC traffic, so even the presence of an ownership operation in one stage does not guarantee a corresponding runtime call later.&lt;/p&gt;

&lt;p&gt;This is why the lab keeps SIL, LLVM IR and assembly evidence separate.&lt;/p&gt;

&lt;p&gt;The more specific your claim, the closer your evidence should be to the layer you are claiming something about.&lt;/p&gt;

&lt;h2&gt;
  
  
  A better senior-level mental model
&lt;/h2&gt;

&lt;p&gt;When reasoning about Swift concurrency and ownership, keep three layers separate:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Isolation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Who is allowed to access this state, and from which concurrency domain?&lt;/p&gt;

&lt;p&gt;Actors and global actors live here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Transfer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Can this value cross a concurrency boundary safely?&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Sendable&lt;/code&gt; lives here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Lifetime accounting&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;How does the generated program keep the referenced object alive and eventually destroy it?&lt;/p&gt;

&lt;p&gt;ARC and compiler lowering live here.&lt;/p&gt;

&lt;p&gt;These layers interact.&lt;/p&gt;

&lt;p&gt;They are not interchangeable.&lt;/p&gt;

&lt;p&gt;That is why a program can become valid under Swift 6's concurrency checker while the ARC entry points observed in generated code remain unchanged.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical rule
&lt;/h2&gt;

&lt;p&gt;If Swift 6 rejects the program, fix the concurrency problem.&lt;/p&gt;

&lt;p&gt;If you want to know what ARC is doing, inspect ARC.&lt;/p&gt;

&lt;p&gt;Do not use one as a proxy for the other.&lt;/p&gt;

&lt;p&gt;That sounds obvious when written down.&lt;/p&gt;

&lt;p&gt;It is much less obvious when an actor has just given the compiler a strong new proof and you are staring at a pair of &lt;code&gt;swift_retain&lt;/code&gt; / &lt;code&gt;swift_release&lt;/code&gt; calls that refuse to disappear.&lt;/p&gt;

&lt;p&gt;The compiler is not contradicting itself.&lt;/p&gt;

&lt;p&gt;You were asking two different questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce the ARC experiment
&lt;/h2&gt;

&lt;p&gt;The full lab is public:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/salari-dev/swift6-arc-atomicity-lab" rel="noopener noreferrer"&gt;https://github.com/salari-dev/swift6-arc-atomicity-lab&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the negative concurrency control;&lt;/li&gt;
&lt;li&gt;actor-isolated, &lt;code&gt;Sendable&lt;/code&gt;, and &lt;code&gt;@unchecked Sendable&lt;/code&gt; cases;&lt;/li&gt;
&lt;li&gt;SIL, LLVM IR and assembly evidence;&lt;/li&gt;
&lt;li&gt;the atomic versus non-atomic A/B;&lt;/li&gt;
&lt;li&gt;compiler-selection tracing;&lt;/li&gt;
&lt;li&gt;reproduction scripts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tested environment documented in the repo is Apple Swift 6.3.3, Xcode 26.6, macOS 26.6.2, arm64, Swift language mode 6, with iPhone 17 Simulator used for simulator validation.&lt;/p&gt;

&lt;p&gt;No second official Swift toolchain was tested, so this should not be presented as a permanent language rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Swift, &lt;a href="https://docs.swift.org/main/documentation/swift/actor/" rel="noopener noreferrer"&gt;Actor&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Swift, &lt;a href="https://docs.swift.org/latest/documentation/swift/sendable/" rel="noopener noreferrer"&gt;Sendable&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Reproduction repository: &lt;a href="https://github.com/salari-dev/swift6-arc-atomicity-lab" rel="noopener noreferrer"&gt;salari-dev/swift6-arc-atomicity-lab&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;First published on &lt;a href="https://salari.dev/writing/swift-actor-isolation-arc-lifetime/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=swift-actor-isolation-arc-lifetime" rel="noopener noreferrer"&gt;salari.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>swift</category>
      <category>ios</category>
      <category>concurrency</category>
      <category>programming</category>
    </item>
    <item>
      <title>Swift 6 rejected the race. ARC stayed atomic.</title>
      <dc:creator>Mike Salari</dc:creator>
      <pubDate>Tue, 06 Oct 2026 15:06:00 +0000</pubDate>
      <link>https://dev.to/salari_dev/swift-6-rejected-the-race-arc-stayed-atomic-4lnh</link>
      <guid>https://dev.to/salari_dev/swift-6-rejected-the-race-arc-stayed-atomic-4lnh</guid>
      <description>&lt;p&gt;Swift 6 rejected the unsafe program exactly where I expected it to.&lt;/p&gt;

&lt;p&gt;I had a small reference-type experiment with state shared across concurrency boundaries. In Swift 6 language mode, the negative control failed with concurrency diagnostics. Good. That part was boring in the best possible way.&lt;/p&gt;

&lt;p&gt;Then I fixed the concurrency problem.&lt;/p&gt;

&lt;p&gt;I moved the state behind actor isolation. I tested &lt;code&gt;Sendable&lt;/code&gt; variants. I also tested an &lt;code&gt;@unchecked Sendable&lt;/code&gt; case where I was explicitly taking responsibility for the safety guarantee.&lt;/p&gt;

&lt;p&gt;The code compiled.&lt;/p&gt;

&lt;p&gt;Then I looked at what ARC was doing.&lt;/p&gt;

&lt;p&gt;I expected the generated code to change.&lt;/p&gt;

&lt;p&gt;It did not.&lt;/p&gt;

&lt;p&gt;In the Apple Swift 6.3.3 cases I tested, the runtime still went through the normal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="n"&gt;swift_retain&lt;/span&gt;
&lt;span class="n"&gt;swift_release&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Actor isolation had changed what Swift could prove about access to the program state.&lt;/p&gt;

&lt;p&gt;It had not changed the ARC runtime entry points I was observing.&lt;/p&gt;

&lt;p&gt;That was the interesting part.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question
&lt;/h2&gt;

&lt;p&gt;Swift 6 has a much stronger concurrency model than earlier Swift versions.&lt;/p&gt;

&lt;p&gt;The compiler can reject code that moves non-Sendable state across isolation boundaries. Actors provide isolation. &lt;code&gt;Sendable&lt;/code&gt; describes values that can safely cross concurrency domains. Strict concurrency checking catches classes of mistakes that previously survived compilation.&lt;/p&gt;

&lt;p&gt;So I wanted to test a fairly simple question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If Swift 6 can prove that some object access is isolated from concurrent mutation, can ARC use cheaper non-atomic reference counting for that object?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sounds plausible.&lt;/p&gt;

&lt;p&gt;If the compiler knows an object is actor-isolated, why pay synchronization costs for reference-count updates that should not race?&lt;/p&gt;

&lt;p&gt;The problem is that two different questions are being mixed together.&lt;/p&gt;

&lt;p&gt;Swift concurrency asks whether program state is being accessed or transferred safely.&lt;/p&gt;

&lt;p&gt;ARC asks how object lifetime is accounted for.&lt;/p&gt;

&lt;p&gt;Those are related, but they are not the same contract.&lt;/p&gt;

&lt;p&gt;I wanted to see where that distinction showed up in generated code rather than stopping at the language model.&lt;/p&gt;

&lt;h2&gt;
  
  
  The experiment
&lt;/h2&gt;

&lt;p&gt;I ran the experiment using &lt;strong&gt;Apple Swift 6.3.3 from Xcode 26.6&lt;/strong&gt;, explicitly compiling in Swift 6 language mode.&lt;/p&gt;

&lt;p&gt;The test environment was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Apple Swift 6.3.3
Xcode 26.6
macOS 26.6.2
arm64
Swift language mode 6
iPhone 17 Simulator for simulator validation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repository contains a negative control plus several valid cases, including actor isolation and &lt;code&gt;Sendable&lt;/code&gt; variants.&lt;/p&gt;

&lt;p&gt;The negative control matters.&lt;/p&gt;

&lt;p&gt;Without it, seeing a successful Swift 6 build tells us almost nothing. I wanted a case where the compiler demonstrably rejected unsafe sharing, then closely related cases where the concurrency problem had actually been addressed.&lt;/p&gt;

&lt;p&gt;The unsafe case failed as expected.&lt;/p&gt;

&lt;p&gt;The valid cases compiled.&lt;/p&gt;

&lt;p&gt;Then I looked at multiple stages of compiler output.&lt;/p&gt;

&lt;p&gt;That distinction matters because SIL ownership instructions are not the same thing as the final ARC runtime calls.&lt;/p&gt;

&lt;p&gt;At the SIL level you can see ownership operations and retain/release behaviour represented in Swift's intermediate representation.&lt;/p&gt;

&lt;p&gt;But if the question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which runtime reference-counting entry point does this build actually use?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;then LLVM IR and assembly are the more interesting places to look.&lt;/p&gt;

&lt;p&gt;In the ordinary Swift 6 builds I tested, I continued to find:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="n"&gt;swift_retain&lt;/span&gt;
&lt;span class="n"&gt;swift_release&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That remained true when the concurrency model around the object changed.&lt;/p&gt;

&lt;p&gt;Actor isolation did not turn the observed calls into non-atomic ARC operations.&lt;/p&gt;

&lt;p&gt;Neither did the &lt;code&gt;Sendable&lt;/code&gt; or &lt;code&gt;@unchecked Sendable&lt;/code&gt; cases I tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I changed one compiler assumption
&lt;/h2&gt;

&lt;p&gt;There is a compiler option that makes this experiment much more useful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-Xfrontend -assume-single-threaded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The name is refreshingly direct.&lt;/p&gt;

&lt;p&gt;Instead of asking the compiler to infer from actor isolation that some individual object's reference counting can be cheaper, this flag changes a much broader assumption about the program.&lt;/p&gt;

&lt;p&gt;The compiler is being told to assume single-threaded execution.&lt;/p&gt;

&lt;p&gt;I compiled the controlled case again.&lt;/p&gt;

&lt;p&gt;This time, the output changed.&lt;/p&gt;

&lt;p&gt;Instead of the ordinary ARC runtime entry points, I observed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="n"&gt;swift_nonatomic_retain&lt;/span&gt;
&lt;span class="n"&gt;swift_nonatomic_release&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that happened in both the unoptimized and optimized configurations I tested.&lt;/p&gt;

&lt;p&gt;The clean A/B looked like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Build&lt;/th&gt;
&lt;th&gt;Retain&lt;/th&gt;
&lt;th&gt;Release&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;normal &lt;code&gt;-Onone&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swift_retain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swift_release&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;single-threaded &lt;code&gt;-Onone&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swift_nonatomic_retain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swift_nonatomic_release&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;normal &lt;code&gt;-O&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swift_retain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swift_release&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;single-threaded &lt;code&gt;-O&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swift_nonatomic_retain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;swift_nonatomic_release&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That gave me the selector I wanted.&lt;/p&gt;

&lt;p&gt;The same general reference-counting operation could be lowered differently when the compiler was explicitly told to assume a single-threaded program.&lt;/p&gt;

&lt;p&gt;But Swift 6 actor isolation by itself had not produced the same result in my experiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this distinction matters
&lt;/h2&gt;

&lt;p&gt;It would be easy to look at Swift 6 concurrency and reason like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An actor guarantees isolated access.&lt;/li&gt;
&lt;li&gt;Therefore this reference cannot be touched concurrently.&lt;/li&gt;
&lt;li&gt;Therefore ARC for this reference does not need atomic operations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first statement can be valid.&lt;/p&gt;

&lt;p&gt;The jump from the first statement to the third is where things become much less obvious.&lt;/p&gt;

&lt;p&gt;Actor isolation is a source-level concurrency guarantee about access to isolated state.&lt;/p&gt;

&lt;p&gt;ARC lifetime operations may happen for references that move through code outside the isolated body, through calling conventions, temporary values, captures, ownership transfers, bridging, optimizer transformations, and other parts of the generated program.&lt;/p&gt;

&lt;p&gt;A proof that some mutable state is actor-isolated is not automatically the same thing as a whole-program proof that every relevant lifetime operation for an object can use non-atomic accounting.&lt;/p&gt;

&lt;p&gt;The experiment does not prove why every individual retain or release remains on a particular runtime path.&lt;/p&gt;

&lt;p&gt;What it does show is narrower and more useful:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In the Apple Swift 6.3.3 cases I tested, making the program concurrency-safe through actor isolation, &lt;code&gt;Sendable&lt;/code&gt;, or &lt;code&gt;@unchecked Sendable&lt;/code&gt; did not change the observed ARC calls from &lt;code&gt;swift_retain&lt;/code&gt; / &lt;code&gt;swift_release&lt;/code&gt; to their non-atomic counterparts.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Explicitly switching the compiler to a single-threaded assumption did.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;compiler/runtime implementation evidence for the tested toolchain, not a Swift language guarantee&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That distinction matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Following the compiler
&lt;/h2&gt;

&lt;p&gt;I also traced the relevant compiler path instead of treating the runtime symbol names as magic.&lt;/p&gt;

&lt;p&gt;For the toolchain path examined in the repository, the single-threaded assumption flows roughly like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;-Xfrontend -assume-single-threaded&lt;/code&gt; (frontend option)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CompilerInvocation&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SILOptions::AssumeSingleThreaded&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SILModule::isDefaultAtomic()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;IRGen atomicity&lt;/li&gt;
&lt;li&gt;GenHeap runtime call selection&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A normal build emits &lt;code&gt;swift_retain&lt;/code&gt; / &lt;code&gt;swift_release&lt;/code&gt;. With the single-threaded assumption it emits &lt;code&gt;swift_nonatomic_retain&lt;/code&gt; / &lt;code&gt;swift_nonatomic_release&lt;/code&gt;. For an actor-isolated object or a &lt;code&gt;Sendable&lt;/code&gt; type, no rule was found: that branch does not reach IRGen.&lt;/p&gt;

&lt;p&gt;That is useful because it connects the frontend assumption to the ARC lowering decision.&lt;/p&gt;

&lt;p&gt;The interesting part is what I did not find in that path.&lt;/p&gt;

&lt;p&gt;I did not find a corresponding rule that says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;actor-isolated object
↓
non-atomic ARC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kt"&gt;Sendable&lt;/span&gt;
&lt;span class="err"&gt;↓&lt;/span&gt;
&lt;span class="n"&gt;non&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;atomic&lt;/span&gt; &lt;span class="kt"&gt;ARC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the generated output from the experiment matched that.&lt;/p&gt;

&lt;p&gt;That does not mean such an optimization could never exist.&lt;/p&gt;

&lt;p&gt;It means I could not demonstrate it in these cases, with this toolchain, and the compiler path I traced points to a broader default-atomicity decision rather than a simple mapping from Swift concurrency annotations to ARC atomicity.&lt;/p&gt;

&lt;p&gt;There is one additional limitation worth stating clearly: the open-source Swift source revision inspected in the repo is not proven to be the exact source revision Apple used to build Swift 6.3.3.&lt;/p&gt;

&lt;p&gt;So the compiler-source tracing is supporting evidence, not a claim of exact source-build identity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sendable is not an ARC annotation
&lt;/h2&gt;

&lt;p&gt;This is also a useful reminder about &lt;code&gt;Sendable&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Sendable&lt;/code&gt; is frequently discussed as if it were a general "thread safe" marker.&lt;/p&gt;

&lt;p&gt;That is too vague.&lt;/p&gt;

&lt;p&gt;Its job is about whether values can safely cross concurrency boundaries under Swift's concurrency model.&lt;/p&gt;

&lt;p&gt;It does not say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use non-atomic reference counting for this instance.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Likewise, &lt;code&gt;@unchecked Sendable&lt;/code&gt; does not unlock an ARC optimization.&lt;/p&gt;

&lt;p&gt;It tells the compiler that you are taking responsibility for a guarantee it cannot verify itself.&lt;/p&gt;

&lt;p&gt;In my tested cases, neither changed the ARC runtime calls I was looking at.&lt;/p&gt;

&lt;p&gt;That separation is important because otherwise it becomes very easy to assign performance properties to concurrency annotations that the language never promised.&lt;/p&gt;

&lt;h2&gt;
  
  
  Actor isolation is still doing real work
&lt;/h2&gt;

&lt;p&gt;None of this makes actor isolation less valuable.&lt;/p&gt;

&lt;p&gt;The negative control was rejected.&lt;/p&gt;

&lt;p&gt;The isolated version compiled.&lt;/p&gt;

&lt;p&gt;That is a meaningful change in the program.&lt;/p&gt;

&lt;p&gt;Swift 6 was enforcing a concurrency contract and preventing a class of unsafe access.&lt;/p&gt;

&lt;p&gt;What did not happen was a second, independent assumption:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Because this code is now accepted by the concurrency checker, ARC can use non-atomic lifetime accounting.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those are two different decisions.&lt;/p&gt;

&lt;p&gt;That distinction is probably the main thing I took away from the experiment.&lt;/p&gt;

&lt;p&gt;Swift's concurrency model can become more precise without every other subsystem automatically inheriting those proofs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just look at SIL?
&lt;/h2&gt;

&lt;p&gt;Because SIL can easily tempt you into making claims one level too early.&lt;/p&gt;

&lt;p&gt;Ownership operations in SIL tell you something about Swift's ownership representation and optimization pipeline.&lt;/p&gt;

&lt;p&gt;They do not necessarily tell you which reference-counting runtime function survives into the final generated program.&lt;/p&gt;

&lt;p&gt;Optimization makes this even more complicated.&lt;/p&gt;

&lt;p&gt;A retain may disappear. Two operations may be combined. A lifetime can be shortened or extended. Something visible at &lt;code&gt;-Onone&lt;/code&gt; may not exist at &lt;code&gt;-O&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For this question, I wanted to check SIL, then continue down to LLVM IR and assembly rather than infer the final runtime behaviour from one intermediate stage.&lt;/p&gt;

&lt;p&gt;That also makes the experiment easier to falsify.&lt;/p&gt;

&lt;p&gt;You do not have to trust my interpretation of a SIL ownership instruction.&lt;/p&gt;

&lt;p&gt;You can inspect the generated IR or assembly and search for the actual runtime symbols.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this experiment does not prove
&lt;/h2&gt;

&lt;p&gt;There are several conclusions I would not draw from this.&lt;/p&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; prove:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Swift ARC is always atomic.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It does not prove:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Actor-isolated objects always use atomic refcounts.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It does not prove:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Sendable&lt;/code&gt; can never influence ARC optimization.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It does not prove:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Atomic reference counting protects your mutable program state.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It does not prove:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;-assume-single-threaded&lt;/code&gt; is a production optimization you should enable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And it does not prove that the behaviour observed in this toolchain is a permanent Swift language rule.&lt;/p&gt;

&lt;p&gt;Those claims are much broader than the evidence.&lt;/p&gt;

&lt;p&gt;The result is specifically about the compiler configurations and source cases I tested.&lt;/p&gt;

&lt;p&gt;The defensible claim is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In these Apple Swift 6.3.3 experiments, actor isolation, &lt;code&gt;Sendable&lt;/code&gt;, and &lt;code&gt;@unchecked Sendable&lt;/code&gt; changed the concurrency validity of the program but did not change the observed ARC runtime entry points. Explicitly compiling with &lt;code&gt;-assume-single-threaded&lt;/code&gt; did.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the claim I am comfortable publishing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical lesson
&lt;/h2&gt;

&lt;p&gt;Most application developers will never choose ARC entry points manually.&lt;/p&gt;

&lt;p&gt;That is not really the point.&lt;/p&gt;

&lt;p&gt;The useful lesson is about how to reason about compiler guarantees.&lt;/p&gt;

&lt;p&gt;Swift 6 rejecting a data race does not mean every subsystem now gets to assume that the entire lifetime of every involved object is single-threaded.&lt;/p&gt;

&lt;p&gt;Concurrency safety is one contract.&lt;/p&gt;

&lt;p&gt;Object lifetime accounting is another.&lt;/p&gt;

&lt;p&gt;Sometimes a compiler can connect proofs across those boundaries.&lt;/p&gt;

&lt;p&gt;Sometimes it cannot, or deliberately does not.&lt;/p&gt;

&lt;p&gt;And sometimes you need to stop looking at the language feature and inspect the generated program.&lt;/p&gt;

&lt;p&gt;That is what happened here.&lt;/p&gt;

&lt;p&gt;Swift 6 rejected the race.&lt;/p&gt;

&lt;p&gt;ARC stayed on the ordinary path.&lt;/p&gt;

&lt;p&gt;Only when I explicitly told the compiler to assume a single-threaded world did the reference-counting path change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;The full experiment is public:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/salari-dev/swift6-arc-atomicity-lab" rel="noopener noreferrer"&gt;https://github.com/salari-dev/swift6-arc-atomicity-lab&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The repository includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the Swift test cases;&lt;/li&gt;
&lt;li&gt;the negative concurrency control;&lt;/li&gt;
&lt;li&gt;generated SIL, LLVM IR and assembly evidence;&lt;/li&gt;
&lt;li&gt;the atomic versus non-atomic A/B;&lt;/li&gt;
&lt;li&gt;compiler-selection tracing;&lt;/li&gt;
&lt;li&gt;runtime-source mapping;&lt;/li&gt;
&lt;li&gt;claim auditing;&lt;/li&gt;
&lt;li&gt;simulator validation;&lt;/li&gt;
&lt;li&gt;reproduction scripts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To regenerate the compiler experiment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./run-experiment.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For simulator validation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./run-simulator-validation.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repository preserves the frozen evidence under &lt;code&gt;Evidence/&lt;/code&gt;, while regenerated output is written separately.&lt;/p&gt;

&lt;p&gt;The experiment currently uses one official Apple Swift toolchain. No second official Swift toolchain was installed and tested, so cross-toolchain reproduction remains unresolved.&lt;/p&gt;

&lt;p&gt;I would encourage you to run it rather than take this article on faith.&lt;/p&gt;

&lt;p&gt;Compiler behaviour is exactly the kind of thing where the interesting answer may change with a future toolchain.&lt;/p&gt;

&lt;p&gt;And if a newer Swift compiler starts lowering one of these cases differently, that would make the experiment more interesting, not less.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Swift, &lt;a href="https://docs.swift.org/main/documentation/swift/actor/" rel="noopener noreferrer"&gt;Actor&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Swift, &lt;a href="https://docs.swift.org/latest/documentation/swift/sendable/" rel="noopener noreferrer"&gt;Sendable&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Reproduction repository: &lt;a href="https://github.com/salari-dev/swift6-arc-atomicity-lab" rel="noopener noreferrer"&gt;salari-dev/swift6-arc-atomicity-lab&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;First published on &lt;a href="https://salari.dev/writing/swift-6-arc-atomicity/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=swift-6-arc-atomicity" rel="noopener noreferrer"&gt;salari.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>swift</category>
      <category>ios</category>
      <category>concurrency</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
