<?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.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>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 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 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 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 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 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 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 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 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>
