<?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: swainwri</title>
    <description>The latest articles on DEV Community by swainwri (@swainwri).</description>
    <link>https://dev.to/swainwri</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%2F1642828%2Fdee0e62a-63c1-45c2-8f69-2c02b8043cf1.jpeg</url>
      <title>DEV Community: swainwri</title>
      <link>https://dev.to/swainwri</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/swainwri"/>
    <language>en</language>
    <item>
      <title>I Got Tired of Hand-Translating Xcode Plists, So I Built a Tiny Free Tool to Do It On-Device</title>
      <dc:creator>swainwri</dc:creator>
      <pubDate>Sun, 19 Jul 2026 03:31:47 +0000</pubDate>
      <link>https://dev.to/swainwri/i-got-tired-of-hand-translating-xcode-plists-so-i-built-a-tiny-free-tool-to-do-it-on-device-3cod</link>
      <guid>https://dev.to/swainwri/i-got-tired-of-hand-translating-xcode-plists-so-i-built-a-tiny-free-tool-to-do-it-on-device-3cod</guid>
      <description>&lt;p&gt;If you've ever localized an iOS or macOS app, you know the drill: you've got a &lt;code&gt;.plist&lt;/code&gt; file full of UI strings sitting in &lt;code&gt;Base.lproj&lt;/code&gt;, and now you need the same file in &lt;code&gt;fr.lproj&lt;/code&gt;, &lt;code&gt;th.lproj&lt;/code&gt;, &lt;code&gt;de.lproj&lt;/code&gt;... and so on for every language you support. You can pay for a translation service, hand it to a freelancer, or sit there manually retyping strings into fifteen copies of the same file.&lt;/p&gt;

&lt;p&gt;I didn't want to do any of those things, so I spent a day building &lt;strong&gt;pListTranslatorApp&lt;/strong&gt; — a small SwiftUI macOS app that opens an Xcode plist, finds the string values you want translated, batch-translates them using Apple's built-in Translation framework, and writes out a ready-to-use localized plist. It's free, it runs entirely on-device, and it's now sitting on GitHub for anyone who wants it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint that shaped the whole thing
&lt;/h2&gt;

&lt;p&gt;I'm developing on a MacBook Air with a 256GB SSD and 8GB of RAM — not a lot of headroom. That ruled out a couple of obvious approaches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cloud translation APIs&lt;/strong&gt; (DeepL, Google Cloud Translate, etc.) work well, but even a generous free tier means an external dependency, an API key to manage, and a network round-trip for something that's ultimately just short UI strings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Downloading a pile of on-device language models "just in case"&lt;/strong&gt; eats real disk space fast — Apple's Translation framework models are shared system-wide across apps, which helps, but they still add up if you're not paying attention.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the app leans entirely on &lt;strong&gt;Apple's &lt;code&gt;Translation&lt;/code&gt; framework&lt;/strong&gt;, downloading exactly one language pack at a time, on demand, and lets you delete it once you're done with that language.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually does
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Open a plist.&lt;/strong&gt; Pick any Xcode &lt;code&gt;.plist&lt;/code&gt; via a standard file importer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose which keys to translate.&lt;/strong&gt; Originally I hardcoded it to look for a key called &lt;code&gt;itemTitle&lt;/code&gt;, but that's obviously too narrow for anyone else's project — so it's now a comma-separated text field. Type &lt;code&gt;itemTitle, title, label&lt;/code&gt; and it'll pick up all of them, anywhere in the plist's nested dictionaries and arrays.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick a target language&lt;/strong&gt; from a picker (Thai, French, German, Spanish, Japanese — easy to extend).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Translate All.&lt;/strong&gt; The app fires a batch of translation requests through &lt;code&gt;TranslationSession&lt;/code&gt;. If the language pack isn't installed yet, macOS's own system prompt pops up to download it — no extra UI work needed on my end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Save.&lt;/strong&gt; Writes a new plist alongside the original, via a proper save panel, ready to drop into the matching &lt;code&gt;.lproj&lt;/code&gt; folder.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There's also a small QA feature I added once I realized machine translation, however good, still benefits from a human sanity check: the app can optionally write the original English string alongside the translation under a &lt;code&gt;key_en&lt;/code&gt; entry, so a native speaker can review both side by side in the same file without needing a diff tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotchas that ate most of the day
&lt;/h2&gt;

&lt;p&gt;None of this was as simple as "call the Translation API and done." A few things tripped me up that I couldn't find clearly documented anywhere, so I'm sharing them here in case they save someone else the debugging time:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The sandbox doesn't grant you a folder, only a file.&lt;/strong&gt; &lt;code&gt;startAccessingSecurityScopedResource()&lt;/code&gt; on the plist you opened only covers that one file. Trying to write a new file into the same directory afterward throws a permissions error, even though it &lt;em&gt;looks&lt;/em&gt; like the same folder. The fix was routing the save through &lt;code&gt;NSSavePanel&lt;/code&gt;, which grants scoped access to whatever the user confirms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There are two very different ways to start a &lt;code&gt;TranslationSession&lt;/code&gt;, and only one can prompt for a download.&lt;/strong&gt; &lt;code&gt;TranslationSession(installedSource:target:)&lt;/code&gt; assumes the language pack already exists and just throws &lt;code&gt;.notInstalled&lt;/code&gt; if it doesn't — it has no UI to fall back on. The &lt;code&gt;.translationTask(configuration:)&lt;/code&gt; view modifier, on the other hand, can trigger the system's own "download this language" prompt automatically. If your translation silently fails with &lt;code&gt;notInstalled&lt;/code&gt;, this is almost certainly why.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;.translationTask&lt;/code&gt; won't always re-fire on a second run.&lt;/strong&gt; If you reuse the same source/target language for a second batch (a very normal thing to do — translating multiple plists into the same target language), SwiftUI can treat the new &lt;code&gt;Configuration&lt;/code&gt; as unchanged and skip re-running the task entirely. Explicitly invalidating and nil-ing the old configuration before assigning a new one helps, but the reliable fix was attaching an &lt;code&gt;.id()&lt;/code&gt; to the modifier, keyed on the language and item count, forcing SwiftUI to treat each run as a genuinely new task.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't rely on dictionary iteration order to match strings back to their slots.&lt;/strong&gt; My first pass used a simple iterator to walk extracted strings back into the plist tree in the same order they came out. That works right up until &lt;code&gt;PropertyListSerialization&lt;/code&gt; parses the file a second time (on save) and iterates its dictionary keys in a different order than the first parse did — silently shifting every translation over by one slot. The fix was recording each string's &lt;em&gt;exact path&lt;/em&gt; through the plist (which key, which array index, at every level) at extraction time, then writing translations back to that exact path rather than relying on sequence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The whole thing is open source on GitHub: &lt;strong&gt;&lt;a href="https://github.com/swainwri/PListTranslator" rel="noopener noreferrer"&gt;https://github.com/swainwri/PListTranslator&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's a small, opinionated tool built for a specific workflow — Xcode plist localization — but if you're building something similar, the source might save you the same afternoon of &lt;code&gt;TranslationError&lt;/code&gt; debugging it cost me. Issues and PRs welcome, especially if you want to extend it to &lt;code&gt;.strings&lt;/code&gt;/&lt;code&gt;.stringsdict&lt;/code&gt; files, which is the natural next step.&lt;/p&gt;

&lt;p&gt;If you use it and it's useful, or if your Thai/French/Italian friends find the output questionable, I'd love to hear about it.&lt;/p&gt;

</description>
      <category>swift</category>
      <category>ios</category>
      <category>macos</category>
    </item>
    <item>
      <title>Legacy Mini Split Views in UIKit</title>
      <dc:creator>swainwri</dc:creator>
      <pubDate>Thu, 29 Jan 2026 22:10:48 +0000</pubDate>
      <link>https://dev.to/swainwri/legacy-mini-split-views-in-uikit-22ne</link>
      <guid>https://dev.to/swainwri/legacy-mini-split-views-in-uikit-22ne</guid>
      <description>&lt;h2&gt;
  
  
  Why I Stopped Fighting UISplitViewController and Built My Own
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;“Storyboards aren’t the problem. Implicit storyboards are.”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This article documents a &lt;strong&gt;real-world, multi-hour debugging session&lt;/strong&gt; that ended with a clean, predictable UIKit architecture — and a small demo repo you can learn from.&lt;/p&gt;

&lt;p&gt;Repo:&lt;br&gt;
👉 &lt;strong&gt;&lt;a href="https://github.com/swainwri/LegacyMiniSplitDemo" rel="noopener noreferrer"&gt;https://github.com/swainwri/LegacyMiniSplitDemo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you’ve ever:&lt;br&gt;
    • Fought floating detail controllers&lt;br&gt;
    • Had &lt;code&gt;navigationController == nil&lt;/code&gt; when it “shouldn’t be”&lt;br&gt;
    • Watched UIKit silently replace your view hierarchy&lt;br&gt;
    • Or wondered why Split View behaves differently every OS release…&lt;/p&gt;

&lt;p&gt;This is for you.&lt;/p&gt;

&lt;p&gt;⸻&lt;/p&gt;
&lt;h2&gt;
  
  
  The problem we were trying to solve
&lt;/h2&gt;

&lt;p&gt;I had a legacy &lt;code&gt;UIKit&lt;/code&gt; app that needed:&lt;br&gt;
    • A &lt;strong&gt;master / detail layout on iPad&lt;/strong&gt;&lt;br&gt;
    • A &lt;strong&gt;stacked navigation flow on iPhone&lt;/strong&gt;&lt;br&gt;
    • Predictable behavior across iOS versions&lt;br&gt;
    • Zero &lt;code&gt;UIKit&lt;/code&gt; “magic”&lt;br&gt;
    • No &lt;code&gt;SwiftUI&lt;/code&gt;&lt;br&gt;
    • No new navigation frameworks&lt;/p&gt;

&lt;p&gt;In theory, this is exactly what &lt;code&gt;UISplitViewController&lt;/code&gt; is for.&lt;/p&gt;

&lt;p&gt;In practice… it wasn’t.&lt;/p&gt;

&lt;p&gt;⸻&lt;/p&gt;
&lt;h2&gt;
  
  
  What went wrong with &lt;code&gt;UISplitViewController&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Over the course of hours, we hit all the classics:&lt;br&gt;
    • Floating detail controllers instead of tiled layouts&lt;br&gt;
    • Master views disappearing&lt;br&gt;
    • Detail views appearing twice&lt;br&gt;
    • Navigation stacks duplicating&lt;br&gt;
    • showDetailViewController doing different things per device&lt;br&gt;
    • preferredDisplayMode, preferredSplitBehavior, style… none behaving consistently&lt;br&gt;
    • iPhone collapsing rules leaking into iPad behavior&lt;br&gt;
    • Storyboards loading even when you thought they weren’t&lt;/p&gt;

&lt;p&gt;The core issue wasn’t misuse.&lt;/p&gt;

&lt;p&gt;The core issue was &lt;strong&gt;loss of control&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;UIKit&lt;/code&gt; was deciding too much for us.&lt;/p&gt;

&lt;p&gt;⸻&lt;/p&gt;
&lt;h2&gt;
  
  
  The real root cause (this is important)
&lt;/h2&gt;

&lt;p&gt;The actual failure mode was a combination of:&lt;br&gt;
    1.  &lt;strong&gt;Implicit storyboard loading&lt;/strong&gt;&lt;br&gt;
    2.  &lt;strong&gt;Implicit Split View lifecycle&lt;/strong&gt;&lt;br&gt;
    3.  &lt;strong&gt;Navigation controllers created behind your back&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Even when you think you’re “configuring” Split View, UIKit has already made decisions you can’t undo.&lt;/p&gt;

&lt;p&gt;At some point the question became:&lt;/p&gt;

&lt;p&gt;Why am I fighting a controller that exists to make decisions for me?&lt;/p&gt;

&lt;p&gt;⸻&lt;/p&gt;
&lt;h2&gt;
  
  
  The pivot: stop using &lt;code&gt;UISplitViewController&lt;/code&gt; entirely
&lt;/h2&gt;

&lt;p&gt;Instead of bending &lt;code&gt;UIKit&lt;/code&gt; to behave like a legacy split view…&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We built one&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Not with hacks.&lt;br&gt;
Not with layout tricks.&lt;br&gt;
Just honest UIKit.&lt;/p&gt;

&lt;p&gt;⸻&lt;/p&gt;
&lt;h2&gt;
  
  
  The new architecture
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;One custom container&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;LegacySplitViewController&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;It owns:&lt;br&gt;
    • A &lt;strong&gt;master container view&lt;/strong&gt;&lt;br&gt;
    • A &lt;strong&gt;detail container view&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That’s it.&lt;/p&gt;

&lt;p&gt;No Split View APIs.&lt;br&gt;
No collapsing rules.&lt;br&gt;
No adaptive magic.&lt;/p&gt;

&lt;p&gt;Just two child navigation controllers.&lt;/p&gt;

&lt;p&gt;⸻&lt;/p&gt;
&lt;h2&gt;
  
  
  Storyboards: yes — but intentionally
&lt;/h2&gt;

&lt;p&gt;This project &lt;strong&gt;does use storyboards&lt;/strong&gt;, but very deliberately:&lt;br&gt;
    • Main_iPad.storyboard&lt;br&gt;
    • Main_iPhone.storyboard&lt;/p&gt;

&lt;p&gt;And crucially:&lt;/p&gt;

&lt;p&gt;❌ No Main.storyboard&lt;br&gt;
❌ No UIMainStoryboardFile&lt;br&gt;
❌ No UISceneStoryboardFile&lt;/p&gt;

&lt;p&gt;in &lt;strong&gt;Info.plist&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Storyboards are loaded &lt;strong&gt;explicitly&lt;/strong&gt; in SceneDelegate, based on device idiom.&lt;/p&gt;

&lt;p&gt;This is the key difference.&lt;/p&gt;

&lt;p&gt;⸻&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;code&gt;SceneDelegate&lt;/code&gt;: where control lives again
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if UIDevice.current.userInterfaceIdiom == .pad {
    window.rootViewController = makePadRoot()
} else {
    window.rootViewController = makePhoneRoot()
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This one decision:&lt;br&gt;
    • Eliminates ambiguity&lt;br&gt;
    • Prevents UIKit auto-loading&lt;br&gt;
    • Makes navigation reasoning trivial again&lt;/p&gt;

&lt;p&gt;⸻&lt;/p&gt;
&lt;h2&gt;
  
  
  iPad behavior (manual, predictable)
&lt;/h2&gt;

&lt;p&gt;On iPad:&lt;br&gt;
    • &lt;code&gt;LegacySplitViewController&lt;/code&gt; is root&lt;br&gt;
    • It contains:&lt;br&gt;
    • &lt;code&gt;masterNav&lt;/code&gt;&lt;br&gt;
    • &lt;code&gt;detailNav&lt;/code&gt;&lt;br&gt;
    • Selecting an item replaces &lt;strong&gt;the detail navigation stack&lt;/strong&gt;&lt;br&gt;
    • The master never disappears unless you hide it&lt;/p&gt;

&lt;p&gt;No floating.&lt;br&gt;
No duplication.&lt;br&gt;
No “why is this a popover”.&lt;/p&gt;

&lt;p&gt;⸻&lt;/p&gt;
&lt;h2&gt;
  
  
  iPhone behavior (stacked, natural)
&lt;/h2&gt;

&lt;p&gt;On iPhone:&lt;br&gt;
    • No split container&lt;br&gt;
    • A single &lt;code&gt;UINavigationController&lt;/code&gt;&lt;br&gt;
    • Selecting an item simply pushes&lt;/p&gt;

&lt;p&gt;Same code paths.&lt;br&gt;
Different roots.&lt;br&gt;
Zero conditionals deep in your view controllers.&lt;/p&gt;

&lt;p&gt;⸻&lt;/p&gt;
&lt;h2&gt;
  
  
  Navigation logic (simplified at last)
&lt;/h2&gt;

&lt;p&gt;From the master:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if UIDevice.current.userInterfaceIdiom == .pad {
    detailNav?.setViewControllers([vc], animated: false)
} else {
    masterNav?.pushViewController(vc, animated: true)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it.&lt;/p&gt;

&lt;p&gt;No showDetailViewController.&lt;br&gt;
No guessing what UIKit will do.&lt;br&gt;
No inspecting parent chains at runtime.&lt;/p&gt;

&lt;p&gt;⸻&lt;/p&gt;

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

&lt;p&gt;The mistake wasn’t “using UISplitViewController wrong”.&lt;/p&gt;

&lt;p&gt;The mistake was assuming it was still the right abstraction for:&lt;br&gt;
    • Legacy UIKit apps&lt;br&gt;
    • Explicit navigation&lt;br&gt;
    • Predictable behavior&lt;br&gt;
    • Multi-year maintenance&lt;/p&gt;

&lt;p&gt;For new &lt;code&gt;SwiftUI&lt;/code&gt; apps? Fine.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;UIKit&lt;/code&gt; apps that must remain sane?&lt;br&gt;
&lt;strong&gt;A manual split is often better&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;⸻&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s intentionally missing
&lt;/h2&gt;

&lt;p&gt;This demo does not include:&lt;br&gt;
    • Sidebar collapse/expand gestures&lt;br&gt;
    • Interactive resizing&lt;br&gt;
    • Fancy adaptive transitions&lt;/p&gt;

&lt;p&gt;Those can be added — &lt;em&gt;now that the foundation is solid&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The point of this repo is correctness first.&lt;/p&gt;

&lt;p&gt;⸻&lt;/p&gt;

&lt;h2&gt;
  
  
  Final takeaway
&lt;/h2&gt;

&lt;p&gt;If you remember one thing:&lt;/p&gt;

&lt;p&gt;UIKit works best when it does &lt;strong&gt;less&lt;/strong&gt;, not more.&lt;/p&gt;

&lt;p&gt;By removing:&lt;br&gt;
    • Implicit storyboards&lt;br&gt;
    • Implicit split behavior&lt;br&gt;
    • Implicit navigation rules&lt;/p&gt;

&lt;p&gt;…we ended up with &lt;strong&gt;simpler code&lt;/strong&gt;, not more.&lt;/p&gt;

&lt;p&gt;⸻&lt;/p&gt;

&lt;h2&gt;
  
  
  Repo
&lt;/h2&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/swainwri/LegacyMiniSplitDemo" rel="noopener noreferrer"&gt;https://github.com/swainwri/LegacyMiniSplitDemo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Clone it.&lt;br&gt;
Break it.&lt;br&gt;
Improve it.&lt;/p&gt;

&lt;p&gt;And next time &lt;code&gt;UIKit&lt;/code&gt; gaslights you — build the container yourself.&lt;/p&gt;

</description>
      <category>uisplitviewcontroller</category>
    </item>
    <item>
      <title>Swift 6 + MultipeerConnectivity</title>
      <dc:creator>swainwri</dc:creator>
      <pubDate>Mon, 19 Jan 2026 03:51:50 +0000</pubDate>
      <link>https://dev.to/swainwri/swift-6-multipeerconnectivity-2mdn</link>
      <guid>https://dev.to/swainwri/swift-6-multipeerconnectivity-2mdn</guid>
      <description>&lt;h2&gt;
  
  
  A Practical, Concurrency‑Safe Tutorial (UIKit, Actors, and Real Devices)
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Audience:&lt;/strong&gt; iOS developers who already know UIKit &amp;amp; basic MultipeerConnectivity and want a &lt;em&gt;correct&lt;/em&gt;, Swift&amp;nbsp;6–compliant architecture that survives real devices, real timing, and real bugs.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why this tutorial exists
&lt;/h2&gt;

&lt;p&gt;Most MultipeerConnectivity (MPC) examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ignore concurrency&lt;/li&gt;
&lt;li&gt;hide delegate chaos behind singletons&lt;/li&gt;
&lt;li&gt;work in the simulator, then fail on devices&lt;/li&gt;
&lt;li&gt;break completely under Swift&amp;nbsp;6&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This tutorial documents a &lt;strong&gt;production‑grade approach&lt;/strong&gt; using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Swift&amp;nbsp;6 actors&lt;/li&gt;
&lt;li&gt;explicit isolation boundaries&lt;/li&gt;
&lt;li&gt;UIKit (not SwiftUI)&lt;/li&gt;
&lt;li&gt;real devices&lt;/li&gt;
&lt;li&gt;deterministic peer identity and state handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything here was built, debugged, and fixed the hard way.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architectural overview
&lt;/h2&gt;

&lt;p&gt;We separate responsibilities &lt;strong&gt;strictly&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UIKit (ViewController)
        ↓
PeerSessionManager   (@MainActor)
        ↓
MPCActor             (actor)
        ↓
MCSession / Browser / Advertiser (delegates)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why this matters
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;UIKit must stay on the main thread&lt;/li&gt;
&lt;li&gt;MultipeerConnectivity delegates are &lt;em&gt;not&lt;/em&gt; main‑safe&lt;/li&gt;
&lt;li&gt;Swift&amp;nbsp;6 enforces rules older code only implied&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Actors are not optional — they are the design.&lt;/p&gt;




&lt;h2&gt;
  
  
  Core types
&lt;/h2&gt;

&lt;h3&gt;
  
  
  PeerSnapshot (immutable, UI‑safe)
&lt;/h3&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;PeerSnapshot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Identifiable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Equatable&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;displayName&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the &lt;strong&gt;only&lt;/strong&gt; peer object the UI ever sees.&lt;/p&gt;




&lt;h3&gt;
  
  
  PeerConnectionState
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="kt"&gt;PeerConnectionState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;notConnected&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;connecting&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;connected&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Never expose &lt;code&gt;MCSessionState&lt;/code&gt; to the UI.&lt;/p&gt;




&lt;h2&gt;
  
  
  MPCActor (the authority)
&lt;/h2&gt;

&lt;p&gt;The actor owns &lt;strong&gt;all mutable MPC state&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MCPeerID ↔ UUID mapping&lt;/li&gt;
&lt;li&gt;peer records&lt;/li&gt;
&lt;li&gt;discovery lifecycle&lt;/li&gt;
&lt;li&gt;session callbacks
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;actor&lt;/span&gt; &lt;span class="kt"&gt;MPCActor&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;uuidByPeerID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;MCPeerID&lt;/span&gt;&lt;span class="p"&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="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;peerIDByUUID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;MCPeerID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[:]&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;peersByID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;PeerRecord&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;didDiscoverPeer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;peerID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;MCPeerID&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;PeerSnapshot&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;uuid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;uuidByPeerID&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;peerID&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;snapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;peersByID&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&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;snapshot&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;snapshot&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;uuid&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="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;snapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;PeerSnapshot&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;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;peerID&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;uuidByPeerID&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;peerID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;
        &lt;span class="n"&gt;peerIDByUUID&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&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;peerID&lt;/span&gt;
        &lt;span class="n"&gt;peersByID&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;PeerRecord&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;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;peerID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;peerID&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;snapshot&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;snapshot&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;Key rule:&lt;/strong&gt;  &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;UUIDs are created once, inside the actor, and never guessed elsewhere.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  PeerSessionManager (@MainActor)
&lt;/h2&gt;

&lt;p&gt;This is the &lt;strong&gt;bridge&lt;/strong&gt; between concurrency and UIKit.&lt;/p&gt;

&lt;p&gt;Responsibilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;owns UI‑safe arrays&lt;/li&gt;
&lt;li&gt;exposes callbacks&lt;/li&gt;
&lt;li&gt;translates actor events → UI updates
&lt;/li&gt;
&lt;/ul&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;PeerSessionManager&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;PeerSessionManager&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;mpc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;MPCActor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="kd"&gt;private(set)&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;peers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;PeerSnapshot&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="kd"&gt;private(set)&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;peerStates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;PeerConnectionState&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[:]&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;onPeersUpdated&lt;/span&gt;&lt;span class="p"&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="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;onPeerStateChanged&lt;/span&gt;&lt;span class="p"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing async leaks into UIKit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Delegate bridging (the critical pattern)
&lt;/h2&gt;

&lt;p&gt;MultipeerConnectivity delegates &lt;strong&gt;cannot be actors&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So we bridge safely:&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;session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;MCSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;peer&lt;/span&gt; &lt;span class="nv"&gt;peerID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;MCPeerID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;didChange&lt;/span&gt; &lt;span class="nv"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;MCSessionState&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;actor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;peerStateChangedFromDelegate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;peerID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;state&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;Inside the actor:&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;peerStateChangedFromDelegate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;peerID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;MCPeerID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;MCSessionState&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;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;handlePeerStateChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;peerID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;peerID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;state&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;Then:&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;private&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;handlePeerStateChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;peerID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;MCPeerID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;MCSessionState&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;uuid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;uuidByPeerID&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;peerID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;mapped&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;PeerConnectionState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connected&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;connected&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connecting&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;connecting&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notConnected&lt;/span&gt;

    &lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stateChanged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mapped&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;This is the Swift&amp;nbsp;6‑correct flow.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Invitation handling (the subtle bug)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Wrong approach:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Calling &lt;code&gt;invitationHandler(true, session)&lt;/code&gt; immediately.&lt;/p&gt;

&lt;p&gt;Why it breaks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the sender flips to &lt;code&gt;.connected&lt;/code&gt; too early&lt;/li&gt;
&lt;li&gt;UI state desyncs&lt;/li&gt;
&lt;li&gt;debugging becomes impossible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Correct approach:&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;func&lt;/span&gt; &lt;span class="nf"&gt;advertiser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;advertiser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;MCNearbyServiceAdvertiser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;didReceiveInvitationFromPeer&lt;/span&gt; &lt;span class="nv"&gt;peerID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;MCPeerID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;withContext&lt;/span&gt; &lt;span class="nv"&gt;context&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="nv"&gt;invitationHandler&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;Bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;MCSession&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="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;manager&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="n"&gt;manager&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;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;invitationHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="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;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;snapshot&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;manager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mpcActorSnapshot&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;peerID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;manager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invitationReceived&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;snapshot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;accept&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
                &lt;span class="nf"&gt;invitationHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;accept&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nv"&gt;session&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;nil&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;p&gt;&lt;strong&gt;UI decides. Transport waits.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  UITableView state rendering
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;peerSnapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ps&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;indexPath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;row&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;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ps&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peerStates&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;peerSnapshot&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notConnected&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this returns &lt;code&gt;.notConnected&lt;/code&gt; incorrectly, you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;duplicate UUIDs&lt;/li&gt;
&lt;li&gt;mismatched snapshot sources&lt;/li&gt;
&lt;li&gt;actor bypassing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Never patch this in the UI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common MPC + Swift&amp;nbsp;6 mistakes (Appendix)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Creating UUIDs outside the actor
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; duplicate peers, state never updates&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; one source of truth&lt;/p&gt;


&lt;h3&gt;
  
  
  2. Calling async APIs from delegate synchronously
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Swift warnings, race conditions&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; &lt;code&gt;nonisolated&lt;/code&gt; → &lt;code&gt;Task&lt;/code&gt; → &lt;code&gt;await&lt;/code&gt;&lt;/p&gt;


&lt;h3&gt;
  
  
  3. Updating UI from the actor
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; random crashes, undefined behavior&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; actor emits → manager updates → UI reloads&lt;/p&gt;


&lt;h3&gt;
  
  
  4. Treating &lt;code&gt;@MainActor&lt;/code&gt; as a thread
&lt;/h3&gt;

&lt;p&gt;It is &lt;strong&gt;not&lt;/strong&gt; a queue.&lt;br&gt;
It is a &lt;strong&gt;semantic boundary&lt;/strong&gt;.&lt;/p&gt;


&lt;h3&gt;
  
  
  5. Fighting Swift&amp;nbsp;6 diagnostics
&lt;/h3&gt;

&lt;p&gt;The compiler is not being pedantic.&lt;br&gt;
It is preventing bugs you &lt;em&gt;will&lt;/em&gt; hit.&lt;/p&gt;


&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;MultipeerConnectivity is not hard.&lt;br&gt;
&lt;strong&gt;State ownership is.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Swift&amp;nbsp;6 didn’t make this worse — it made the bugs visible.&lt;/p&gt;

&lt;p&gt;If you respect isolation, identity, and flow direction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MPC works&lt;/li&gt;
&lt;li&gt;UI stays correct&lt;/li&gt;
&lt;li&gt;debugging becomes sane&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This architecture scales.&lt;/p&gt;



&lt;p&gt;&lt;em&gt;Built on real devices. Debugged the hard way.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Appendix: Common MultipeerConnectivity + Swift 6 Mistakes
&lt;/h2&gt;

&lt;p&gt;This appendix exists because MultipeerConnectivity (MPC) predates Swift Concurrency. Swift 6 enforces rules that MPC was never designed for. The result is a long list of compiler errors that sound scary but are actually pointing at very specific architectural problems.&lt;/p&gt;

&lt;p&gt;This section translates those errors into plain English, shows why they happen, and explains the correct fix.&lt;/p&gt;

&lt;p&gt;⸻&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;❌ “Sending invitationHandler risks causing data races”&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What you probably wrote&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Task { @MainActor in
    invitationHandler(true, session)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why Swift 6 complains&lt;/p&gt;

&lt;p&gt;invitationHandler is a non-Sendable escaping closure provided by Apple. It must be called synchronously, exactly once, from the delegate callback.&lt;/p&gt;

&lt;p&gt;When you capture it inside a Task, Swift can no longer guarantee:&lt;br&gt;
    • when it runs&lt;br&gt;
    • which thread runs it&lt;br&gt;
    • whether it runs twice&lt;/p&gt;

&lt;p&gt;From Swift’s point of view, this is a data race risk.&lt;/p&gt;

&lt;p&gt;✅ Correct pattern&lt;/p&gt;

&lt;p&gt;Call the handler immediately, then do async work afterwards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;guard let session = manager.session else {
    invitationHandler(false, nil)
    return
}

invitationHandler(true, session)   // synchronous, safe

Task { @MainActor in
    // UI updates here
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rule: MPC invitation handlers are not async-aware. Treat them like C callbacks.&lt;/p&gt;

&lt;p&gt;⸻&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;❌ “Main actor-isolated property cannot be referenced from a nonisolated context”&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Where this appears&lt;/p&gt;

&lt;p&gt;Delegate methods such as:&lt;br&gt;
    • MCSessionDelegate&lt;br&gt;
    • MCNearbyServiceAdvertiserDelegate&lt;br&gt;
    • MCNearbyServiceBrowserDelegate&lt;/p&gt;

&lt;p&gt;Why this happens&lt;/p&gt;

&lt;p&gt;MPC delegate methods are:&lt;br&gt;
    • not actor-isolated&lt;br&gt;
    • called on arbitrary system threads&lt;/p&gt;

&lt;p&gt;Swift 6 correctly forbids this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nonisolated func advertiser(...) {
    manager.session   // ❌ illegal
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because manager.session lives on the @MainActor.&lt;/p&gt;

&lt;p&gt;✅ Correct mental model&lt;/p&gt;

&lt;p&gt;Think of MPC delegates as interrupt handlers.&lt;/p&gt;

&lt;p&gt;They must:&lt;br&gt;
    1.  Capture data synchronously&lt;br&gt;
    2.  Forward events elsewhere&lt;br&gt;
    3.  Do no UI, no actor state access directly&lt;/p&gt;

&lt;p&gt;Correct pattern&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nonisolated func advertiser(...) {
    Task { @MainActor in
        manager.handleInvite(...)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rule: Delegates → hop → actor / main&lt;/p&gt;

&lt;p&gt;⸻&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;❌ Calling async code from sync flow (the “backwards async” bug)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Classic example&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;func didReceiveInvitation(...) {
    let snapshot = await mpc.didDiscoverPeer(peerID) // ❌ impossible
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why this is wrong&lt;/p&gt;

&lt;p&gt;Delegate callbacks are synchronous entry points. You cannot block them waiting for async work.&lt;/p&gt;

&lt;p&gt;Trying to do so:&lt;br&gt;
    • breaks MPC timing guarantees&lt;br&gt;
    • causes invitations to be rejected&lt;br&gt;
    • creates subtle deadlocks&lt;/p&gt;

&lt;p&gt;✅ Correct approach&lt;/p&gt;

&lt;p&gt;Invert control flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nonisolated func advertiser(...) {
    invitationHandler(true, session)

    Task {
        await actor.recordPeer(peerID)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rule: Sync system APIs must emit events, not wait for answers.&lt;/p&gt;

&lt;p&gt;⸻&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;❌ Creating multiple UUIDs for the same peer&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Symptom&lt;br&gt;
    • peerStates[peerSnapshot.id] is always nil&lt;br&gt;
    • UI never updates&lt;br&gt;
    • Multiple UUIDs for the same peer appear in logs&lt;/p&gt;

&lt;p&gt;Root cause&lt;/p&gt;

&lt;p&gt;You generated PeerSnapshot in multiple layers:&lt;br&gt;
    • Browser delegate&lt;br&gt;
    • Advertiser delegate&lt;br&gt;
    • Session delegate&lt;/p&gt;

&lt;p&gt;Each one did:&lt;/p&gt;

&lt;p&gt;let snapshot = PeerSnapshot(id: UUID(), ...)&lt;/p&gt;

&lt;p&gt;So the same peer had different identities everywhere.&lt;/p&gt;

&lt;p&gt;✅ Correct fix&lt;/p&gt;

&lt;p&gt;One source of truth: the actor&lt;/p&gt;

&lt;p&gt;uuidByPeerID: [MCPeerID: UUID]&lt;br&gt;
peerIDByUUID: [UUID: MCPeerID]&lt;/p&gt;

&lt;p&gt;All snapshots come from MPCActor.didDiscoverPeer().&lt;/p&gt;

&lt;p&gt;Rule: IDs are born once, owned once.&lt;/p&gt;

&lt;p&gt;⸻&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;❌ Letting the actor create MCSession / Advertiser / Browser&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why this fails&lt;/p&gt;

&lt;p&gt;These objects:&lt;br&gt;
    • have delegates&lt;br&gt;
    • expect synchronous callbacks&lt;br&gt;
    • are not Sendable&lt;/p&gt;

&lt;p&gt;Putting them in an actor causes:&lt;br&gt;
    • reentrancy issues&lt;br&gt;
    • dropped delegate calls&lt;br&gt;
    • connection failures&lt;/p&gt;

&lt;p&gt;✅ Correct ownership&lt;/p&gt;

&lt;p&gt;Object  Owner&lt;br&gt;
MCSession   PeerSessionManager&lt;br&gt;
Advertiser  PeerSessionManager&lt;br&gt;
Browser PeerSessionManager&lt;br&gt;
State + mapping MPCActor&lt;/p&gt;

&lt;p&gt;Rule: Actors own logic, not system objects.&lt;/p&gt;

&lt;p&gt;⸻&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;❌ Updating UI from actors or delegates&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Wrong&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;actor.emit { tableView.reloadData() }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why&lt;/p&gt;

&lt;p&gt;UI must:&lt;br&gt;
    • run on main thread&lt;br&gt;
    • be isolated from concurrency&lt;/p&gt;

&lt;p&gt;✅ Correct pipeline&lt;/p&gt;

&lt;p&gt;System → Delegate → Actor → Manager → UI&lt;/p&gt;

&lt;p&gt;Only the manager talks to UIKit.&lt;/p&gt;

&lt;p&gt;⸻&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;❌ Misunderstanding nonisolated&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What nonisolated actually means&lt;/p&gt;

&lt;p&gt;“This method can be called without actor protection.”&lt;/p&gt;

&lt;p&gt;It does not mean:&lt;br&gt;
    • thread-safe&lt;br&gt;
    • async-safe&lt;br&gt;
    • free to access actor state&lt;/p&gt;

&lt;p&gt;When to use it&lt;/p&gt;

&lt;p&gt;Only for:&lt;br&gt;
    • delegate entry points&lt;br&gt;
    • forwarding events&lt;/p&gt;

&lt;p&gt;And immediately hop away.&lt;/p&gt;

&lt;p&gt;⸻&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;❌ Trusting MPC error logs too much&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Messages like:&lt;/p&gt;

&lt;p&gt;Not in connected state, so giving up...&lt;/p&gt;

&lt;p&gt;Are symptoms, not causes.&lt;/p&gt;

&lt;p&gt;They usually mean:&lt;br&gt;
    • invitation handler called too late&lt;br&gt;
    • wrong session passed&lt;br&gt;
    • peer identity mismatch&lt;/p&gt;

&lt;p&gt;Fix architecture first — logs will disappear.&lt;/p&gt;

&lt;p&gt;⸻&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MultipeerConnectivity (Apple)
│
├─ MCSession / Advertiser / Browser
│     (Apple-owned, ObjC, delegate-based, non-Sendable)
│
├─ Delegate Bridges (nonisolated)
│     • MCSessionDelegateBridge
│     • MPCAdvertiserDelegateBridge
│     • MPCBrowserDelegateBridge
│
├─ MPCActor (Swift 6 actor)
│     • Owns peer identity
│     • Owns UUID ↔ MCPeerID mapping
│     • Owns peer lifecycle truth
│     • Emits events
│
├─ PeerSessionManager (@MainActor)
│     • Owns MCSession lifetime
│     • Owns advertiser/browser lifetime
│     • Owns UI-facing state (arrays, dictionaries)
│     • Translates actor events → UI callbacks
│
└─ ViewController (UIKit)
      • Displays state
      • Sends intents (invite, send, etc.)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Final Rule of Thumb&lt;/p&gt;

&lt;p&gt;Swift 6 doesn’t make MPC harder — it makes mistakes visible.&lt;/p&gt;

&lt;p&gt;If Swift complains:&lt;br&gt;
    • it’s usually right&lt;br&gt;
    • but the fix is architectural, not syntactic&lt;/p&gt;

&lt;p&gt;⸻&lt;br&gt;
&lt;a href="https://github.com/swainwri/MPCAndSwift6" rel="noopener noreferrer"&gt;https://github.com/swainwri/MPCAndSwift6&lt;/a&gt; for project.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>networking</category>
      <category>swift</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
