<?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: Muhittin Çamdalı</title>
    <description>The latest articles on DEV Community by Muhittin Çamdalı (@muhittin_camdali).</description>
    <link>https://dev.to/muhittin_camdali</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%2F3364360%2F9f4ec516-99f4-4deb-ac45-f117560278ff.png</url>
      <title>DEV Community: Muhittin Çamdalı</title>
      <link>https://dev.to/muhittin_camdali</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/muhittin_camdali"/>
    <language>en</language>
    <item>
      <title>SwiftUI Document API: ReadableDocument and WritableDocument</title>
      <dc:creator>Muhittin Çamdalı</dc:creator>
      <pubDate>Mon, 26 Oct 2026 08:53:18 +0000</pubDate>
      <link>https://dev.to/muhittin_camdali/swiftui-document-api-readabledocument-and-writabledocument-3pb6</link>
      <guid>https://dev.to/muhittin_camdali/swiftui-document-api-readabledocument-and-writabledocument-3pb6</guid>
      <description>&lt;p&gt;SwiftUI's Document API got a fundamental update with iOS 27: Apple introduced two new protocols for document-based apps, &lt;code&gt;ReadableDocument&lt;/code&gt; and &lt;code&gt;WritableDocument&lt;/code&gt;. In this post I walk through the difference between these protocols and &lt;code&gt;FileDocument&lt;/code&gt;/&lt;code&gt;ReferenceFileDocument&lt;/code&gt;, the incremental/asynchronous write mechanism, and the multi-document creation flow with &lt;code&gt;DocumentCreationSource&lt;/code&gt;, step by step, based on Apple's WWDC26 resources and an independent write-up that corroborates them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Pro Tip:&lt;/strong&gt; If you're starting a project whose new deployment target is 27+, start directly with &lt;code&gt;ReadableDocument&lt;/code&gt;/&lt;code&gt;WritableDocument&lt;/code&gt; instead of &lt;code&gt;FileDocument&lt;/code&gt; — migrating back later is far more expensive than starting there.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why the new protocols arrived
&lt;/h2&gt;

&lt;p&gt;Apple's official WWDC26 guide describes the expanded SwiftUI Document API this way: "The expanded SwiftUI Document API gives you direct control over the structure of your saved documents. For reading and writing, conform to WritableDocument and ReadableDocument…" This sentence sums up the origin point of the new protocols: the old &lt;code&gt;FileDocument&lt;/code&gt;/&lt;code&gt;ReferenceFileDocument&lt;/code&gt; pair didn't give you fine-grained control over the document's structure on disk.&lt;/p&gt;

&lt;p&gt;Apple's WWDC26 session 269 walkthrough directly backs up the same motivation: "First, the write method is nonisolated and asynchronous. This lets me perform expensive disk writing operations in the background, so the app stays responsive. I write only the parts of the package that actually need updating, by comparing the current and the previous snapshots." So the reason isn't one thing: disk-structure control, incremental writes, and non-blocking background I/O.&lt;/p&gt;

&lt;p&gt;On top of that, the new Document type is now a reference type marked &lt;code&gt;@Observable&lt;/code&gt;. In onmyway133's words: "The document is a reference type marked &lt;a class="mentioned-user" href="https://dev.to/observable"&gt;@observable&lt;/a&gt;, so SwiftUI does not recreate it on every change."&lt;/p&gt;

&lt;p&gt;The WWDC26 guide page doesn't quote &lt;code&gt;FileDocument&lt;/code&gt; directly, but Apple's session 269 summary explicitly mentions this protocol: "Expanded document APIs for SwiftUI apps, including the new DocumentCreationSource API for custom new-document flows, performance improvements for reading and writing large documents, and first-class support for direct document URL access via the FileDocument and ReferenceFileDocument protocols." onmyway133's write-up summarizes this more sharply: "two new protocols replace FileDocument and ReferenceFileDocument for new code." So the new protocols are a pair that replaces the old ones for newly written code — a distinction that matters later (specifically, in the iOS 26 compatibility section).&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating FileDocument → ReadableDocument (a side-by-side example)
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;StickerDocument&lt;/code&gt; definition below, based on the WWDC26 session's example, shows what conforming to &lt;code&gt;WritableDocument&lt;/code&gt; looks like.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ReadableDocument&lt;/code&gt; is defined in the WWDC26 session like this: "ReadableDocument is a twin to WritableDocument. Here's how they compare. Each protocol requires a list of supported content types. WritableDocument provides a snapshot and ReadableDocument knows how to apply it." The same session clarifies disk access this way: "ReadableDocument's friend is DocumentReader, which does all the disk-related heavy lifting." So it works on a twin-protocol model, and applying snapshots read from disk back onto the document is handled through a &lt;code&gt;DocumentReader&lt;/code&gt; that owns that job.&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;StickerDocument&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;WritableDocument&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;writableContentTypes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;UTType&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stickerDocument&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="kd"&gt;@MainActor&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UTType&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="n"&gt;sending&lt;/span&gt; &lt;span class="kt"&gt;PageSnapshot&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// encode current state into a PageSnapshot&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;writer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sending&lt;/span&gt; &lt;span class="kt"&gt;WriteConfiguration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sending&lt;/span&gt; &lt;span class="kt"&gt;Writer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// return a Writer that knows how to persist a PageSnapshot&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 &lt;code&gt;@MainActor&lt;/code&gt; marking on the definition above shows that taking a snapshot runs on the main thread; the &lt;code&gt;nonisolated&lt;/code&gt; marking you'll see on the write side further down shows the opposite — that operation can run without being tied to any actor.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the old FileDocument side used to look like
&lt;/h3&gt;

&lt;p&gt;To complete this "side-by-side example" comparison, the same &lt;code&gt;StickerDocument&lt;/code&gt; type's conformance to the old &lt;code&gt;FileDocument&lt;/code&gt; protocol used to look like this — a struct that declares &lt;code&gt;readableContentTypes&lt;/code&gt;, does reading in a single pass inside &lt;code&gt;init(configuration:)&lt;/code&gt;, and writing in a single pass inside &lt;code&gt;fileWrapper(configuration:)&lt;/code&gt;:&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;struct&lt;/span&gt; &lt;span class="kt"&gt;StickerDocument&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;FileDocument&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;readableContentTypes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;UTType&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stickerDocument&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&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;configuration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;ReadConfiguration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throws&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// decode the data inside configuration.file (a FileWrapper) into the struct in one pass&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;fileWrapper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;WriteConfiguration&lt;/span&gt;&lt;span class="p"&gt;)&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;FileWrapper&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// encode the struct's current state into a new FileWrapper in one pass&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;Side by side with the &lt;code&gt;WritableDocument&lt;/code&gt; example above, the difference is clear: the old side runs &lt;code&gt;init(configuration:)&lt;/code&gt; and &lt;code&gt;fileWrapper(configuration:)&lt;/code&gt; synchronously in one piece, while the new side prepares &lt;code&gt;snapshot(contentType:)&lt;/code&gt; on &lt;code&gt;@MainActor&lt;/code&gt; and pushes the actual disk write through a separate, &lt;code&gt;nonisolated&lt;/code&gt; &lt;code&gt;writer&lt;/code&gt; (shown further down).&lt;/p&gt;

&lt;h3&gt;
  
  
  The role of PageSnapshot and DocumentReader
&lt;/h3&gt;

&lt;p&gt;In the WWDC26 session 269 example, the type returned by &lt;code&gt;snapshot(contentType:)&lt;/code&gt; above is concretely defined as: &lt;code&gt;struct PageSnapshot { var background: Image; var metadata: StickerPlacements; var stickers: [Image] }&lt;/code&gt;. So the snapshot freezes the document's state — background image, &lt;code&gt;metadata&lt;/code&gt; with sticker positions, and the sticker images — into one value type; &lt;code&gt;ReadableDocument&lt;/code&gt;'s &lt;code&gt;apply(snapshot:previous:)&lt;/code&gt; applies this &lt;code&gt;PageSnapshot&lt;/code&gt;, read back from disk, onto the document object.&lt;/p&gt;

&lt;p&gt;In the same session, &lt;code&gt;StickerDocument&lt;/code&gt;'s conformance to &lt;code&gt;ReadableDocument&lt;/code&gt; is given as a separate &lt;code&gt;extension&lt;/code&gt;: &lt;code&gt;extension StickerDocument: ReadableDocument {}&lt;/code&gt;. This shows that &lt;code&gt;WritableDocument&lt;/code&gt; and &lt;code&gt;ReadableDocument&lt;/code&gt; can be defined as two separate conformance blocks on the same class — meaning reading and writing responsibilities can be separated in the code itself as well.&lt;/p&gt;

&lt;p&gt;The real difference in a practical migration: &lt;code&gt;FileDocument&lt;/code&gt; was a value type (struct), so every read produced a new copy; on &lt;code&gt;ReadableDocument&lt;/code&gt;, you're applying a snapshot to a reference-type, &lt;code&gt;@Observable&lt;/code&gt; document object instead. Migration isn't "change the protocol signature" — it's moving the document model from value to reference.&lt;/p&gt;

&lt;p&gt;The most concrete consequence of this shift shows up in the SwiftUI view layer. onmyway133's statement — "so SwiftUI does not recreate it on every change, and a TextEditor bound to a document property does not lose its model on each keystroke" — shows this directly: once &lt;code&gt;ReadableDocument&lt;/code&gt; moves to a reference type, the document object's identity is preserved even when the view redraws, so a &lt;code&gt;TextEditor&lt;/code&gt; bound to the document property doesn't lose its model on every keystroke.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;FileDocument (old)&lt;/th&gt;
&lt;th&gt;ReadableDocument (new)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Type nature&lt;/td&gt;
&lt;td&gt;Value (struct)&lt;/td&gt;
&lt;td&gt;Reference, &lt;code&gt;@Observable&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read responsibility&lt;/td&gt;
&lt;td&gt;In one pass, inside &lt;code&gt;init(configuration:)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Applying a snapshot via &lt;code&gt;DocumentReader&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recreation on change&lt;/td&gt;
&lt;td&gt;New struct copy on every edit&lt;/td&gt;
&lt;td&gt;Object identity preserved, not recreated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content type declaration&lt;/td&gt;
&lt;td&gt;&lt;code&gt;static var readableContentTypes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;readableContentTypes&lt;/code&gt;, &lt;code&gt;reader(configuration:)&lt;/code&gt;, &lt;code&gt;apply(snapshot:previous:)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  ReferenceFileDocument → WritableDocument
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ReferenceFileDocument&lt;/code&gt; was already a reference type, so moving to &lt;code&gt;WritableDocument&lt;/code&gt; is conceptually less of a leap than the &lt;code&gt;ReadableDocument&lt;/code&gt; migration.&lt;/p&gt;

&lt;p&gt;onmyway133's point brings clarity here: "You convert between your data and disk through a snapshot, using either the FileWrapperDocumentReader and FileWrapperDocumentWriter convenience types or a fully custom reader and writer for streaming or direct URL access." So on the &lt;code&gt;WritableDocument&lt;/code&gt; side, the data conversion now happens through a &lt;code&gt;snapshot&lt;/code&gt;, either via a concrete convenience type like &lt;code&gt;FileWrapperDocumentWriter&lt;/code&gt; or through a fully custom reader/writer — conceptually corresponding to &lt;code&gt;ReferenceFileDocument&lt;/code&gt;'s &lt;code&gt;snapshot(contentType:)&lt;/code&gt; + &lt;code&gt;fileWrapper(snapshot:configuration:)&lt;/code&gt; pair, but with disk access now extended with streaming/direct URL support.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;ReferenceFileDocument (old)&lt;/th&gt;
&lt;th&gt;WritableDocument (new)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Type nature&lt;/td&gt;
&lt;td&gt;Reference (class), &lt;code&gt;ObservableObject&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Reference, &lt;code&gt;@Observable&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write helper type&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fileWrapper(snapshot:configuration:)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;FileWrapperDocumentWriter&lt;/code&gt; / &lt;code&gt;DocumentWriter&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disk access&lt;/td&gt;
&lt;td&gt;Single-shot &lt;code&gt;FileWrapper&lt;/code&gt; production&lt;/td&gt;
&lt;td&gt;Streaming or direct URL support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Progress reporting&lt;/td&gt;
&lt;td&gt;Not specified&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Subprogress&lt;/code&gt; parameter&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  A concrete Writer type
&lt;/h3&gt;

&lt;p&gt;WWDC26 session 269 gives a concrete writer type that conforms to &lt;code&gt;DocumentWriter&lt;/code&gt; like this: &lt;code&gt;struct Writer&amp;lt;Snapshot&amp;gt;: DocumentWriter { typealias Snapshot = PageSnapshot; let contentType: UTType }&lt;/code&gt;. So &lt;code&gt;Writer&lt;/code&gt; is a generic helper type that knows which &lt;code&gt;PageSnapshot&lt;/code&gt; to write for which &lt;code&gt;contentType&lt;/code&gt;; the &lt;code&gt;WritableDocument.writer(configuration:)&lt;/code&gt; method returns an instance of this type every time it's called.&lt;/p&gt;

&lt;h3&gt;
  
  
  Exporting the same document as PNG too
&lt;/h3&gt;

&lt;p&gt;In the same session, the content types &lt;code&gt;Writer&lt;/code&gt; supports aren't limited to a single format. Around 14:35, &lt;code&gt;.png&lt;/code&gt; is added to the &lt;code&gt;writableContentTypes&lt;/code&gt; list; at 14:48, the writer branches on it like this: &lt;code&gt;if contentType.conforms(to: .stickerDocument) { /* write the .stickerDocument package */ } else if contentType.conforms(to: .png) { /* write the flattened image for .png */ }&lt;/code&gt;. So the same &lt;code&gt;Writer&lt;/code&gt;, by checking the &lt;code&gt;contentType&lt;/code&gt; parameter, can produce either the document's own package format (&lt;code&gt;.stickerDocument&lt;/code&gt;) or a flattened PNG output — this is the branch that kicks in for exporting the page visually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Incremental and asynchronous writing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Computing the diff with the previous parameter
&lt;/h3&gt;

&lt;p&gt;The signature in the WWDC26 session's code example shows directly how incremental writing works: &lt;code&gt;DocumentWriter.write(snapshot:to:previous:progress:)&lt;/code&gt;. The &lt;code&gt;previous&lt;/code&gt; parameter here carries the prior state into the writer; this lets the writer write only the part that changed, not the whole document.&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;nonisolated&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sending&lt;/span&gt; &lt;span class="kt"&gt;PageSnapshot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="nv"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sending&lt;/span&gt; &lt;span class="kt"&gt;PageSnapshot&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt;
    &lt;span class="nv"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;consuming&lt;/span&gt; &lt;span class="kt"&gt;Subprogress&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="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// write .stickerDocument&lt;/span&gt;
    &lt;span class="c1"&gt;// previous == nil  → first save, full write&lt;/span&gt;
    &lt;span class="c1"&gt;// previous != nil  → only the diff is written (incremental write)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apple's session 269 walkthrough directly confirms this: the write compares current and previous snapshots and applies only to the package parts that actually need updating, running in the background without blocking the main thread — so incremental writing isn't just "write less data," it's also "don't keep the UI thread busy."&lt;/p&gt;

&lt;p&gt;In practice: in a large document (say, a drawing file with dozens of pages), every &lt;code&gt;Cmd+S&lt;/code&gt; now writes to disk only the parts that changed since the last operation, not the entire document.&lt;/p&gt;

&lt;h2&gt;
  
  
  Progress reporting with Subprogress
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;progress: consuming Subprogress&lt;/code&gt; parameter you saw in the signature above is Foundation's progress-reporting API integrated directly into the SwiftUI Document layer. The WWDC26 guide summarizes it this way: "offer asynchronous, incremental disk operations and progress reporting via the Foundation Subprogress API." onmyway133 confirms the same point: "Packages can be read and written incrementally, and progress flows through Subprogress." — two independent sources agree here.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;consuming&lt;/code&gt; keyword shows that the &lt;code&gt;Subprogress&lt;/code&gt; value is handed over (ownership transfer) to the &lt;code&gt;write&lt;/code&gt; call — meaning the writer reports the progress of its own sub-operations back up to the caller through this single parameter. This means that when you want to show the user something like "42% saved" while saving a large document, you don't need to manually synchronize your own progress counters; the framework carries that for you.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the consuming keyword means
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The result from the signature (verified in WWDC26): consuming Subprogress&lt;/span&gt;
&lt;span class="c1"&gt;// "consuming" → the value is handed over to the write() call, ownership never returns.&lt;/span&gt;
&lt;span class="c1"&gt;// Practical effect: you don't need to update progress by hand,&lt;/span&gt;
&lt;span class="c1"&gt;// DocumentWriter reports its own sub-operations' progress through this parameter.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  DocumentCreationSource and NewDocumentButton
&lt;/h2&gt;

&lt;p&gt;The WWDC26 guide defines this feature in one clear sentence: "The DocumentCreationSource API lets you declare multiple creation sources with a NewDocumentButton for each." So instead of one "New Document" button, an app can now define multiple buttons, each for a different starting state (blank page, template, import from photo, and so on).&lt;/p&gt;

&lt;p&gt;The literal call from the WWDC26 session's code example is this:&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;extension&lt;/span&gt; &lt;span class="kt"&gt;DocumentCreationSource&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;blank&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"blank"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;photo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"photo"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;DocumentGroupLaunchScene&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Create a Sticker Page"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;NewDocumentButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"New Sticker Page"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;blank&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kt"&gt;NewDocumentButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Sticker Page from Photo…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;photo&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;h3&gt;
  
  
  Adding multiple sources to the scene
&lt;/h3&gt;

&lt;p&gt;The pattern is clear: each source is a static &lt;code&gt;DocumentCreationSource&lt;/code&gt; value initialized with an &lt;code&gt;id&lt;/code&gt; string, and &lt;code&gt;NewDocumentButton&lt;/code&gt; takes a title string plus this value. The buttons' layout in the scene is just as clear: multiple &lt;code&gt;NewDocumentButton&lt;/code&gt;s are defined one after another inside &lt;code&gt;DocumentGroupLaunchScene&lt;/code&gt;, each carrying its own &lt;code&gt;source&lt;/code&gt;. In practice: a drawing app with two flows like "Blank Page" and "Start from Photo" means two separate &lt;code&gt;NewDocumentButton&lt;/code&gt; lines, each with a different &lt;code&gt;source&lt;/code&gt; value.&lt;/p&gt;

&lt;h2&gt;
  
  
  iCloud/Files integration pitfalls
&lt;/h2&gt;

&lt;p&gt;There's a broadly applicable pitfall to keep in mind when thinking about iCloud or Files syncing, confirmed in onmyway133's write-up: "One thing to remember is that SwiftUI tracks unsaved changes through undo actions, so without registered undo actions it will not autosave." In other words, SwiftUI tracks unsaved changes through undo actions. If a change hasn't been registered on the undo stack, autosave isn't triggered.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why autosave doesn't trigger without a registered undo action
&lt;/h3&gt;

&lt;p&gt;The practical consequence of this is: if you mutate your document by directly assigning to an &lt;code&gt;@Observable&lt;/code&gt; property (without registering an undo action), SwiftUI doesn't mark that change as "unsaved" and won't autosave it — the file stays in its old state on disk (and therefore in iCloud sync too). That's why every mutation needs to go through a registered action via &lt;code&gt;UndoManager&lt;/code&gt;; otherwise the change appears to the user as if it's "lost," when in fact it was never written at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  iOS 26 backward compatibility
&lt;/h2&gt;

&lt;p&gt;Apple's canonical documentation data is clear: the new protocols ship only with iOS 27.0, iPadOS 27.0, Mac Catalyst 27.0, macOS 27.0, and visionOS 27.0 (per the platform records in readabledocument.json and writabledocument.json) — not backported to iOS 26. The old &lt;code&gt;FileDocument&lt;/code&gt; and &lt;code&gt;ReferenceFileDocument&lt;/code&gt; have existed since iOS 14.0 / macOS 11.0; as of publish date, a mixed deployment target (one also covering below 27) has to stay on these protocols, since the new ones don't run there.&lt;/p&gt;

&lt;p&gt;The consequence is this: if your deployment target is 27+, onmyway133's write-up gives clear advice — "If you are starting a document app today and your deployment target is 27 or later, reach for these rather than the older protocols." With a mixed deployment target (iOS 26+27), staying on the old API isn't a preference, it's a requirement — the new protocols don't exist on iOS 26.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to do with a mixed deployment target
&lt;/h3&gt;

&lt;p&gt;This advice needs to be read together with the onmyway133-sourced "replace ... for new code" statement from the "Why the new protocols arrived" section above: the new protocols replace the old ones for newly written code, but since the old ones are still usable as of publish date, an app with a mixed deployment target can host both protocol families at once — one for the 27+ flow, the other for backward compatibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  🏆 GOLDEN TIP: Don't make the migration a "big bang"
&lt;/h2&gt;

&lt;p&gt;The biggest mistake when moving to the new protocols is changing the entire document model in one commit. Instead, move the read-only side to &lt;code&gt;ReadableDocument&lt;/code&gt; first and leave writing on the old &lt;code&gt;fileWrapper&lt;/code&gt; path a while longer — you can test the two sides separately. Verify the undo-based autosave behavior (the pitfall above) only after moving the write side, with real user flows — it never shows up on the read side, and the surprise usually hits on the first real "save." I prefer starting with the smallest, least critical document type, so you learn the framework's async/incremental behavior in a low-risk module, not in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;p&gt;If you've read this far, you've earned an ordered checklist you can follow when moving to the new Document API. The items below gather the sourced pitfalls and steps from this post into a single sequence — check each one off before you start the migration.&lt;/p&gt;

&lt;p&gt;Confirm your deployment target is genuinely 27 or later&lt;br&gt;
Determine whether your document model is a value type or a reference type&lt;br&gt;
Move the ReadableDocument side first, leave WritableDocument for the second step&lt;br&gt;
Write every mutation as a registered action through UndoManager&lt;br&gt;
Test autosave behavior only after you've registered undo actions&lt;br&gt;
For large documents, log and verify that the previous parameter arrives populated&lt;br&gt;
Print Subprogress progress to the console before wiring it to a real UI indicator&lt;br&gt;
If you use multiple NewDocumentButtons, test each one's source value separately&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What replaces FileDocument now?
&lt;/h3&gt;

&lt;p&gt;With iOS, macOS, and visionOS 27, Apple introduced the &lt;code&gt;ReadableDocument&lt;/code&gt; and &lt;code&gt;WritableDocument&lt;/code&gt; protocols for document-based SwiftUI apps. In onmyway133's words, these two protocols replace the old &lt;code&gt;FileDocument&lt;/code&gt;/&lt;code&gt;ReferenceFileDocument&lt;/code&gt; pair ("two new protocols replace FileDocument and ReferenceFileDocument for new code.") and are positioned as the recommended approach for new code.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the difference between ReadableDocument and WritableDocument?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ReadableDocument&lt;/code&gt; holds the list of supported content types and knows how to apply snapshots read from disk onto the document; this work is carried out via a &lt;code&gt;DocumentReader&lt;/code&gt;. &lt;code&gt;WritableDocument&lt;/code&gt; provides the list of writable formats and a &lt;code&gt;snapshot&lt;/code&gt; mechanism that returns the current content, through a writer type that conforms to &lt;code&gt;DocumentWriter&lt;/code&gt; — twin protocols, one carrying the reading responsibility, the other the saving responsibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I incrementally save a large file in SwiftUI?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;WritableDocument&lt;/code&gt;'s writer takes the prior state via &lt;code&gt;previous&lt;/code&gt; in &lt;code&gt;write(snapshot:to:previous:progress:)&lt;/code&gt; and writes only the changed part. The same call reports progress via &lt;code&gt;progress: consuming Subprogress&lt;/code&gt; — so instead of rewriting large documents from scratch, you save them piece by piece, asynchronously.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you define multiple new-document sources in a DocumentGroup?
&lt;/h3&gt;

&lt;p&gt;You define a &lt;code&gt;DocumentCreationSource&lt;/code&gt; value for each different starting state and pass it to a &lt;code&gt;NewDocumentButton("Title", source: source)&lt;/code&gt; call. The verified call from the WWDC26 code example looks like &lt;code&gt;NewDocumentButton("New Sticker Page", source: .blank)&lt;/code&gt;; you can reuse the same button type with different &lt;code&gt;source&lt;/code&gt; values to offer multiple creation options inside a &lt;code&gt;DocumentGroupLaunchScene&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens if I don't register an undo action?
&lt;/h3&gt;

&lt;p&gt;Because SwiftUI tracks unsaved changes through the undo stack, autosave isn't triggered unless a mutation was made as an action registered with &lt;code&gt;UndoManager&lt;/code&gt;. This can leave the document looking changed in the UI while staying in its old state on disk — which is why every edit needs to happen inside an undo-aware action.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need to change my old FileDocument code right away?
&lt;/h3&gt;

&lt;p&gt;Not in the short term. &lt;code&gt;FileDocument&lt;/code&gt; and &lt;code&gt;ReferenceFileDocument&lt;/code&gt; have been usable since iOS 14.0 / macOS 11.0. The new pair ships only with iOS/macOS/visionOS 27.0+, not backported to iOS 26. If your target also covers below 27, staying on the old API is required — the new protocols don't run there. See '## Update (September 2026)' below for a post-publish development on these protocols' status.&lt;/p&gt;

&lt;h2&gt;
  
  
  Update (September 2026)
&lt;/h2&gt;

&lt;p&gt;After this post's publish date (June 19, 2026), a real change landed in Apple's canonical documentation data: &lt;code&gt;FileDocument&lt;/code&gt; and &lt;code&gt;ReferenceFileDocument&lt;/code&gt; were marked deprecated as of 27.2. The warning in &lt;code&gt;FileDocument&lt;/code&gt;'s records reads: "Conform your type to Document instead." &lt;code&gt;ReferenceFileDocument&lt;/code&gt;'s reads: "Use Document protocol instead." Both protocols had existed since iOS/iPadOS/Mac Catalyst 14.0, macOS 11.0; the deprecation only arrived with 27.2, and isn't a retroactive behavior change.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Document&lt;/code&gt; protocol Apple points to inherits both &lt;code&gt;ReadableDocument&lt;/code&gt; and &lt;code&gt;WritableDocument&lt;/code&gt;: "Inherits From: ReadableDocument, WritableDocument", with the abstract "A document that supports both reading and writing." (iOS/macOS/visionOS 27.0). So the twin-protocol model this post describes hasn't changed; &lt;code&gt;Document&lt;/code&gt; was added on top as a third protocol unifying the two under one name. Read this together with the earlier '## iOS 26 backward compatibility' section: if your deployment target also covers below 27, staying on the old protocols is still required — but it's now "deprecated, yet currently the only option," not "not deprecated."&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ReadableDocument&lt;/code&gt; and &lt;code&gt;WritableDocument&lt;/code&gt; are among the most concrete infrastructure updates SwiftUI has shipped for document-based apps: a reference-type, &lt;code&gt;@Observable&lt;/code&gt; document model, incremental/asynchronous writing that reports progress via &lt;code&gt;Subprogress&lt;/code&gt;, and multiple document-creation sources via &lt;code&gt;DocumentCreationSource&lt;/code&gt;. But this doesn't have to be a "big bang" migration: the new protocols replace the old ones for newly written code, but &lt;code&gt;FileDocument&lt;/code&gt;/&lt;code&gt;ReferenceFileDocument&lt;/code&gt; are still required for deployment targets that cover below iOS 27; migrating only becomes a choice once your deployment target is 27+ (see the Update section above).&lt;/p&gt;

&lt;p&gt;To refresh SwiftUI's state management before moving your document model to a reference type, see &lt;a href="https://muhittincamdali.com/blog/swiftui-property-wrappers-deep-dive/" rel="noopener noreferrer"&gt;my deep dive into SwiftUI property wrappers&lt;/a&gt;. Planning a document-based macOS app? &lt;a href="https://muhittincamdali.com/blog/macos-swiftui-desktop-development/" rel="noopener noreferrer"&gt;The guide to desktop development with SwiftUI on macOS&lt;/a&gt; is a natural next step. Curious about other iOS 27 breaking changes? See &lt;a href="https://muhittincamdali.com/en/blog/ios-27-state-macro-contentbuilder-kirilanlar/" rel="noopener noreferrer"&gt;the post on what broke in the State macro and ContentBuilder&lt;/a&gt;. If you're splitting your document model into components, &lt;a href="https://muhittincamdali.com/blog/swiftui-custom-components-library/" rel="noopener noreferrer"&gt;the custom component library guide&lt;/a&gt; and, for large-document performance, &lt;a href="https://muhittincamdali.com/blog/swiftui-performance-optimization/" rel="noopener noreferrer"&gt;the performance optimization post&lt;/a&gt; are useful references too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developer.apple.com/wwdc26/guides/swiftui/" rel="noopener noreferrer"&gt;WWDC26 SwiftUI guide&lt;/a&gt; — Apple's official guide; the primary source for ReadableDocument/WritableDocument, Subprogress, and DocumentCreationSource.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://onmyway133.com/posts/whats-new-in-swiftui-in-ios-27" rel="noopener noreferrer"&gt;What's new in SwiftUI in iOS 27 (onmyway133)&lt;/a&gt; — an independent write-up dated June 9, 2026; a second source confirming most items, including the undo-based autosave pitfall.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.apple.com/videos/play/wwdc2026/269" rel="noopener noreferrer"&gt;WWDC26 "What's new in SwiftUI" session video&lt;/a&gt; — the session where the DocumentWriter signature and the NewDocumentButton code example appear.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.apple.com/documentation/swiftui/readabledocument" rel="noopener noreferrer"&gt;ReadableDocument documentation&lt;/a&gt; — the protocol's official reference page.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.apple.com/documentation/swiftui/writabledocument" rel="noopener noreferrer"&gt;WritableDocument documentation&lt;/a&gt; — the protocol's official reference page.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.apple.com/documentation/swiftui/filedocument" rel="noopener noreferrer"&gt;FileDocument documentation&lt;/a&gt; — the old protocol's canonical reference page.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.apple.com/documentation/swiftui/referencefiledocument" rel="noopener noreferrer"&gt;ReferenceFileDocument documentation&lt;/a&gt; — the old reference-type protocol's canonical page.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.apple.com/documentation/swiftui/document" rel="noopener noreferrer"&gt;Document protocol documentation&lt;/a&gt; — the recommended replacement for FileDocument/ReferenceFileDocument, inheriting ReadableDocument+WritableDocument.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.apple.com/documentation/swiftui/documentgroup" rel="noopener noreferrer"&gt;DocumentGroup documentation&lt;/a&gt; — the canonical type DocumentCreationSource and NewDocumentButton attach to.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.apple.com/documentation/foundation/subprogress" rel="noopener noreferrer"&gt;Foundation Subprogress documentation&lt;/a&gt; — the Foundation API used for progress reporting.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://muhittincamdali.com/en/blog/swiftui-document-api-readable-writable/" rel="noopener noreferrer"&gt;muhittincamdali.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>swift</category>
      <category>ios</category>
      <category>swiftui</category>
      <category>mobile</category>
    </item>
    <item>
      <title>App Intents on iOS 27: LongRunningIntent and Siri</title>
      <dc:creator>Muhittin Çamdalı</dc:creator>
      <pubDate>Mon, 26 Oct 2026 08:51:57 +0000</pubDate>
      <link>https://dev.to/muhittin_camdali/app-intents-on-ios-27-longrunningintent-and-siri-4ogb</link>
      <guid>https://dev.to/muhittin_camdali/app-intents-on-ios-27-longrunningintent-and-siri-4ogb</guid>
      <description>&lt;p&gt;iOS 27 brought a major update to the App Intents framework in WWDC26's Session 345: new APIs like &lt;code&gt;LongRunningIntent&lt;/code&gt;, &lt;code&gt;CancellableIntent&lt;/code&gt;, &lt;code&gt;EntityCollection&lt;/code&gt;, and &lt;code&gt;SyncableEntity&lt;/code&gt; now make Siri integrations both more resilient and consistent across devices. In this guide I walk through iOS 27 App Intents LongRunningIntent SyncableEntity Siri integration, drawing on WWDC26 Session 345 and Apple's official API documentation, showing what actually changed as of the beta period (June 2026).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Pro Tip:&lt;/strong&gt; When migrating an existing &lt;code&gt;AppIntent&lt;/code&gt; to &lt;code&gt;LongRunningIntent&lt;/code&gt;, first wrap your &lt;code&gt;perform()&lt;/code&gt; body in a &lt;code&gt;performBackgroundTask&lt;/code&gt; block — testing that refactor on its own before adding the protocol makes it much easier to tell which error came from which change.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why App Intents matters again on iOS 27
&lt;/h2&gt;

&lt;p&gt;The core promise of App Intents hasn't changed: making your app's content and actions discoverable for system experiences like Apple Intelligence, Siri, Spotlight, Shortcuts, and widgets. WWDC26 Session 345 covers the additions from what it calls "our 2027 releases" under eight headings; the documentation lists the same APIs under the "iOS 27.0" tag. I'm focusing on three main axes: long-running work finishing without interruption, no performance loss on large data sets, and entities staying consistent across devices — mapped to &lt;code&gt;LongRunningIntent&lt;/code&gt;, &lt;code&gt;EntityCollection&lt;/code&gt;, and &lt;code&gt;SyncableEntity&lt;/code&gt; respectively; together with &lt;code&gt;CancellableIntent&lt;/code&gt; and &lt;code&gt;RelevantEntities&lt;/code&gt; I cover these as five core APIs, and the final section is devoted to &lt;code&gt;ExecutionTargets&lt;/code&gt; and &lt;code&gt;@UnionValue&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you haven't touched App Intents fundamentals yet, I'd recommend reading &lt;a href="https://muhittincamdali.com/blog/app-intents-shortcuts/" rel="noopener noreferrer"&gt;App Intents and Shortcuts integration&lt;/a&gt; first — this guide doesn't repeat those fundamentals, it focuses directly on the iOS 27 delta.&lt;/p&gt;

&lt;h2&gt;
  
  
  Breaking the 30-second wall with LongRunningIntent
&lt;/h2&gt;

&lt;p&gt;According to Apple's official documentation, &lt;code&gt;LongRunningIntent&lt;/code&gt; is "an interface you use to extend the background execution time of an app intent that performs a long-running task." The underlying problem: the system traditionally gives a normal intent only up to 30 seconds of run time ("the system traditionally gives it up to 30 seconds to finish its task"). Work like batch photo processing, large file transfers, or a multi-step automation may not finish in that window — in the session, Apple's team describes it exactly as "the intent kept failing because it couldn't finish within the 30-second limit."&lt;/p&gt;

&lt;p&gt;On the Swift side, &lt;code&gt;LongRunningIntent&lt;/code&gt; is a protocol that extends &lt;code&gt;ProgressReportingIntent&lt;/code&gt; (&lt;code&gt;protocol LongRunningIntent : ProgressReportingIntent&lt;/code&gt;). &lt;code&gt;ProgressReportingIntent&lt;/code&gt; isn't actually a new API — it's been around since iOS 17.0; iOS 27's contribution is combining that progress-reporting infrastructure with &lt;code&gt;LongRunningIntent&lt;/code&gt;, which extends background run time.&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;import&lt;/span&gt; &lt;span class="kt"&gt;AppIntents&lt;/span&gt;

&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;SyncPhotoLibraryIntent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;LongRunningIntent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;LocalizedStringResource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Fotoğrafları Senkronize Et"&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;perform&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="kd"&gt;some&lt;/span&gt; &lt;span class="kt"&gt;IntentResult&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="kt"&gt;ReturnsValue&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="o"&gt;&amp;gt;&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;batches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;loadBatches&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;summary&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;performBackgroundTask&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;progress&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;totalUnitCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Int64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batches&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;batches&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enumerated&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&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;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;syncBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;progress&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completedUnitCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Int64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&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;"&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;batches&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt; grup senkronize edildi"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;result&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="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;)&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;loadBatches&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&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="p"&gt;[]&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;syncBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;:&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="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;throws&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;While the work is running, the system automatically shows a Live Activity as progress reports come in, and the user can stop the work from that Live Activity; what's new in iOS 27 is that this Live Activity is triggered automatically by App Intents. If you want to see the mechanism itself in more detail, you can check out &lt;a href="https://muhittincamdali.com/blog/interactive-widgets-ios17/" rel="noopener noreferrer"&gt;interactive widgets and Live Activity&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing progress to Live Activity: using the progress object correctly
&lt;/h2&gt;

&lt;p&gt;The most practical thing about &lt;code&gt;LongRunningIntent&lt;/code&gt; is that you don't need a manual &lt;code&gt;ActivityKit&lt;/code&gt; implementation. What makes that possible is the &lt;code&gt;progress&lt;/code&gt; object inherited from &lt;code&gt;ProgressReportingIntent&lt;/code&gt;: update &lt;code&gt;totalUnitCount&lt;/code&gt; and &lt;code&gt;completedUnitCount&lt;/code&gt;, and the system reflects those values in the Live Activity automatically.&lt;/p&gt;

&lt;p&gt;Watch how often you update &lt;code&gt;progress&lt;/code&gt;. Instead of updating after every single record, update at meaningful stage boundaries (e.g. after each batch) — this avoids needless redraws and gives the user a smoother progress experience:&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;import&lt;/span&gt; &lt;span class="kt"&gt;AppIntents&lt;/span&gt;

&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;ProcessRecordsIntent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;LongRunningIntent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;LocalizedStringResource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Kayıtları İşle"&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;perform&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="kd"&gt;some&lt;/span&gt; &lt;span class="kt"&gt;IntentResult&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="kt"&gt;ReturnsValue&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;Int&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="mi"&gt;250&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;processedCount&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;performBackgroundTask&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;progress&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;totalUnitCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Int64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&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;processed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;stride&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;)&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;chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;..&amp;lt;&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&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;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="n"&gt;processed&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;
                &lt;span class="c1"&gt;// Update every 25 records, not every record.&lt;/span&gt;
                &lt;span class="n"&gt;progress&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completedUnitCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Int64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;processed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;processed&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;result&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="n"&gt;processedCount&lt;/span&gt;&lt;span class="p"&gt;)&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;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;:&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="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;throws&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;When the work is cancelled, the &lt;code&gt;onCancel&lt;/code&gt; block I'll cover in the &lt;code&gt;CancellableIntent&lt;/code&gt; section takes over.&lt;/p&gt;

&lt;h2&gt;
  
  
  CancellableIntent: the natural partner of iOS 26.4, not iOS 27
&lt;/h2&gt;

&lt;p&gt;According to Apple's official API metadata, &lt;code&gt;CancellableIntent&lt;/code&gt; was introduced across all platforms not with iOS 27, but with &lt;strong&gt;iOS 26.4&lt;/strong&gt;. It resurfaced in WWDC26 Session 345 as the natural complement to &lt;code&gt;LongRunningIntent&lt;/code&gt; — a long-running task also needed to become cancellable.&lt;/p&gt;

&lt;p&gt;The official definition is: "An interface to support the graceful cancellation of your app intent's task." You use it by wrapping the main body of the work with the &lt;code&gt;withIntentCancellationHandler(operation:onCancel:isolation:)&lt;/code&gt; method. The method's &lt;code&gt;onCancel&lt;/code&gt; parameter is of type &lt;code&gt;(IntentCancellationReason) -&amp;gt; Void&lt;/code&gt;, meaning it hands you the cancellation reason directly:&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;import&lt;/span&gt; &lt;span class="kt"&gt;AppIntents&lt;/span&gt;

&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;SyncLibraryIntent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;AppIntent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;ProgressReportingIntent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;CancellableIntent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;LocalizedStringResource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Kütüphaneyi Senkronize Et"&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;perform&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="kd"&gt;some&lt;/span&gt; &lt;span class="kt"&gt;IntentResult&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="kt"&gt;ProvidesDialog&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;batchCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&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;withIntentCancellationHandler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;progress&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;totalUnitCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Int64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batchCount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;batchCount&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;syncBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;progress&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completedUnitCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Int64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;dialog&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;batchCount&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt; grup senkronize edildi"&lt;/span&gt;&lt;span class="p"&gt;)&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;reason&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="c1"&gt;// Clean up based on the reason.&lt;/span&gt;
            &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;cleanupPartialSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"timeout"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;userCancelled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;cleanupPartialSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"user_cancelled"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;cleanupPartialSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"unknown"&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;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;syncBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;index&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="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;throws&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;cleanupPartialSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;reason&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Per the WWDC26 session, cancellation can come from three different sources: "whether the person tapped cancel, the system timed out or needed to reclaim resources." You don't need to keep a separate tracker in your own state to tell the reason apart; as the documentation puts it, "If you want to know the reason for cancellation, use the withIntentCancellationHandler(operation:onCancel:isolation:) method that this protocol offers instead." The &lt;code&gt;IntentCancellationReason&lt;/code&gt; value passed to the handler carries that information, and you can distinguish between &lt;code&gt;.timeout&lt;/code&gt; and &lt;code&gt;.userCancelled&lt;/code&gt; as shown above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling large data sets with EntityCollection
&lt;/h2&gt;

&lt;p&gt;When you use a normal &lt;code&gt;@Parameter&lt;/code&gt; array, the system fully resolves every entity before the intent runs — calling the relevant &lt;code&gt;query&lt;/code&gt; and filling in all of its properties. With hundreds or thousands of records, that creates a noticeable delay.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;EntityCollection&lt;/code&gt; is "an array of entity identifiers that you use to improve the efficiency of operations involving large numbers of entities," per Apple's docs. Its signature is &lt;code&gt;struct EntityCollection&amp;lt;Entity&amp;gt; where Entity : AppEntity&lt;/code&gt;, arriving with iOS 27.0. Switch the parameter type to this, and the system only carries the entity identifiers, leaving the full resolve to the intent itself:&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;struct&lt;/span&gt; &lt;span class="kt"&gt;BatchTagPhotosIntent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;AppIntent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;LocalizedStringResource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Fotoğrafları Etiketle"&lt;/span&gt;

    &lt;span class="kd"&gt;@Parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Fotoğraflar"&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;photos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;EntityCollection&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;PhotoEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;perform&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="kd"&gt;some&lt;/span&gt; &lt;span class="kt"&gt;IntentResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// photos here is a list of identifiers that aren't fully resolved yet;&lt;/span&gt;
        &lt;span class="c1"&gt;// you resolve only the subset you actually need yourself.&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;identifier&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;photos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;identifiers&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;tagPhoto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"iOS27"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;result&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;In the WWDC26 session, Apple's team showed this with a Shortcut demo that finds and tags 1,000 photos, describing the result with &lt;code&gt;EntityCollection&lt;/code&gt; as "almost instant"; no exact millisecond or percentage figure was shared.&lt;/p&gt;

&lt;h2&gt;
  
  
  SyncableEntity and cross-device Siri conversations
&lt;/h2&gt;

&lt;p&gt;As Apple defines it, &lt;code&gt;SyncableEntity&lt;/code&gt; is "an interface that indicates your entity has an identifier that's consistent across devices." Its signature is &lt;code&gt;protocol SyncableEntity : AppEntity&lt;/code&gt;, arriving with iOS 27.0. Its purpose is clear: "Siri uses this capability to transfer a conversation from one device to another."&lt;/p&gt;

&lt;p&gt;The problem: if your entity ID is generated per-device (say, a local database row identifier), the same photo gets a different ID on iPhone and iPad. When Siri wants to continue on one device a conversation that started on another, it can't resolve that ID, and the conversation breaks. If the identifier is already consistent across devices — say, a server-issued UUID — you can add the protocol, as the docs put it, "without any additional changes." If you keep the local and stable identifiers separate, make the entity's &lt;code&gt;id&lt;/code&gt; property type &lt;code&gt;SyncableEntityIdentifier&lt;/code&gt;:&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;import&lt;/span&gt; &lt;span class="kt"&gt;AppIntents&lt;/span&gt;

&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;PhotoEntity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;AppEntity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;SyncableEntity&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// The local ID (CoreData row identifier) and the stable ID (CloudKit&lt;/span&gt;
    &lt;span class="c1"&gt;// record name) are matched into a single identifier.&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;SyncableEntityIdentifier&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;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&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;localID&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="nv"&gt;stableID&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="nv"&gt;title&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="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;SyncableEntityIdentifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;local&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;localID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;stable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;stableID&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;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;typeDisplayRepresentation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;TypeDisplayRepresentation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Fotoğraf"&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;defaultQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;PhotoQuery&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;displayRepresentation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;DisplayRepresentation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;DisplayRepresentation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&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;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;PhotoQuery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;EntityQuery&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;entities&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nv"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;SyncableEntityIdentifier&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;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;]&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="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;PhotoEntity&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Contextual suggestions with RelevantEntities
&lt;/h2&gt;

&lt;p&gt;The framing of &lt;code&gt;RelevantEntities&lt;/code&gt; isn't the same width across the two primary sources, so use it knowing that. The session's definition is broad: "With RelevantEntities, you can suggest entities to the system and provide context about when and why they're relevant." The official doc's summary is narrower: "a type you use to donate your app's songs, albums, artists, and other media items to play during workouts." As of the beta, the only documented context generator on &lt;code&gt;AppEntityContext&lt;/code&gt; is &lt;code&gt;audio(_:)&lt;/code&gt;, meaning you tag your donation with an &lt;code&gt;AudioContext&lt;/code&gt;; the session's example ties running playlists to a workout context:&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;import&lt;/span&gt; &lt;span class="kt"&gt;AppIntents&lt;/span&gt;

&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;donateRunningPlaylists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;dailyRun&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;PlaylistEntity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;runningMix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;PlaylistEntity&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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;playlistEntities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dailyRun&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;runningMix&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;workoutContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;AppEntityContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;workout&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;RelevantEntities&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateEntities&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;playlistEntities&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="n"&gt;workoutContext&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// Remove specific entries:&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;RelevantEntities&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEntities&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;playlistEntities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;workoutContext&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;// Or remove all of them:&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;RelevantEntities&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeAllEntities&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;If you want to narrow the context, there are the &lt;code&gt;workout(activityType:)&lt;/code&gt; and &lt;code&gt;workout(intensityLevel:)&lt;/code&gt; type methods; the session's example writes &lt;code&gt;.workout(activityType: .running)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The record persists until you call &lt;code&gt;removeAllEntities()&lt;/code&gt;, &lt;code&gt;removeAllEntities(for:)&lt;/code&gt;, &lt;code&gt;removeEntities(_:)&lt;/code&gt;, or &lt;code&gt;removeEntities(_:from:)&lt;/code&gt;. For a non-media "contextual suggestion" scenario, the distinction the session suggests is this: use Spotlight for making content searchable, interaction donation for the system learning usage patterns, and &lt;code&gt;RelevantEntities&lt;/code&gt; for telling the system which content is relevant in specific situations — these are covered in more detail in the FAQ section below.&lt;/p&gt;

&lt;h2&gt;
  
  
  ExecutionTargets: which operation runs in which process
&lt;/h2&gt;

&lt;p&gt;When intents live in a package shared by the app and its extensions, the system decides which process runs them using heuristic rules — and that isn't always the right process. Apple's team defines it in the session as: "ExecutionTargets lets you tell the system exactly which process should run your intent." You can choose the main app, an App Intents extension, a WidgetKit extension, or any combination — for example, keeping a data-writing widget button's intent in the main app.&lt;/p&gt;

&lt;p&gt;Likewise, &lt;code&gt;@UnionValue&lt;/code&gt; also comes from the session: "A Swift enum where each case wraps a different type, letting a single parameter represent one of several options." The macro lets a parameter accept one of several entity types; its use isn't limited to widgets either: "this isn't limited to Widgets — @UnionValue parameters work everywhere your intent does, including the Shortcuts app." As the session puts it, what the macro produces is the type information, case metadata, and picker support the system needs.&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;@UnionValue&lt;/span&gt;
&lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="kt"&gt;MediaSelection&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nf"&gt;song&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;SongEntity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nf"&gt;album&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;AlbumEntity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nf"&gt;podcast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;PodcastEntity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;typeDisplayRepresentation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;TypeDisplayRepresentation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Medya"&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;caseDisplayRepresentations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;Cases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;DisplayRepresentation&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="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;song&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Şarkı"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;album&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Albüm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;podcast&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Podcast"&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;PlayMediaIntent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;AppIntent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;@Parameter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Medya"&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;selection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;MediaSelection&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;perform&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="kd"&gt;some&lt;/span&gt; &lt;span class="kt"&gt;IntentResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;selection&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;song&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;song&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;play&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;song&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;album&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;album&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;play&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;album&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;podcast&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;podcast&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;play&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;podcast&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;result&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;h2&gt;
  
  
  The order to follow when migrating existing AppIntents to iOS 27
&lt;/h2&gt;

&lt;p&gt;The order below is a practical implementation sequence I derived from the API contracts above:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Measure the duration first.&lt;/strong&gt; If your &lt;code&gt;perform()&lt;/code&gt; body regularly exceeds 30 seconds, it's a &lt;code&gt;LongRunningIntent&lt;/code&gt; candidate; if it doesn't, you don't need to change anything. Measure on a real device with real data volume — simulator timings can be misleading.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrap it in &lt;code&gt;performBackgroundTask&lt;/code&gt;.&lt;/strong&gt; Move it into this closure without changing your existing logic, and fill in the &lt;code&gt;progress&lt;/code&gt; fields. Testing this step on its own makes it easier to isolate errors that show up in the next step when you add &lt;code&gt;CancellableIntent&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add &lt;code&gt;CancellableIntent&lt;/code&gt; and write a real &lt;code&gt;onCancel&lt;/code&gt;.&lt;/strong&gt; Don't leave it empty — whether your app stays consistent when the user stops the task depends on this block. Decide here whether you'll roll back partially processed records or leave them as they are.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review your array parameters.&lt;/strong&gt; Any &lt;code&gt;@Parameter&lt;/code&gt; array carrying more than a few dozen records is an &lt;code&gt;EntityCollection&lt;/code&gt; candidate; first clarify whether your intent needs the entity's full property set or just its identifier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add &lt;code&gt;SyncableEntity&lt;/code&gt; to entities used across devices&lt;/strong&gt;, especially in scenarios where Siri conversations start on one device and finish on another. If your local ID is already stable (say, a server-assigned UUID), you may not need any additional mapping; you only need a &lt;code&gt;SyncableEntityIdentifier&lt;/code&gt; mapping if you have device-specific IDs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluate &lt;code&gt;RelevantEntities&lt;/code&gt; within its documented context.&lt;/strong&gt; If you're after a general "suggested content" experience, also put Spotlight indexing and the &lt;code&gt;IntentDonationManager&lt;/code&gt; donation flow on the table.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Table: Which API solves which problem
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;Problem it solves&lt;/th&gt;
&lt;th&gt;iOS version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LongRunningIntent&lt;/td&gt;
&lt;td&gt;30-second background run-time limit&lt;/td&gt;
&lt;td&gt;iOS 27.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CancellableIntent&lt;/td&gt;
&lt;td&gt;Graceful cancellation and cleanup&lt;/td&gt;
&lt;td&gt;iOS 26.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EntityCollection&lt;/td&gt;
&lt;td&gt;Full-resolve cost on large parameter arrays&lt;/td&gt;
&lt;td&gt;iOS 27.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SyncableEntity&lt;/td&gt;
&lt;td&gt;Unstable entity ID across devices&lt;/td&gt;
&lt;td&gt;iOS 27.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RelevantEntities&lt;/td&gt;
&lt;td&gt;Context-based content suggestions&lt;/td&gt;
&lt;td&gt;iOS 27.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ExecutionTargets&lt;/td&gt;
&lt;td&gt;Which process the intent runs in&lt;/td&gt;
&lt;td&gt;iOS 27.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@UnionValue&lt;/td&gt;
&lt;td&gt;Multiple entity types in a single parameter&lt;/td&gt;
&lt;td&gt;iOS 27.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Table: Migration prioritization checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Recommended API&lt;/th&gt;
&lt;th&gt;Priority&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Intent frequently hits the 30-second timeout&lt;/td&gt;
&lt;td&gt;LongRunningIntent + CancellableIntent&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parameter array carries hundreds/thousands of records&lt;/td&gt;
&lt;td&gt;EntityCollection&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same content gives a different Siri result on different devices&lt;/td&gt;
&lt;td&gt;SyncableEntity&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing content suggestions in a workout/audio context&lt;/td&gt;
&lt;td&gt;RelevantEntities&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unclear or needlessly heavy process placement for the intent&lt;/td&gt;
&lt;td&gt;ExecutionTargets&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  🏆 GOLDEN TIP: When testing progress reporting, verify Live Activity on a real device, not the simulator
&lt;/h2&gt;

&lt;p&gt;Even if LongRunningIntent's progress updates look visually correct in the simulator, the real timing of the Live Activity, its lock-screen behavior, and the latency of a cancel tap reaching onCancel can only be measured realistically on a physical device. Instead of updating progress.completedUnitCount very frequently inside perform(), update it at meaningful stage boundaries; otherwise the Live Activity redraws needlessly often, showing up to the user as a jittery progress bar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;p&gt;I've put together a practical checklist to go through before you start your iOS 27 App Intents migration. This list summarizes the order in which to evaluate the five APIs covered in the sections above, and the steps most often skipped during migration; you can go through it in order before writing a new intent or migrating an existing one.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Measure perform() duration on a real device, confirm whether it exceeds 30 seconds&lt;/li&gt;
&lt;li&gt;Don't break your existing error handling when moving it into the performBackgroundTask closure&lt;/li&gt;
&lt;li&gt;Add CancellableIntent and write real cleanup in the onCancel handler, don't leave it empty&lt;/li&gt;
&lt;li&gt;Use the reason value that arrives in onCancel, don't track the cause in a separate state&lt;/li&gt;
&lt;li&gt;Review array parameters, move anything exceeding a hundred records to EntityCollection&lt;/li&gt;
&lt;li&gt;Make the id type of entities used across devices SyncableEntityIdentifier&lt;/li&gt;
&lt;li&gt;Tag your RelevantEntities donation with a documented context&lt;/li&gt;
&lt;li&gt;Test Live Activity behavior on a real device, not the simulator&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do you write an intent that runs longer than 30 seconds on iOS 27?
&lt;/h3&gt;

&lt;p&gt;Conform your intent to &lt;code&gt;LongRunningIntent&lt;/code&gt;, which also builds on &lt;code&gt;ProgressReportingIntent&lt;/code&gt;. Put your work inside a &lt;code&gt;performBackgroundTask&lt;/code&gt; block and update &lt;code&gt;progress&lt;/code&gt; at regular intervals; per the docs, if you don't update it regularly, the system may cancel the background extension and end your task early. While it runs, an automatic Live Activity appears with a stop option.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is SyncableEntity, and why can't Siri find the entity across devices?
&lt;/h3&gt;

&lt;p&gt;By default, your entity ID might be generated per device (say, a local database row identifier); the same content gets a different ID on iPhone and iPad. When Siri tries to continue on another device a conversation that started on one, it can't resolve that ID. Add &lt;code&gt;SyncableEntity&lt;/code&gt; and provide a stable identifier (server-assigned, or a CloudKit record ID) — making the &lt;code&gt;id&lt;/code&gt; property's type &lt;code&gt;SyncableEntityIdentifier&lt;/code&gt; if you keep local and stable identifiers separate — and Siri recognizes the entity the same way on every device.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you process thousands of records quickly with EntityCollection?
&lt;/h3&gt;

&lt;p&gt;With a normal array parameter, the system fully resolves every entity before the intent runs; with thousands of records this creates a serious delay. Switch the type to &lt;code&gt;EntityCollection&lt;/code&gt; and the system only passes identifiers, skipping the full resolve. In Apple's own demo with 1,000 photos, this produced an "almost instant" result; no exact millisecond figure was shared.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I make my app visible in iOS 27 Siri?
&lt;/h3&gt;

&lt;p&gt;The distinction the session draws points to three complementary paths. First, indexing your content in Spotlight — that's how Siri finds and surfaces your content. Second, donating user actions with &lt;code&gt;IntentDonationManager&lt;/code&gt;; the system learns patterns and suggests similar actions. Third, for content that's never been searched or played, giving the "this content is relevant in this situation" hint with &lt;code&gt;RelevantEntities&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is using CancellableIntent mandatory, or is LongRunningIntent alone enough?
&lt;/h3&gt;

&lt;p&gt;Not mandatory, but strongly recommended. &lt;code&gt;LongRunningIntent&lt;/code&gt; only extends run time; without &lt;code&gt;CancellableIntent&lt;/code&gt;, your app gets no chance to clean up when the user stops the work, and can end up half-finished. In the WWDC26 session, the two protocols are presented as a natural pair.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I build a general "suggested content" list with RelevantEntities?
&lt;/h3&gt;

&lt;p&gt;The two sources speak at different widths, so be careful. The session describes the API broadly ("suggest entities to the system and provide context about when and why they're relevant"), while the official doc's summary talks about media content like songs, albums, artists, playlists, and podcasts. For a general, non-media suggestion experience, factor in Spotlight indexing and &lt;code&gt;IntentDonationManager&lt;/code&gt; donation too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Update (September 2026)
&lt;/h2&gt;

&lt;p&gt;This guide was written during the beta period. Apple released iOS 27.0 (24A437) and Xcode 27 (27A266a) on September 14, 2026; the APIs above are no longer in beta, they're in general availability. As of September 21, 2026, iOS 27.2 beta 2 (24B5089g) is in distribution.&lt;/p&gt;

&lt;p&gt;Related posts published later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://muhittincamdali.com/blog/ios-27-xcode-27-gelistirici-rehberi/" rel="noopener noreferrer"&gt;the iOS 27 and Xcode 27 developer guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;iOS 27's App Intents update isn't a single big feature, it's a set of five complementary APIs: &lt;code&gt;LongRunningIntent&lt;/code&gt; and &lt;code&gt;CancellableIntent&lt;/code&gt; make long-running work reliable, &lt;code&gt;EntityCollection&lt;/code&gt; solves scale, &lt;code&gt;SyncableEntity&lt;/code&gt; provides cross-device consistency, and &lt;code&gt;RelevantEntities&lt;/code&gt; handles context-based content suggestions. If you have an existing App Intents integration, it's enough to first identify your duration and scale issues and then follow the migration order above.&lt;/p&gt;

&lt;p&gt;If you're not yet familiar with the core App Intents concepts, start with &lt;a href="https://muhittincamdali.com/blog/app-intents-shortcuts/" rel="noopener noreferrer"&gt;App Intents and Shortcuts integration&lt;/a&gt;. To see the Live Activity mechanism in depth, &lt;a href="https://muhittincamdali.com/blog/interactive-widgets-ios17/" rel="noopener noreferrer"&gt;interactive widgets and Live Activity&lt;/a&gt; will be useful. If you're curious about Siri's conversation-based side, you can check out &lt;a href="https://muhittincamdali.com/blog/sirikit-voice-integration/" rel="noopener noreferrer"&gt;voice integration with SiriKit&lt;/a&gt;. To see what's broken on the SwiftUI side of iOS 27, you can read &lt;a href="https://muhittincamdali.com/en/blog/ios-27-state-macro-contentbuilder-kirilanlar/" rel="noopener noreferrer"&gt;State macro and ContentBuilder breaks on iOS 27&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developer.apple.com/videos/play/wwdc2026/345/" rel="noopener noreferrer"&gt;WWDC26 Session 345 — Discover new capabilities in the App Intents framework&lt;/a&gt; — the official introduction of LongRunningIntent, CancellableIntent, EntityCollection, SyncableEntity, RelevantEntities, ExecutionTargets, and @UnionValue.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.apple.com/documentation/appintents/longrunningintent" rel="noopener noreferrer"&gt;LongRunningIntent — Apple Developer Documentation&lt;/a&gt; — the official definition of the protocol that extends the 30-second background run-time limit.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.apple.com/documentation/appintents/cancellableintent" rel="noopener noreferrer"&gt;CancellableIntent — Apple Developer Documentation&lt;/a&gt; — graceful cancellation support and using withIntentCancellationHandler(operation:onCancel:isolation:).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.apple.com/documentation/appintents/entitycollection" rel="noopener noreferrer"&gt;EntityCollection — Apple Developer Documentation&lt;/a&gt; — the type that reduces the full-resolve cost for large entity sets.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.apple.com/documentation/appintents/syncableentity" rel="noopener noreferrer"&gt;SyncableEntity — Apple Developer Documentation&lt;/a&gt; — the cross-device stable entity identifier interface and the SyncableEntityIdentifier example.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.apple.com/documentation/appintents/relevantentities" rel="noopener noreferrer"&gt;RelevantEntities — Apple Developer Documentation&lt;/a&gt; — contextual suggestion donation and the updateEntities(_:for:) API.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.apple.com/documentation/appintents/progressreportingintent" rel="noopener noreferrer"&gt;ProgressReportingIntent — Apple Developer Documentation&lt;/a&gt; — the progress-reporting infrastructure that's existed since iOS 17.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.apple.com/news/releases/" rel="noopener noreferrer"&gt;Releases — Apple Developer&lt;/a&gt; — release dates for iOS 27.0 (24A437), Xcode 27 (27A266a), and iOS 27.2 beta 2 (24B5089g).&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://muhittincamdali.com/en/blog/app-intents-ios-27-longrunningintent-siri/" rel="noopener noreferrer"&gt;muhittincamdali.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>swift</category>
      <category>ios</category>
      <category>swiftui</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Swift 6 Strict Concurrency: Production Migration Playbook</title>
      <dc:creator>Muhittin Çamdalı</dc:creator>
      <pubDate>Mon, 26 Oct 2026 08:09:28 +0000</pubDate>
      <link>https://dev.to/muhittin_camdali/swift-6-strict-concurrency-production-migration-playbook-27m4</link>
      <guid>https://dev.to/muhittin_camdali/swift-6-strict-concurrency-production-migration-playbook-27m4</guid>
      <description>&lt;h1&gt;
  
  
  Swift 6 Strict Concurrency: Production Migration Playbook
&lt;/h1&gt;

&lt;p&gt;Swift 6.0's &lt;strong&gt;strict concurrency&lt;/strong&gt; looks, at first glance, like an opt-in compile flag. But in reality? It's a serious refactor mandate touching every one of your classes, singletons, delegate patterns, and dependency-injection architecture.&lt;/p&gt;

&lt;p&gt;This article walks through the &lt;strong&gt;12 common pitfalls&lt;/strong&gt; you are likely to hit when moving a large production iOS codebase to strict concurrency mode, their fixes, and the correct actor migration order. Spoiler: &lt;code&gt;@MainActor&lt;/code&gt; is not the cure for everything.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pro Tip:&lt;/strong&gt; Before starting the migration, don't begin with &lt;code&gt;SWIFT_STRICT_CONCURRENCY = complete&lt;/code&gt; — start with &lt;code&gt;targeted&lt;/code&gt;. Targeted mode only checks Sendable; complete mode also enforces actor isolation. Starting targeted cuts your first-pass error count in half.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  1. The Three Levels of Strict Concurrency Mode
&lt;/h2&gt;

&lt;p&gt;Swift 6 strict concurrency is controlled by the &lt;code&gt;SWIFT_STRICT_CONCURRENCY&lt;/code&gt; build setting, and it has three levels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Minimal — Swift 5.x default behavior&lt;/span&gt;
SWIFT_STRICT_CONCURRENCY &lt;span class="o"&gt;=&lt;/span&gt; minimal

&lt;span class="c"&gt;# Targeted — only Sendable explicit marked types check&lt;/span&gt;
SWIFT_STRICT_CONCURRENCY &lt;span class="o"&gt;=&lt;/span&gt; targeted

&lt;span class="c"&gt;# Complete — full Swift 6 strict concurrency&lt;/span&gt;
SWIFT_STRICT_CONCURRENCY &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;complete&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Practical migration strategy:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Weeks 1-2:&lt;/strong&gt; Move the whole project from &lt;code&gt;minimal&lt;/code&gt; to &lt;code&gt;targeted&lt;/code&gt;. Fix Sendable warnings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weeks 3-4:&lt;/strong&gt; Turn on &lt;code&gt;complete&lt;/code&gt; mode module by module. The UI layer goes last.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Week 5:&lt;/strong&gt; Full project on &lt;code&gt;complete&lt;/code&gt;. Swift 6 language mode active.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Migrating small modules first (Networking, Persistence) reduces production risk — changes that leak into the UI layer are usually the most expensive.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Sendable: The End of Invisible Leaks
&lt;/h2&gt;

&lt;p&gt;A large share of the migration work comes down to &lt;code&gt;Sendable&lt;/code&gt; conformance. A type being Sendable means it's &lt;strong&gt;safe to share across multiple threads&lt;/strong&gt;.&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="c1"&gt;// ❌ Strict mode error: User isn't Sendable&lt;/span&gt;
&lt;span class="kd"&gt;class&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;preferences&lt;/span&gt;&lt;span class="p"&gt;:&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="kt"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;// [String: Any] isn't Sendable!&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Solution: struct + typed properties&lt;/span&gt;
&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;User&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Sendable&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;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UUID&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;preferences&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UserPreferences&lt;/span&gt;  &lt;span class="c1"&gt;// Sendable Codable struct&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Common Sendable pitfalls:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;[String: Any]&lt;/code&gt; — &lt;code&gt;Any&lt;/code&gt; isn't Sendable; use a typed dictionary&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;URL&lt;/code&gt; — Sendable ✓, &lt;code&gt;URLRequest&lt;/code&gt; Sendable ✓&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Date&lt;/code&gt; — Sendable ✓ (Swift 5.5+)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UIImage&lt;/code&gt; — not Sendable (reference type, mutable)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NSData&lt;/code&gt; — not Sendable; &lt;code&gt;Data&lt;/code&gt; is Sendable ✓&lt;/li&gt;
&lt;li&gt;Closure types — require the &lt;code&gt;@Sendable&lt;/code&gt; annotation&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pro Tip:&lt;/strong&gt; Don't try to make legacy Objective-C types (NSObject subclasses) Sendable — they're reference types and mutable. Wrap them in a pure-Swift struct instead.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  3. Actor Migration Order: The Right Way
&lt;/h2&gt;

&lt;p&gt;Actors are the gold standard for concurrent state in Swift 6. But migrating them in the wrong order means walking into a maze of rework. &lt;strong&gt;Recommended order:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Stateless services&lt;/strong&gt; (URLSession wrapper, JSON decoder) — not an actor, use &lt;code&gt;struct&lt;/code&gt; or &lt;code&gt;enum&lt;/code&gt; with static methods&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stateful services&lt;/strong&gt; (cache, session manager) — make them an &lt;code&gt;actor&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DI container / service locator&lt;/strong&gt; — global state, &lt;code&gt;actor&lt;/code&gt; or &lt;code&gt;@MainActor&lt;/code&gt; class&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ViewModels&lt;/strong&gt; — &lt;code&gt;@MainActor&lt;/code&gt; (they have a UI dependency)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Views&lt;/strong&gt; — SwiftUI is already MainActor-isolated
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Stateful cache → actor (Correct)&lt;/span&gt;
&lt;span class="kd"&gt;actor&lt;/span&gt; &lt;span class="kt"&gt;ImageCache&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;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIImage&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;func&lt;/span&gt; &lt;span class="nf"&gt;image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;URL&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;UIImage&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&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;setImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIImage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Usage: await required&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;imageCache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;image&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="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pitfall:&lt;/strong&gt; don't try to preserve your old synchronous APIs when converting to &lt;code&gt;actor&lt;/code&gt;. Every public method becomes async. If call sites are too scattered, use a &lt;strong&gt;transition wrapper&lt;/strong&gt;:&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;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;ImageCacheBridge&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;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;ImageCache&lt;/span&gt;

    &lt;span class="kd"&gt;nonisolated&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;imageSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;URL&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;UIImage&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Sync fallback for legacy paths (deprecated)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kt"&gt;MainActor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assumeIsolated&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;legacyCacheStorage&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;url&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;[[demo:actor-race-sim]]&lt;/p&gt;

&lt;h2&gt;
  
  
  4. &lt;code&gt;@MainActor&lt;/code&gt; Pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@MainActor&lt;/code&gt; is the most powerful tool for UIKit/SwiftUI interop. But misuse hurts performance badly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pitfall 1: Marking the entire class &lt;code&gt;@MainActor&lt;/code&gt;&lt;/strong&gt;&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="c1"&gt;// ❌ Wrong — JSON parsing blocks the main thread&lt;/span&gt;
&lt;span class="kd"&gt;@MainActor&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;APIClient&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;fetchUsers&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="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="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;data&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="kt"&gt;URLSession&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;usersURL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="kt"&gt;JSONDecoder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&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="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// 200ms main thread block!&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Correct — only UI-bound method @MainActor&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;APIClient&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;nonisolated&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;fetchUsers&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="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="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;data&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="kt"&gt;URLSession&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;usersURL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="kt"&gt;JSONDecoder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&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="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;@MainActor&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;updateUI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="nv"&gt;users&lt;/span&gt;&lt;span class="p"&gt;:&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="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// UI update logic&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;strong&gt;Pitfall 2: Incorrectly capturing values in &lt;code&gt;@MainActor&lt;/code&gt; closures&lt;/strong&gt;&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="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;@MainActor&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="c1"&gt;// This whole block runs on the main thread — don't do heavy work here!&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;processed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;heavyProcessing&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;// main blocking!&lt;/span&gt;
    &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;processed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Correct — heavy work in background, only UI update on main&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;let&lt;/span&gt; &lt;span class="nv"&gt;processed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;heavyProcessing&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;// arbitrary executor&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="kt"&gt;MainActor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&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="nf"&gt;updateView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;processed&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;strong&gt;Pitfall 3:&lt;/strong&gt; &lt;code&gt;MainActor.assumeIsolated&lt;/code&gt; is for synchronous compatibility, but misuse causes a runtime crash. Only use it where you can guarantee the code is already running on the main thread.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Async/Await and Existing Callback Patterns
&lt;/h2&gt;

&lt;p&gt;A large, older codebase can easily have hundreds of closure-based APIs. Migrating all of them to &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; at once isn't feasible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bridge pattern:&lt;/strong&gt;&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="c1"&gt;// Old callback API&lt;/span&gt;
&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;fetchData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;@escaping&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;Data&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// legacy code&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Async wrapper&lt;/span&gt;
&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;fetchData&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;Data&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;withCheckedThrowingContinuation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;continuation&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
        &lt;span class="n"&gt;fetchData&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="n"&gt;continuation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&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;strong&gt;Watch out — &lt;code&gt;withCheckedThrowingContinuation&lt;/code&gt; pitfalls:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The continuation is one-shot — resuming it twice by mistake crashes&lt;/li&gt;
&lt;li&gt;If you store the closure and never resume it, the async caller hangs forever&lt;/li&gt;
&lt;li&gt;Cancellation handling must be explicit — use &lt;code&gt;withTaskCancellationHandler&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In production, use &lt;code&gt;@CheckedContinuation&lt;/code&gt; to catch continuation leaks (Swift 6 default, has a debug check).&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Does the Singleton Pattern Survive Strict Concurrency?
&lt;/h2&gt;

&lt;p&gt;You're told singletons are an anti-pattern, but in a codebase with 200K LOC and 8 years of history, they exist and aren't going anywhere. How do they survive strict concurrency?&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="c1"&gt;// ❌ Strict mode error&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;AnalyticsTracker&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;shared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;AnalyticsTracker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;// mutable global state warning&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;sessionID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uuidString&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Solution 1: actor&lt;/span&gt;
&lt;span class="kd"&gt;actor&lt;/span&gt; &lt;span class="kt"&gt;AnalyticsTracker&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;shared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;AnalyticsTracker&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;sessionID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uuidString&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;setSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;id&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;sessionID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Solution 2: immutable&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;AnalyticsTracker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Sendable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;shared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;AnalyticsTracker&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;sessionID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uuidString&lt;/span&gt;  &lt;span class="c1"&gt;// immutable, Sendable safe&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;init&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;strong&gt;Decision matrix:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Internal state gets mutated → actor&lt;/li&gt;
&lt;li&gt;Pure read-only → Sendable final class&lt;/li&gt;
&lt;li&gt;UI-bound singleton → &lt;code&gt;@MainActor final class&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  7. Delegate Pattern → AsyncSequence Migration
&lt;/h2&gt;

&lt;p&gt;Most Apple frameworks are delegate-based. Under strict concurrency, delegate methods need Sendable 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="c1"&gt;// Old pattern&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;LocationTracker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;NSObject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;CLLocationManagerDelegate&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;locationManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;manager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;CLLocationManager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;didUpdateLocations&lt;/span&gt; &lt;span class="nv"&gt;locations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;CLLocation&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// delegate callback&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Modern pattern — AsyncSequence&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;LocationTracker&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;locations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;AsyncStream&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;CLLocation&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;AsyncStream&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;continuation&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;bridge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;LocationBridge&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
                &lt;span class="n"&gt;continuation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;yield&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="c1"&gt;// CLLocationManager setup&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="c1"&gt;// Usage — clean&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tracker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;locations&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;viewModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateLocation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;location&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 pattern works the same way for MapKit, BLE, Camera, and Audio — wrap the old delegates in an AsyncStream.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Test Mocking + Sendable
&lt;/h2&gt;

&lt;p&gt;Test mocks need to be Sendable, otherwise tests fail under the concurrent runner.&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="c1"&gt;// Test double — Sendable conform&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;MockUserService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UserServicing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Sendable&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;stubUsers&lt;/span&gt;&lt;span class="p"&gt;:&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="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;users&lt;/span&gt;&lt;span class="p"&gt;:&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="o"&gt;=&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;stubUsers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;users&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;fetchUsers&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="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="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;stubUsers&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;strong&gt;Pro Tip:&lt;/strong&gt; XCTest's default test parallelization does a Sendable check. If a test class holds state, mark it &lt;code&gt;@MainActor&lt;/code&gt; or move that state into an &lt;code&gt;actor&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Production Profiling: The Performance Trade-off
&lt;/h2&gt;

&lt;p&gt;Strict concurrency isn't free. Actor hops add a small cost to every async call. These are the metrics to compare in your own app before and after the migration:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;th&gt;How to measure&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Launch time&lt;/td&gt;
&lt;td&gt;Actor hops added to the main thread can delay the first screen&lt;/td&gt;
&lt;td&gt;Instruments App Launch template + MetricKit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory baseline&lt;/td&gt;
&lt;td&gt;Extra tasks and actor objects can raise the memory floor&lt;/td&gt;
&lt;td&gt;Xcode Memory Report + MetricKit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU under load&lt;/td&gt;
&lt;td&gt;Frequent actor hops can raise CPU share in busy flows&lt;/td&gt;
&lt;td&gt;Instruments Time Profiler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build time&lt;/td&gt;
&lt;td&gt;Complete mode makes type checking heavier and builds slower&lt;/td&gt;
&lt;td&gt;Xcode Build Timeline&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Build time&lt;/strong&gt; is usually the most noticeable increase. Fix: incremental build caching and modularization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runtime overhead&lt;/strong&gt; stays negligible for most mobile apps; still, verify hot paths with Time Profiler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On the crash side&lt;/strong&gt;, the expected gain is that data races closed at compile time no longer surface as runtime crashes; measure the effect by comparing crash analytics before and after the migration.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Migration Checklist
&lt;/h2&gt;

&lt;p&gt;A recommended sequential checklist for migrating a production app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;strong&gt;Phase 0:&lt;/strong&gt; Project on &lt;code&gt;SWIFT_STRICT_CONCURRENCY = targeted&lt;/code&gt;, clean build&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Phase 1:&lt;/strong&gt; Migrate the networking layer, Sendable conformance&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Phase 2:&lt;/strong&gt; Persistence layer (Core Data, SwiftData) actor migration&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Phase 3:&lt;/strong&gt; Service container (DI) as &lt;code&gt;actor&lt;/code&gt; or &lt;code&gt;@MainActor&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Phase 4:&lt;/strong&gt; Mark ViewModels &lt;code&gt;@MainActor&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Phase 5:&lt;/strong&gt; Mark UIKit ViewControllers &lt;code&gt;@MainActor&lt;/code&gt; (SwiftUI views already are)&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Phase 6:&lt;/strong&gt; Turn on &lt;code&gt;SWIFT_STRICT_CONCURRENCY = complete&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Phase 7:&lt;/strong&gt; Swift 6 language mode (&lt;code&gt;SWIFT_VERSION = 6&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Phase 8:&lt;/strong&gt; Enforce strict mode in the CI/CD pipeline&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Phase 9:&lt;/strong&gt; Two weeks of crash-analytics observation&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Phase 10:&lt;/strong&gt; Publish the performance baseline&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pro Tip:&lt;/strong&gt; Don't do the migration on a feature branch — go gradual on main. On a large codebase the migration can take weeks or even months, so conflicts on a long-lived branch are unavoidable. Merging module-by-module into main is far more sustainable.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Strict concurrency is the delivery on Swift's 8-year-old safety promise. The ability to catch data races at compile time is valuable because it closes this class of runtime crashes at the source. But the migration isn't cheap — on a large codebase, budget for effort that can stretch over months, noticeably longer build times, and a small runtime overhead.&lt;/p&gt;

&lt;p&gt;The pragmatic call: for new projects, Swift 6 + complete strict concurrency by default. For existing projects, a &lt;strong&gt;targeted → complete&lt;/strong&gt; transition on a phased, module-by-module plan. &lt;code&gt;@MainActor&lt;/code&gt; isn't the cure for everything; used correctly, it means keeping heavy work off the main thread with &lt;code&gt;nonisolated&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Related resources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.swift.org/migration/documentation/migrationguide/" rel="noopener noreferrer"&gt;Apple Swift Concurrency Migration Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0337-support-incremental-migration-to-concurrency-checking.md" rel="noopener noreferrer"&gt;Swift Evolution SE-0337&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pointfree.co" rel="noopener noreferrer"&gt;Strict Concurrency Performance Benchmarks (Point-Free)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://muhittincamdali.com/en/blog/swift-6-strict-concurrency-migration-playbook/" rel="noopener noreferrer"&gt;muhittincamdali.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>swift</category>
      <category>ios</category>
      <category>concurrency</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Swift 6.3 Migration: Actor Isolation Warnings and Escape Hatches</title>
      <dc:creator>Muhittin Çamdalı</dc:creator>
      <pubDate>Mon, 26 Oct 2026 05:04:31 +0000</pubDate>
      <link>https://dev.to/muhittin_camdali/swift-63-migration-actor-isolation-warnings-and-escape-hatches-3bm7</link>
      <guid>https://dev.to/muhittin_camdali/swift-63-migration-actor-isolation-warnings-and-escape-hatches-3bm7</guid>
      <description>&lt;p&gt;Don't be surprised if a project that compiled cleanly on Swift 6.2 starts throwing actor isolation warnings after you move to Swift 6.3 without changing a line — it's not a coincidence. Swift 6.3 introduced no new language rule or default for actor isolation; even so, some patterns silent on 6.2 now warn on 6.3 — and in the documented case below, the root isn't a new rule at all but the conservative behavior of region isolation analysis. This isn't a generic "what is strict concurrency" explainer; it shows, directly, which code patterns broke in the 6.2 → 6.3 delta, with real compiler messages and real escape hatches.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Pro Tip:&lt;/strong&gt; Whether a piece of code is &lt;code&gt;isolated&lt;/code&gt; or &lt;code&gt;nonisolated&lt;/code&gt; is part of its signature — changing it is almost always a source-breaking change, and that's exactly where you get hit hardest during a version upgrade.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Today's reality: 6.3.3 is shipped, 6.4 is on the way
&lt;/h2&gt;

&lt;p&gt;Swift 6.3 was officially released on March 24, 2026, signed by Holly Borla and Joe Heck. The headline items weren't about concurrency — more flexible C interop, cross-platform build tooling, embedded-environment enhancements, and an official Android SDK. So the official announcement doesn't even mention concurrency; yet the documented warning is real — you'll see why below, in the conservative rule of region isolation analysis.&lt;/p&gt;

&lt;p&gt;The release chain today (early September 2026): after 6.3 came the 6.3.1 and 6.3.2 patches, then 6.3.3 on June 30, 2026 — still the current stable tag on swift.org's install page. Meanwhile Swift 6.4 prep continues: Xcode 27 was still in beta at the end of August 2026 (beta 6, August 24, 2026), so 6.4 isn't an officially released version yet. swift.org's August 20, 2026 post "Embedded Swift Improvements Coming in Swift 6.4" even says "coming" right in the title — 6.4 is still a version tried via development snapshots.&lt;br&gt;
Point-Free's post from their Xcode 27 beta days is a live example: the team had to explicitly mark something &lt;code&gt;@MainActor&lt;/code&gt; that should already have been so, because of an isolation-checking regression in Swift 6.4, then quickly merged the fix and shipped a release.&lt;/p&gt;
&lt;h2&gt;
  
  
  Summary of 6.2 → 6.3 language changes
&lt;/h2&gt;

&lt;p&gt;To understand Swift 6.3's tightening of actor isolation, you first need what changed in 6.2 — half of the configuration that triggers the warning, the module's default-isolation setting, arrived in 6.2 via SE-0466.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SE-0466 — Module-wide default actor isolation.&lt;/strong&gt; This proposal reached "Implemented" status in Swift 6.2 and added a compiler setting that lets &lt;code&gt;@MainActor&lt;/code&gt; inference become the default within a module. The goal was to reduce false-positive data-race errors in single-threaded code — in the proposal's own words: "The easiest and best way to model single-threaded code is with a global actor. Everything on a global actor runs sequentially."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SE-0461 — &lt;code&gt;nonisolated(nonsending)&lt;/code&gt; default.&lt;/strong&gt; Also "Implemented" in 6.2, this proposal changed the behavior of &lt;code&gt;nonisolated async&lt;/code&gt; functions: such functions now run on the caller's actor by default instead of hopping to a separate executor. The &lt;code&gt;NonisolatedNonsendingByDefault&lt;/code&gt; upcoming-feature flag turns this behavior on.&lt;/p&gt;

&lt;p&gt;Together these two changes make 6.2's philosophy clear: the compiler now runs on "sequential by default, parallel when needed" logic. 6.3 announced no new concurrency default; even so, some patterns silent on 6.2 now produce new isolation warnings.&lt;/p&gt;
&lt;h2&gt;
  
  
  Documented case: the &lt;code&gt;@Sendable&lt;/code&gt; × &lt;code&gt;@escaping&lt;/code&gt; isolation warning
&lt;/h2&gt;

&lt;p&gt;A thread opened by Itai Ferber on the Swift Forums is the most concrete documented example. Ferber's description is precise, in his own words: "This only reproduces on Swift 6.3 in a project with &lt;code&gt;nonisolated&lt;/code&gt; default isolation and concurrency checking set to Complete; earlier Swift versions, default &lt;code&gt;MainActor&lt;/code&gt; isolation, or minimal/targeted concurrency checking don't produce this warning."&lt;/p&gt;

&lt;p&gt;This alone gives a practical hint: if your project uses the &lt;code&gt;-default-isolation MainActor&lt;/code&gt; flag (i.e., you've turned on SE-0466's MainActor-default at the module level), you won't see this specific warning. The producing configuration: module default isolation still &lt;code&gt;nonisolated&lt;/code&gt;, target compiled with &lt;code&gt;SWIFT_STRICT_CONCURRENCY=complete&lt;/code&gt;.&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="c1"&gt;// Reproducer from Ferber's forum thread: silent on 6.2, warns on 6.3&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;External&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// System-provided, Obj-C-originated signature (in reality, Timer.scheduledTimer)&lt;/span&gt;
    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;ƒ1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;work&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;@escaping&lt;/span&gt; &lt;span class="kd"&gt;@Sendable&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;Void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;work&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="kd"&gt;@MainActor&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;Internal&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;ƒ2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;@escaping&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;Void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;completion&lt;/span&gt;&lt;span class="p"&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;ƒ3&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="kd"&gt;@MainActor&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;complete&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
            &lt;span class="kt"&gt;External&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ƒ1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kt"&gt;MainActor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assumeIsolated&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;ƒ2&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="kt"&gt;MainActor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assumeIsolated&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                            &lt;span class="c1"&gt;// ⚠️ Sending 'complete' risks causing data races&lt;/span&gt;
                            &lt;span class="n"&gt;complete&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&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;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;complete&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;Ferber's actual compiler output reads: "Sending 'complete' risks causing data races; this is an error in the Swift 6 language mode ... Task-isolated 'complete' is captured by a main actor-isolated closure." Notice: the warning isn't about the closure itself, but the capture of the &lt;code&gt;var complete&lt;/code&gt; variable defined inside the &lt;code&gt;Task { @MainActor in }&lt;/code&gt; body.&lt;/p&gt;

&lt;p&gt;Two fixes are documented: removing &lt;code&gt;@Sendable&lt;/code&gt; from &lt;code&gt;External.ƒ1()&lt;/code&gt;, or &lt;code&gt;@escaping&lt;/code&gt; from &lt;code&gt;Internal.ƒ2()&lt;/code&gt;. In Ferber's words: "Removing &lt;code&gt;@Sendable&lt;/code&gt; from &lt;code&gt;External.ƒ1()&lt;/code&gt; or &lt;code&gt;@escaping&lt;/code&gt; from &lt;code&gt;Internal.ƒ2()&lt;/code&gt; resolves the warning." Why: because &lt;code&gt;complete&lt;/code&gt; passes through a &lt;code&gt;nonisolated&lt;/code&gt; context, it must be implicitly sent to MainActor, and &lt;code&gt;sending&lt;/code&gt; semantics require statically verifying the value can no longer be used from the source context — impossible in &lt;code&gt;nonisolated&lt;/code&gt; contexts. Ferber accepts the diagnosis: "warning about such a mutable &lt;code&gt;var&lt;/code&gt; capture seems reasonable."&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;6.2 behavior&lt;/th&gt;
&lt;th&gt;6.3 behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;nonisolated&lt;/code&gt; default + Complete checking&lt;/td&gt;
&lt;td&gt;Compiles silently&lt;/td&gt;
&lt;td&gt;Warning on Sendable+escaping closure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;-default-isolation MainActor&lt;/code&gt; (SE-0466 on)&lt;/td&gt;
&lt;td&gt;Compiles silently&lt;/td&gt;
&lt;td&gt;Compiles silently (no warning)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Minimal/targeted checking&lt;/td&gt;
&lt;td&gt;Compiles silently&lt;/td&gt;
&lt;td&gt;Compiles silently&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;@Sendable&lt;/code&gt; OR &lt;code&gt;@escaping&lt;/code&gt; alone&lt;/td&gt;
&lt;td&gt;No issue&lt;/td&gt;
&lt;td&gt;No issue&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Takeaway: check &lt;code&gt;-default-isolation&lt;/code&gt; — but as diagnosis, not a fix. Flipping to the MainActor-default makes the warning vanish; that doesn't prove the code race-free. As jamieQ put it, "this is a limitation of how the region isolation analysis implementation applies its conservative rules" — if the code really is safe, a deliberate escape hatch (&lt;code&gt;nonisolated(unsafe)&lt;/code&gt;) is reasonable. An alternative from the same thread: instead of the implicit box the compiler generates for the closure capture, put the value in your own reference type with matching isolation — a wrapper like &lt;code&gt;@MainActor final class Workaround&amp;lt;T&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Escape hatches: &lt;code&gt;nonisolated(unsafe)&lt;/code&gt; and &lt;code&gt;@preconcurrency&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;nonisolated(unsafe)&lt;/code&gt; is one of the most commonly reached-for escape hatches in Swift 6 migrations — but sprinkling it everywhere can hide real data races instead of speeding up the migration. A real migration account on the Swift Forums shows the distinction: "A few legacy/spaghetti code issues came up but were resolvable with &lt;code&gt;nonisolated(unsafe)&lt;/code&gt;." The same post notes third-party libraries: "Dealing with third-party libraries was pretty easy — it was usually enough to just add &lt;code&gt;@preconcurrency&lt;/code&gt; to the import or protocol and move on." Neither is specific to 6.3 — general Swift 6 migration practice — but both remain your first line of defense moving to 6.3.&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;import&lt;/span&gt; &lt;span class="kt"&gt;Foundation&lt;/span&gt;

&lt;span class="c1"&gt;// Quickly silencing a legacy singleton (use with care — can hide a real data race)&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;LegacyCache&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;nonisolated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unsafe&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;shared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;LegacyCache&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;storage&lt;/span&gt;&lt;span class="p"&gt;:&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="kt"&gt;Data&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="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// A representative third-party protocol not yet Sendable-conformant&lt;/span&gt;
&lt;span class="kd"&gt;protocol&lt;/span&gt; &lt;span class="kt"&gt;RequestDelegate&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;didFinish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;@MainActor&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;APIClient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;@preconcurrency&lt;/span&gt; &lt;span class="kt"&gt;RequestDelegate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// @preconcurrency defers the conformance's isolation check to runtime&lt;/span&gt;
    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;didFinish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;print&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;count&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;Protocol conformance isolation is a separate layer: SE-0470 "Global-actor isolated conformances" — status "Implemented (Swift 6.2)" — pins down the rules for a type conforming to a protocol in a global-actor-isolated way (an &lt;code&gt;@MainActor&lt;/code&gt; class conforming to a &lt;code&gt;nonisolated&lt;/code&gt; protocol, say), turned on via the &lt;code&gt;InferIsolatedConformances&lt;/code&gt; upcoming-feature flag. Effect: an &lt;code&gt;@MainActor&lt;/code&gt;-marked type conforming to a protocol that doesn't require isolation may now need explicit annotation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The root of the same case: region isolation analysis
&lt;/h2&gt;

&lt;p&gt;The root of the warning has less to do with which actor a closure "resumes" on, and more with how the compiler classifies the captured variable: the &lt;code&gt;Task&lt;/code&gt;-isolated &lt;code&gt;complete&lt;/code&gt; is considered sent the moment it's captured by an &lt;code&gt;@MainActor&lt;/code&gt;-isolated closure.&lt;/p&gt;

&lt;p&gt;John McCall from the Swift team describes this in the same thread: "The problem is simply that Swift treats a capture in a closure with different isolation as a send, when what we actually need to do is reason about the isolation of the functions where the captured variable is used." The distinction matters: this only shows up with indirect captures — a variable captured by the outer closure only because it's used in a more deeply nested one. Point-Free's note on the Xcode 27/Swift 6.4 beta process shows that, in a library adopting &lt;code&gt;NonisolatedNonsendingByDefault&lt;/code&gt;, 6.4 keeps tightening checking: "Swift 6.4 seems to have started catching more issues in async code related to this setting" — meaning closure/async isolation checking keeps tightening from 6.3 toward 6.4, a gradual trend rather than a one-time jump.&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;import&lt;/span&gt; &lt;span class="kt"&gt;Foundation&lt;/span&gt;

&lt;span class="c1"&gt;// Counter-example: isolation chain is NOT broken here — shows what the&lt;/span&gt;
&lt;span class="c1"&gt;// indirect-capture pattern above does NOT look like&lt;/span&gt;
&lt;span class="kd"&gt;actor&lt;/span&gt; &lt;span class="kt"&gt;DownloadManager&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;@escaping&lt;/span&gt; &lt;span class="kd"&gt;@Sendable&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;Data&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Void&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;let&lt;/span&gt; &lt;span class="nv"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;performFetch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="c1"&gt;// Task inherits the actor's isolation; completion is @Sendable,&lt;/span&gt;
            &lt;span class="c1"&gt;// so there's no mutable capture being sent across a boundary&lt;/span&gt;
            &lt;span class="nf"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&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="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;performFetch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Data&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kt"&gt;Data&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 first question to ask in practice when you hit warnings like this: "Which actor should this closure live on, and is its isolation determined explicitly or by inference?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Gradual migration without turning warnings into errors
&lt;/h2&gt;

&lt;p&gt;The most common mistake moving to 6.3 is pulling the entire target into Swift 6 language mode in one shot. swift.org's migration guide states the rule explicitly: "In targets that adopt the Swift 6 language mode, complete checking is unconditionally on and requires no setting change." So there's no in-between; the line from a Swift Forums account — "First I switched the language version in Xcode to 6 and set concurrency checking to minimal" — actually contradicts this rule. The gradual path is the reverse: keep the target in Swift 5 mode, raise "Strict Concurrency Checking" in Xcode from minimal → targeted → complete, clean up warnings, then switch the language mode to 6.&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="c1"&gt;// swift-tools-version: 6.0&lt;/span&gt;
&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;PackageDescription&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;package&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Package&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"MyPackage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="c1"&gt;// Step 1: v5 language mode + complete checking (warnings only)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"LegacyModule"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nv"&gt;swiftSettings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;swiftLanguageMode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enableUpcomingFeature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"StrictConcurrency"&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="c1"&gt;// Step 2: swiftSettings removed, target falls back to default v6 language mode&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"MigratedModule"&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;Step two: walk back carelessly sprinkled &lt;code&gt;@MainActor&lt;/code&gt; annotations. The same forum post admits: "Looking back later, I realized I could remove some of the mainactor annotations I'd added carelessly." Reaching for &lt;code&gt;@MainActor&lt;/code&gt; everywhere as a "silence the error" reflex creates debt to clean up later — better to understand the actual data-race point the compiler flags and fix it there.&lt;/p&gt;

&lt;p&gt;Step three: when a team hits a compiler regression or an overly conservative diagnosis, open an issue and apply a temporary workaround without waiting for a language-team fix. Point-Free sums it up: "We've filed issues about these problems, but that doesn't help our users. So we've applied workarounds in the meantime." If you get stuck on a regression, add the annotation and keep moving — track the issue separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managing version conflicts in SPM packages
&lt;/h2&gt;

&lt;p&gt;A version upgrade affects not just your own code but the SPM packages you depend on — and their maintainers may have hit the same regressions first. Point-Free's experience with ComposableArchitecture and StructuredQueries is a good example. For StructuredQueries: "Swift 6.4 seems to have introduced a small type-checking regression that broke compilation of &lt;code&gt;@Table&lt;/code&gt; macro code. The fix was simple ... and we shipped a release within 2 days." A similar quick-fix cycle played out for ComposableArchitecture too (the &lt;code&gt;@MainActor&lt;/code&gt; example above).&lt;/p&gt;

&lt;p&gt;This shows a concrete order to follow for dependency management when moving to 6.3 (or later 6.4):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Check package versions first&lt;/strong&gt;: look in the changelogs of your critical packages (ones like ComposableArchitecture, StructuredQueries) for a patch note mentioning "Swift 6.3" or "isolation."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't upgrade the target without updating the package&lt;/strong&gt;: before changing your compiler version, run &lt;code&gt;swift package update&lt;/code&gt; to pull dependencies to their latest patch — maintainers have usually hit the regression before you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If a version is locked, track the issue&lt;/strong&gt;: if your &lt;code&gt;Package.resolved&lt;/code&gt; has locked a version predating 6.3, search the package's GitHub issues for "6.3" or "isolation."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temporary workaround in your own fork&lt;/strong&gt;: if a critical package hasn't shipped a fix yet — with a reflex similar to what Point-Free did on the maintainer side — open the issue and add a temporary &lt;code&gt;nonisolated(unsafe)&lt;/code&gt; or an explicit &lt;code&gt;@MainActor&lt;/code&gt; in your own integration layer while you wait for the package's official fix, rather than forking the package itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These two examples confirm something else: isolation regressions during upgrades aren't specific to your codebase — popular production libraries hit the same friction. Knowing you're not alone helps morale during the migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to expect in Swift 6.4
&lt;/h2&gt;

&lt;p&gt;No official GA date for Swift 6.4 has been announced — Xcode 27 is still in beta as of early September 2026. But swift.org's August 20, 2026 post, signed by Doug Gregor, documents concrete 6.4 Embedded Swift improvements: "Embedded Swift previously only supported existential (&lt;code&gt;any&lt;/code&gt;) types with the &lt;code&gt;AnyObject&lt;/code&gt; constraint ... Now all &lt;code&gt;any&lt;/code&gt; types, including &lt;code&gt;Any&lt;/code&gt; itself, are usable in Embedded Swift." The same post expands untyped-throws support: "Untyped throws is equivalent to throwing a value of type &lt;code&gt;any Error&lt;/code&gt;. With the generalization of &lt;code&gt;any&lt;/code&gt; types, Embedded Swift now fully supports untyped throws." Metatypes get their own section too: "Swift 6.4 brings complete support for metatypes in Embedded Swift."&lt;/p&gt;

&lt;p&gt;These changes don't target actor isolation directly, but matter for teams using Swift in embedded systems (microcontrollers, WebAssembly) — and Point-Free's experience shows 6.4's concurrency checking keeps up 6.3's tightening trend, sometimes surfacing new regressions. Advice: don't jump to the 6.4 beta before finishing your 6.3 migration — tracking the two versions' isolation-behavior differences separately makes it easier to tell which warning came from where.&lt;/p&gt;

&lt;p&gt;If your team is following the Xcode 27 betas, watching the monthly digests on the &lt;code&gt;swift.org/blog&lt;/code&gt; page (like the September 4, 2026 "What's new in Swift: August 2026 Edition") is the most reliable way not to miss the moment 6.4 approaches stable — the official announcement usually lands in sync with Xcode's own GA.&lt;/p&gt;

&lt;h2&gt;
  
  
  🏆 GOLDEN TIP: The fastest way to isolate a warning
&lt;/h2&gt;

&lt;p&gt;When you hit a new isolation warning, ask this first: at which checking level (minimal/targeted/complete) and under which default-isolation setting (nonisolated/MainActor) does it appear? As in Itai Ferber's forum finding, the same code can produce four different outcomes under four different configurations. Temporarily flipping your target's &lt;code&gt;SWIFT_STRICT_CONCURRENCY&lt;/code&gt; and &lt;code&gt;-default-isolation&lt;/code&gt; flags and recompiling the same file lets you tell, within a few minutes, whether the warning is a real data race or a configuration-dependent compiler behavior. But remember: this is a diagnostic tool, not a fix — a configuration where the warning disappears doesn't prove the code is safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;p&gt;We put together a short checklist you can keep on hand to figure out whether your project is really ready to move to Swift 6.3. Check off the items below in order; each one corresponds to a real warning case or escape hatch mentioned in this post.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Note your target's default-isolation setting (nonisolated or MainActor) and its concurrency checking level (minimal/targeted/complete)&lt;/li&gt;
&lt;li&gt;List every function that uses a Sendable + escaping closure combination&lt;/li&gt;
&lt;li&gt;Flag every line using nonisolated(unsafe) and re-review whether it's genuinely safe&lt;/li&gt;
&lt;li&gt;Scan third-party imports for places that need @preconcurrency added&lt;/li&gt;
&lt;li&gt;Check the Swift 6.3 compatibility notes for your critical dependencies (especially actor/concurrency-heavy packages)&lt;/li&gt;
&lt;li&gt;Staying in Swift 5 language mode, raise the checking level from minimal to targeted to complete, and switch the language mode to 6 last&lt;/li&gt;
&lt;li&gt;Re-review any @MainActor annotations added carelessly during migration once the migration is done&lt;/li&gt;
&lt;li&gt;Fully finish your 6.3 migration before moving to the Xcode 27 beta — don't mix the isolation behavior of the two versions&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What code breaks when moving from Swift 6.2 to 6.3?
&lt;/h3&gt;

&lt;p&gt;The most common breaking point is code using &lt;code&gt;@Sendable @escaping&lt;/code&gt; closures under the combination of &lt;code&gt;nonisolated&lt;/code&gt; module default and &lt;code&gt;Complete&lt;/code&gt; concurrency checking. In a case documented on the Swift Forums, this combination compiled silently on 6.2 but starts producing a "risks causing data races" warning on 6.3. The general rule: since a declaration's isolation status (&lt;code&gt;isolated&lt;/code&gt;/&lt;code&gt;nonisolated&lt;/code&gt;) is part of its API contract, a version upgrade can change how that contract is checked.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is actor isolation stricter in Swift 6.3?
&lt;/h3&gt;

&lt;p&gt;The official 6.3 announcement doesn't declare a new concurrency feature; even so, some patterns that compiled silently on 6.2 produce warnings on 6.3. In the documented case, the source isn't a new rule at all, but the conservative behavior of region isolation analysis: a mutable variable captured in a closure with different isolation is treated as "sent."&lt;/p&gt;

&lt;h3&gt;
  
  
  When will Swift 6.4 be released?
&lt;/h3&gt;

&lt;p&gt;No exact GA date has been officially announced. What's known: Xcode 27 (which includes Swift 6.4) has been in beta since June 2026 and was at beta 6 as of August 24, 2026. swift.org's August 20, 2026 Embedded Swift post also describes 6.4 as still "coming" — as of early September 2026, 6.4 has not been officially released.&lt;/p&gt;

&lt;h3&gt;
  
  
  When should I use nonisolated vs. @concurrent?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;nonisolated(nonsending)&lt;/code&gt; is suited for helper functions that are a logical part of the caller's work and don't need to hop to a separate thread — such functions stay on the caller's actor. &lt;code&gt;@concurrent&lt;/code&gt;, on the other hand, is for genuinely expensive work that must run in parallel and shouldn't block the caller's actor — it's used to explicitly request the old "always run on a separate executor" behavior. When the &lt;code&gt;NonisolatedNonsendingByDefault&lt;/code&gt; flag is on, plain &lt;code&gt;nonisolated async&lt;/code&gt; functions stay on the caller's actor by default; you need to explicitly mark any function where you want to keep the old behavior with &lt;code&gt;@concurrent&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which build settings should I check before moving to 6.3?
&lt;/h3&gt;

&lt;p&gt;Check your target's &lt;code&gt;-default-isolation&lt;/code&gt; flag (MainActor or nonisolated) and its &lt;code&gt;SWIFT_STRICT_CONCURRENCY&lt;/code&gt; level (minimal/targeted/complete). Per the forum finding in this post, the same code produces different results on 6.3 depending on the combination of these two settings — the documented warning doesn't appear in projects with the MainActor-default turned on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I clean up my existing nonisolated(unsafe) usages before moving to 6.3?
&lt;/h3&gt;

&lt;p&gt;Not mandatory, but recommended. &lt;code&gt;nonisolated(unsafe)&lt;/code&gt; should remain a temporary escape hatch; the 6.3 migration is a good opportunity to review whether these annotations are still necessary or are simply leftovers from an old "silence it and move on" reflex.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Migrating to Swift 6.3 is less about learning a new language feature and more about understanding how the compiler now checks the actor isolation defaults introduced by 6.2 (SE-0466, SE-0461). The documented case teaches the lesson on its own: a mutable variable indirectly captured behind a &lt;code&gt;Sendable&lt;/code&gt;+&lt;code&gt;escaping&lt;/code&gt; signature runs into the conservative rule of region isolation analysis. Protocol conformance isolation (SE-0470) ties back to the same root: isolation is now a checked part of the API contract. If you want to refresh Swift's strict concurrency foundations before this migration, the &lt;a href="https://muhittincamdali.com/blog/swift-6-strict-concurrency-derin-rehber-2026/" rel="noopener noreferrer"&gt;Swift 6 Strict Concurrency Deep Guide&lt;/a&gt; is a good starting point; if you're looking for a step-by-step migration plan, the &lt;a href="https://muhittincamdali.com/en/blog/swift-6-strict-concurrency-migration-playbook/" rel="noopener noreferrer"&gt;Swift 6 Strict Concurrency Migration Playbook&lt;/a&gt; complements this post.&lt;/p&gt;

&lt;p&gt;If you want a deeper look at how actors fundamentally work, check out &lt;a href="https://muhittincamdali.com/blog/swift-concurrency-actors/" rel="noopener noreferrer"&gt;Swift Concurrency and Actors&lt;/a&gt;; for the &lt;code&gt;Task&lt;/code&gt; and &lt;code&gt;TaskGroup&lt;/code&gt; side of structured concurrency, see &lt;a href="https://muhittincamdali.com/blog/swift-structured-concurrency/" rel="noopener noreferrer"&gt;Swift Structured Concurrency&lt;/a&gt;. For projects using actors in distributed systems, &lt;a href="https://muhittincamdali.com/blog/swift-distributed-actors/" rel="noopener noreferrer"&gt;Swift Distributed Actors&lt;/a&gt; covers the beyond-the-network isolation scenarios this post doesn't. You can also refresh the background on the general concurrency updates that came with Swift 6.2 at WWDC26 from &lt;a href="https://muhittincamdali.com/en/blog/swift-6-2-concurrency-yenilikleri-wwdc26/" rel="noopener noreferrer"&gt;Swift 6.2 Concurrency Innovations&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One last note: every example in this article was written against 6.3.3 (today's stable release). If you've started trying Swift 6.4 with the Xcode 27 beta, keep in mind that isolation behavior may differ from 6.3 — keeping the two versions' regressions separate in the same migration process makes it easier to tell where each warning is coming from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.swift.org/blog/swift-6.3-released/" rel="noopener noreferrer"&gt;Swift 6.3 Released&lt;/a&gt; — official 6.3 announcement, March 24, 2026, summary of language changes&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://forums.swift.org/t/announcing-swift-6-3-3/87888" rel="noopener noreferrer"&gt;Announcing Swift 6.3.3&lt;/a&gt; — June 30, 2026 patch announcement, current stable release&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.swift.org/blog/embedded-swift-improvements-coming-in-swift-6.4/" rel="noopener noreferrer"&gt;Embedded Swift Improvements Coming in Swift 6.4&lt;/a&gt; — August 20, 2026, Doug Gregor, official evidence that 6.4 hasn't shipped yet&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0466-control-default-actor-isolation.md" rel="noopener noreferrer"&gt;SE-0466: Control Default Actor Isolation&lt;/a&gt; — module-wide default isolation proposal, Implemented in 6.2&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/swiftlang/swift-evolution/blob/main/proposals/0461-async-function-isolation.md" rel="noopener noreferrer"&gt;SE-0461: Run nonisolated async functions on the caller's actor&lt;/a&gt; — the &lt;code&gt;nonisolated(nonsending)&lt;/code&gt; proposal, Implemented in 6.2&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://forums.swift.org/t/new-swift-6-3-isolation-warnings-with-sendable-x-escaping-escape-hatch/84998" rel="noopener noreferrer"&gt;New Swift 6.3 isolation warnings with Sendable x escaping escape hatch&lt;/a&gt; — Itai Ferber, Swift Forums, February 25, 2026, the primary source for this post&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://forums.swift.org/t/my-experience-attempting-a-migration-to-swift-6/79069" rel="noopener noreferrer"&gt;My experience (attempting) a migration to swift 6&lt;/a&gt; — Swift Forums, April 3, 2025, a gradual-migration account from the Swift 6.0 era&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.pointfree.co/blog/posts/218-xcode-27-support-in-the-point-free-ecosystem" rel="noopener noreferrer"&gt;Xcode 27 Support in the Point-Free Ecosystem&lt;/a&gt; — Point-Free, real regression-fix examples from ComposableArchitecture/StructuredQueries&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.swift.org/migration/documentation/swift-6-concurrency-migration-guide/enabledataracesafety/" rel="noopener noreferrer"&gt;Enable data-race safety checking&lt;/a&gt; — swift.org migration guide, language mode and checking level rules&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.swift.org/blog/whats-new-in-swift-august-2026/" rel="noopener noreferrer"&gt;What's new in Swift: August 2026 Edition&lt;/a&gt; — swift.org, September 4, 2026, monthly digest&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://muhittincamdali.com/en/blog/swift-6-3-gecis-actor-isolation-kirilanlar/" rel="noopener noreferrer"&gt;muhittincamdali.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

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