The horrors of Multipeer Connectivity and SwiftUI 4
Published: 2022-07-31 19:15:38

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 `StateObject`s and `EnvironmentVariable`s 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()
```
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")
```
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") {
        ...
    }
}
...
```
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
    }
}
```
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()
        }
    }
}
```

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](https://github.com/TheNightmanCodeth/RPS). I'll be publishing a full tutorial on YouTube soon, so keep an eye on my page here for updates if you're interested!