UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Devices Not Connecting (MultipeerConnectivity)

Forums > iOS

Background

I am working on a SwiftUI app that has peer-to-peer networking with the Multipeer Connectivity framework. The main data structures at play in the connectivity are as follows:

  • PlayerService: A singleton class that manages the peer-to-peer connectivity
  • TrafficControl: An enumeration that holds variables/constants used throughout the app
  • Several SwiftUI Views: Several of my views access and make changes to the PlayerService

Problem

Devices fail to connect each other, and I receive the following errors:

SocketStream read error [0x600000219fe0]: 1 54
[MCNearbyDiscoveryPeerConnection] Read failed.
[MCNearbyDiscoveryPeerConnection] Stream error occurred: Error Domain=NSPOSIXErrorDomain Code=54 "Connection reset by peer" UserInfo={_kCFStreamErrorCodeKey=54, _kCFStreamErrorDomainKey=1}

Soon after, I receive these warnings:

[GCKSession] Not in connected state, so giving up for participant [4CC37618] on channel [0].
[GCKSession] Not in connected state, so giving up for participant [4CC37618] on channel [1].
[GCKSession] Not in connected state, so giving up for participant [4CC37618] on channel [2].
[GCKSession] Not in connected state, so giving up for participant [4CC37618] on channel [3].
[GCKSession] Not in connected state, so giving up for participant [4CC37618] on channel [4].
[GCKSession] Not in connected state, so giving up for participant [4CC37618] on channel [5].

Old Attempts

I am not sure what exactly is causing the issue, but I did get an error once saying that there was an error on the advertiser side, which I believe means there is an issue with the MCNearbyServiceAdvertiser. I have tried to solve the issue several times with methods including, but not limited to: changing Info.plist information, printing errors received when sending data to try to find problem (in earlier attempts, the peers would disconnect after sending data), shortening display name of MCPeerID after realizing there is a 63-character limit, comparing input/output data being sent, and testing all encryption preferences.

Current Attempt

Right now, I have added a print statement to every function in the PlayerService out of desperation to see if I can figure out where the problem is. Unforunately, I still have not found the problem.

Code

Finally, it's time to show the code. Unfortunately, I will not be able to provide a minimum-reproducible example of what is going on because, as stated above, I have no idea what exactly is causing the issue! However, I will provide the essential information (everything that deals with the MultipeerConnectivity.


First, the property list (Info.plist), since the error could be there:

Info.plist (simplified)

<dict>
    <key>NSBonjourServices</key>
    <array>
        <string>_fk-lobby._tcp</string>
        <string>_fk-lobby._udp</string>
    </array>

    <key>NSLocalNetworkUsageDescription</key>
    <string>We need access to your device's local network to connect to other devices.</string>
</dict>

Next, the Traffic Control enum:

TrafficControl.swift (simplified)

enum TrafficControl {
    static var player = Player()
    static let playerService = PlayerService()

    enum Network {
        static let serviceType = "fk-lobby"
        static let connectionTimeoutTime = 30.0 // seconds
        static let dataSendMaxAttempts = 5
    }
}

Now for the Player class:

Player.swift (simplified)

final class Player: ObservableObject, Identifiable, Equatable {

    // MARK: - Properties

    // NOTE: Whenever these properties change, `sendPlayerInfoUpdate()` is called
    @Published var name: String {
        didSet { sendPlayerInfoUpdate() }
    }

    @Published var avatar: Avatar {
        didSet { sendPlayerInfoUpdate() }
    }

    @Published var money: Double {
        didSet { sendPlayerInfoUpdate() }
    }

    @Published var role: RoleGroup {
        didSet { sendPlayerInfoUpdate() }
    }

    let peerID: MCPeerID

    var discoveryInfo: [String: String] {
        return [
            "name": self.name,
            "lobbyMemberCount": 0, // Constant for simplicity
            "gameState": "idle"    // Constant for simplicity
        ]
    }

    // MARK: - Initializer

    init(...) {
        // ...
        let uuid = UUID().uuidString
        let shortUUID = uuid.suffix(8)

        self.peerID = MCPeerID(displayName: shortUUID)
    }

    // MARK: - Methods

    func becomeHost() {
        // ...
        TrafficControl.playerService.startAdvertiser()
    }

    func sendPlayerInfoUpdate() {
        // If the local player is the host, the player will call the following code on all the other lobby members
        TrafficControl.playerService.sendPlayerInfo(to: /* lobby host */ )
    }
}

Next, the Player Service:

NOTE: The sendPlayerInfo(to:) method above is a convenience method which uses the send(data:to:) method below, so I will not include the sendPlayerInfo(to:) method.

PlayerService.swift (simplified)

final class PlayerService: NSObject, ObservableObject {

    // MARK: - Properties

    private var serviceAdvertiser: MCNearbyServiceAdvertiser?
    private var serviceBrowser: MCNearbyServiceBrowser?

    private lazy var session: MCSession = {
        let mcSession = MCSession(peer: localPlayer.peerID, securityIdentity: nil, encryptionPreference: .required)
        mcSession.delegate = self

        return mcSession
    }()

    @Published var discoveredLobbies = [LobbyInfo]()

    // MARK: - Methods

    func startAdvertiser() {
        // If service browser exists, stop it
        if serviceBrowser != nil {
            stopBrowser()
        }

        // If service advertiser does not exist, create it
        if serviceAdvertiser == nil {
            serviceAdvertiser = MCNearbyServiceAdvertiser(peer: TrafficControl.player.peerID, discoveryInfo: localPlayer.discoveryInfo, serviceType: TrafficControl.Network.serviceType)
            serviceAdvertiser?.delegate = self
        }

        // Start advertiser
        serviceAdvertiser?.startAdvertisingPeer()
    }

    func stopAdvertiser() {
        serviceAdvertiser?.stopAdvertisingPeer()
        serviceAdvertiser?.delegate = nil
        serviceAdvertiser = nil
    }

    func startBrowser() {
        // If service advertiser exists, stop it
        if serviceAdvertiser != nil {
            stopAdvertiser()
        }

        // If service browser does not exist, create it
        if serviceBrowser == nil {
            serviceBrowser = MCNearbyServiceBrowser(peer: TrafficControl.player.peerID, serviceType: TrafficControl.Network.serviceType)
            serviceBrowser?.delegate = self
        }

        // Start browser
        serviceBrowser?.startBrowsingForPeers()
    }

    func stopBrowser() {
        serviceBrowser?.stopBrowsingForPeers()
        serviceBrowser?.delegate = nil
        serviceBrowser = nil
    }

    func send(data: Data, to peer: MCPeerID) {
        // Attempt system that tries to send data 5 times before giving up

        var attempts = 0
        var successfullySent = false

        let attemptSend = { [weak self] in
            guard let self = self else {
                attempts += 1
                return
            }

            guard self.session.connectedPeers.contains(peer) else {
                attempts += 1
                return
            }

            do {
                try self.session.send(data, toPeers: [peer], with: .reliable)
                successfullySent = true
            } catch {
                attempts += 1
            }
        }

        while !successfullySent && attemps < TrafficControl.Network.dataSendMaxAttempts {
            attemptSend()
        }
    }

    func join(peer: MCPeerID) {
        guard peer != TrafficControl.player.peerID else { return } // Ensure peer is not joining itself
        serviceBrowser?.invitePeer(peer, to: session, withContext: nil, timeout: TrafficControl.Network.connectionTimeoutTime)
    }
}

Now I will conform PlayerService to MCSessionDelegate:

PlayerService.swift (simplified conformance)

extension PlayerService: MCSessionDelegate {
    func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
        guard state == .connected else { return }

        // The following send messages are convenience methods that use the `send(data:to:)` method
        if TrafficControl.player.isHost {
            sendLobbyInfo(to: peerID)
        } else {
            sendPlayerInfo(to: peerID)
        }
    }

    // ...
}

Then I will conform PlayerService to MCNearbyServiceAdvertiserDelegate (I will not show conformance to MCNearbyServiceBrowserDelegate because the all I do there is update the discoveredLobbies array):

NOTE: I get the error before invitationHandler(true, session) is called

PlayerService.swift (simplified conformance)

extension PlayerService: MCNearbyServiceAdvertiserDelegate {
    func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID,
                   withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
        guard TrafficControl.player.isHost else {
            invitationHandler(false, nil)
            return
        }

        invitationHandler(true, session)
    }
}

Finally, I will show the User Interface.

Home Screen:

HomeView.swift (simplified)

struct HomeView: View {
    var body: some View {
        Button("Host a Lobby") {
            // Edit profile view so it calls `TrafficControl.player.becomeHost() when done editing
            // ...
            navigator.navigate(to: .profile) // Navigate to profile editor screen
        }

        Button("Join a Lobby") {
            TrafficControl.playerService.startBrowser()
            navigator.navigate(to: .lobbyList) // Navigate to lobby list screen
        }
    }
}

LobbyListView.swift (simplified)

struct LobbyListView: View {
    var body: some View {
        List {
            ForEach(TrafficControl.playerService.discoveredLobbies) { info in
                Button(info.hostName) {`
                    // Edit profile editor so it calls `TrafficControl.playerService.join(peer: info.hostPeerID)` when done editing
                    // ...
                    navigator.navigate(to: .profile) // Navigate to profile editor screen
                }
            }
        }
    }
}

Conclusion

Those are all the files I need help taking a look at. Please let me know if I can improve on anything. I appreciate all the help I can get! If you think the problem might be something I did not mention, make sure to tell me what exactly you think it might be.

2      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.