DEV Community

Joe Diragi
Joe Diragi

Posted on

The horrors of Multipeer Connectivity and SwiftUI 4

Multipeer Connectivity is a technology released by Apple at their 2014 (I think) WWDC. Multipeer Connectivity (MPC for short) is a part of Apple's Nearby Interaction framework that aims to make direct communication between devices as seamless sand secure as possible. When I set out to create a rock, paper, scissors game that utilized this framework I thought I'd be in for a mostly painless learning experience. To be fair, if this framework had been updated to support SwiftUI I really would not have had a single problem. From what I read on StackOverflow and the Apple developer forums, this framework is really pretty simple to use. That being said, it is clear the framework was designed to work with UIKit and hasn't been updated in a while.
You might be thinking to yourself, "What does UIKit/SwiftUI have to do with a peer-to-peer communication framework?", and you'd be correct in doing so. The thing is, SwiftUI is also a much different programming paradigm than the old way of doing things. SwiftUI's addition of things like StateObjects and EnvironmentVariables really muddy the water when compared to the way apps written with UIKit were built.
I started building the app with the intention of creating a YouTube tutorial, since I read doing so can be a great way to hone ones skills and potentially build a following and further ones career. I dove right into the documentation and things were going smooth. It turned out there are 2 main ways to go about creating an app that utilizes MPC: use an MCAdvertiserAssistant to handle all of the dirty work with pairing, or use the MCNearbyServiceAdvertiser to implement the logic yourself.
When I read through the docs I was confident my use case only required the use of the simplified MCAdvertiserAssistant, which can be used as simply as:

let peerID = MCPeerID(displayName: "Username")
let session = MCSession(peer: peerID, securityIdentity: nil, encryptionPreference: .none)
let assistant = MCAdvertiserAssistant(serviceType: "rps-service", discoveryInfo: nil, session: session)
assistant.start()
Enter fullscreen mode Exit fullscreen mode

Then, the MCAdvertiserAssistant would take care of the hard work for us. I implemented a nearby device browser like so:

let serviceBrowser = MCNearbyServiceBrowser(peer: peerID, serviceType: "rps-service")
Enter fullscreen mode Exit fullscreen mode

Once that was setup I assigned a delegate to the service browser to find available peers and add them to a @Published property that my view could use to allow the player to invite a peer to a game. Once the other player received the invitation, the MCAdvertiserAssistant would take care of the rest.
I deployed the app on my iPhone and my MacBook, created usernames for both and immediately I could see the other player on my device. It was working great! When I selected the MacBook player from my iPhone, a dialog was displayed almost instantly asking if I wanted to pair with the iPhone and start a game. When I selected yes I was brought into the game and everything was working perfectly.
That was all finished and working in a matter of hours, pairing, peer-to-peer messaging and gameplay done!
Or so I thought. I went back to test pairing in the opposite direction, selected the iPhone from my MacBook and....
Nothing!
Xcode spat out some crazy error regarding the alert that the MCAdvertiserAssistant was trying to display. I spent hours searching through any StackOverflow thread that mentioned MultiPeer Connectivity and SwiftUI. All of the accepted answers were fixes that only worked with UIKit applications, and half of the reason I was creating the tutorial in the first place was to show the basics of SwiftUI development.
Finally it occurred to me that I would have to ditch the MCAdvertiserAssistant and implement my own pairing logic using MCNearbyServiceAdvertiser (🤢).
The MCNearbyServiceAdvertiserDelegate has two functions, one gets called when the advertiser cannot start advertising and the other is called when it receives an invitation from a peer. The problem now was figuring out how to show an alert inside of my view from within my RPSMultipeerSession class.
In SwiftUI, alerts are essentially a property of a view. They are shown like so:

@State var showAlert: Bool = false
...
HStack {
    ...
}
.alert("Title", isPresented: $showAlert) {
    Button("Action 1") {
        ...
    }
}
...
Enter fullscreen mode Exit fullscreen mode

When showAlert in the above example is set to true, the alert is shown. I figured I could use a published variable inside of RPSMultipeerSession to show the alert when an invitation is received inside of the MCNearbyServiceAdvertiserDelegate. That worked just fine, however, in order to accept the invitation, one must call the invitationHandler passed into the function inside of the delegate. In other words, there was no obvious way to accept the invitation from inside of the view.

This is the ugly part. I essentially had to hack together a solution here by creating another published variable in the RPSMultipeerSession class that holds the invitationHandler from the MCNearbyServiceDelegate's didReceiveInvitationFromPeer method.

Here's some code ❤️

The fun part is of course in the MCNearbyServiceAdvertiserDelegate. Specifically this method:

func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void {
    DispatchQueue.main.async {
        self.recvdInvite = true
        self.invitationHandler = invitationHandler
    }
}
Enter fullscreen mode Exit fullscreen mode

Here we update the published variable to alert the UI that we received an invite and it should show an alert. This is what the UI looks like (give or take):

struct PairView: View {
    @StateObject var rpsSession: RPSMultipeerSession

    var body: some View {
        if (!rpsSession.paired) {
            HStack {
                List(rpsSession.availablePeers, id: \.self) { peer in
                    Button(peer.displayName) {
                        rpsSession.serviceBrowser.invitePeer(peer, to: rpsSession.session, withContext: nil, timeout: 20)
                    }
                }
            }
            .alert("Received an invite", isPresented: $rpsSession.recvdInvite) {
                Button("Accept") {
                    if (rpsSession.invitationHandler != nil) {
                        rpsSession.invitationHandler!(true, rpsSession.session)
                    }
                }
            }
        } else {
            GameView()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

With all of that in place, pairing finally works correctly both ways between iOS and macOS. If you'd like to take a look at the complete source code, it's on my GitHub. I'll be publishing a full tutorial on YouTube soon, so keep an eye on my page here for updates if you're interested!

Top comments (1)

Collapse
 
halexutrex234 profile image
halexutrex

Working with Multipeer Connectivity (MPC) and SwiftUI 4 can quickly turn into a nightmare due to Apple's lack of updates aligning MPC with SwiftUI’s modern paradigms. While the initial setup with MCAdvertiserAssistant and MCNearbyServiceBrowser appeared straightforward and worked flawlessly in one direction, it failed miserably when trying to initiate pairing in reverse. The challenge lay in triggering SwiftUI alerts from within a delegate method not tied to the view hierarchy—ultimately forcing a workaround using @Published properties to store the invitationHandler, then triggering UI responses through bindings. This messy but functional hack is an unfortunate necessity for SwiftUI developers working with older frameworks. It’s similar to using third-party tools like the Deadlox Android Game Mod, where you inject functionalities such as aimbot or ESP into games like Garena Free Fire—the mods work, but they bypass intended architecture, often requiring clever workarounds that aren't officially supported or cleanly integrated.