<?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: Amol Srivastava</title>
    <description>The latest articles on DEV Community by Amol Srivastava (@amol_srivastava_7fb7543ef).</description>
    <link>https://dev.to/amol_srivastava_7fb7543ef</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%2F4090650%2F4518a922-c05c-46ed-89ee-db8f8443bd8b.jpg</url>
      <title>DEV Community: Amol Srivastava</title>
      <link>https://dev.to/amol_srivastava_7fb7543ef</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/amol_srivastava_7fb7543ef"/>
    <language>en</language>
    <item>
      <title>Building Local Multiplayer iOS Experiences Without a Server</title>
      <dc:creator>Amol Srivastava</dc:creator>
      <pubDate>Sun, 23 Aug 2026 10:10:22 +0000</pubDate>
      <link>https://dev.to/amol_srivastava_7fb7543ef/building-local-multiplayer-ios-experiences-without-a-server-1678</link>
      <guid>https://dev.to/amol_srivastava_7fb7543ef/building-local-multiplayer-ios-experiences-without-a-server-1678</guid>
      <description>&lt;p&gt;Every "local multiplayer" tutorial you'll find assumes two devices on the same Wi-Fi network, then walks you through Bonjour service discovery. That's the easy 80%. The part nobody covers is what happens when there's no Wi-Fi at all — two phones, a restaurant with no guest network, both parties expecting the app to just work. That gap is where &lt;code&gt;MultipeerConnectivity&lt;/code&gt; actually earns its keep, and it's the backbone of every phone-to-phone experience I've shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why MultipeerConnectivity over a backend
&lt;/h2&gt;

&lt;p&gt;For a two-person local game or icebreaker app, a server buys you nothing. It adds latency, a cost line item, an account system nobody wants to sign up for, and a single point of failure for a feature that's fundamentally "two people in the same room." &lt;code&gt;MultipeerConnectivity&lt;/code&gt; uses Bluetooth and peer-to-peer Wi-Fi automatically, falling back between them without you writing that logic yourself. No network required, no accounts, no server bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three objects you actually need
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;MultipeerConnectivity&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;LocalSession&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;NSObject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;ObservableObject&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;myPeerID&lt;/span&gt; &lt;span class="o"&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;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIDevice&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&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;serviceType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"my-app-sync"&lt;/span&gt; &lt;span class="c1"&gt;// lowercase, 1-15 chars, no special chars beyond hyphen&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;lazy&lt;/span&gt; &lt;span class="k"&gt;var&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="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;MCSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;myPeerID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;securityIdentity&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="nv"&gt;encryptionPreference&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;required&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delegate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;
    &lt;span class="p"&gt;}()&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;lazy&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;advertiser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;MCNearbyServiceAdvertiser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nv"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;myPeerID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;discoveryInfo&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="nv"&gt;serviceType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;serviceType&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;lazy&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;MCNearbyServiceBrowser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;myPeerID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;serviceType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;serviceType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="kd"&gt;@Published&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;connectedPeers&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="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;MCSession&lt;/code&gt; is the actual pipe data flows through. &lt;code&gt;MCNearbyServiceAdvertiser&lt;/code&gt; broadcasts "I'm here, invite me." &lt;code&gt;MCNearbyServiceBrowser&lt;/code&gt; looks for those broadcasts. In practice, both devices run both roles simultaneously — you don't design a client and a host, you design two peers that happen to connect to each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  The delegate methods that matter
&lt;/h2&gt;

&lt;p&gt;Most of &lt;code&gt;MCSessionDelegate&lt;/code&gt; is boilerplate you'll paste once and forget. Two callbacks are worth understanding properly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;extension&lt;/span&gt; &lt;span class="kt"&gt;LocalSession&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;MCSessionDelegate&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;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="kt"&gt;DispatchQueue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;connected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connectedPeers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&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;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;notConnected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connectedPeers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;removeAll&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt; &lt;span class="o"&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;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;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;didReceive&lt;/span&gt; &lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fromPeer&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="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Decode and publish into your app's state on the main thread.&lt;/span&gt;
        &lt;span class="c1"&gt;// This fires on a background queue — never touch @Published state&lt;/span&gt;
        &lt;span class="c1"&gt;// without hopping to main first, or SwiftUI will silently drop updates.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;didChange&lt;/code&gt; callback is your entire connection-state model. Don't build a separate "is connected" flag elsewhere — derive everything from &lt;code&gt;connectedPeers&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually breaks in production
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Advertise and browse at the same time, always.&lt;/strong&gt; Early versions of these apps had a "Host" and "Join" button. Users pick the wrong one, or one person's app is slow to open the browser and misses the advertisement window entirely. Have both devices advertise and browse simultaneously from the moment the feature screen opens — whichever one the OS connects first wins, and neither user has to make a choice that can be wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Handle &lt;code&gt;.connecting&lt;/code&gt; as a real UI state.&lt;/strong&gt; There's a window — sometimes a couple of seconds — where a peer is found but not yet connected. If your UI jumps straight from "searching" to "connected," a slow handshake reads as a frozen app. Show a distinct "connecting to [name]..." state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data messages are unordered relative to each other unless you make them not.&lt;/strong&gt; If you send two pieces of state in quick succession, don't assume they arrive in the order you sent them. Either send a single serialized snapshot of state per update instead of granular deltas, or include a sequence number and reconcile on the receiving end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test with Bluetooth off and on, and with both.&lt;/strong&gt; Peer-to-peer Wi-Fi and Bluetooth have different range and reliability characteristics. An app that only gets tested on a desk with both radios full-strength will surprise you in a noisy real-world environment. Specifically test the case where discovery starts working, then drops mid-session — your reconnect logic is the part everyone forgets to build.&lt;/p&gt;

&lt;p&gt;Once this pattern is in place, "local multiplayer" stops being a feature you dread building and becomes a five-minute addition to any two-person app idea — no backend ticket required.&lt;/p&gt;

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