WWDC22 SALE: Save 50% on all my Swift books and bundles! >>

SOLVED: Multipeer Connectivity - Need help sending and receiving data correctly

Forums > Swift

I am trying to make an app where users can create and join "Lobbies", or sessions, using the MultipeerConnectivity framework. Whenever a user tries to join a lobby, I want them to receive information about the members already in the lobby in order to display the correct UI on their screen. The way I am trying to achieve this is, when a user joins a lobby, they send a request for basic information of the lobby members, such as their display name and avatar, to the host. When the host receives the request, they should send the information back. However, for some reason, when sending a request, the requester receives the request they had sent and since they are not the host of the lobby, an error occurs because they are unable to provide the information. I have created a PlayerInfo struct to hold basic information that the user joining the lobby needs as well as a Player class, which handles the work for sending and receiving data. I will show the code doing all the work with code comments to hopefully make it easier to understand. Here is the code I have of the PlayerInfo struct:

PlayerInfo

import MultipeerConnectivity

/* This is a struct used to capture just the more importance information of the 'Player' class so I don't capture lots of data that I won't
be using in arrays or wherever else I will use it */

struct PlayerInfo: Codable {
    // Properties to hold the display and device names of the Player
    var displayName: String
    var deviceName: String {
        return UIDevice.current.name
    }

    /* Properties to hold the avatar image and role of the player. *Note: I used a property wrapper for 'UIImage' called 'Image' to make this
  struct conform to Codable */
    var avatar: Image
    var role: Player.Role

    // Initializer to assign values to all properties of the 'PlayerInfo' struct
    init(peerID: MCPeerID, avatar: UIImage, role: Player.Role) {
        self.displayName = peerID.displayName
        self.avatar = Image(image: avatar)
        self.role = role
    }

    /* Another initializer that makes it easier to initialize a 'PlayerInfo' struct just by passing in a 'Player' class. I will just take the info I need
  from the 'Player' class and pass it in to the other initializer I made */
    init(fromPlayer player: Player) {
        self.init(peerID: player.peerID, avatar: player.avatar, role: player.role)
    }
}

And here is the code for my Player class:

Player

import MultipeerConnectivity
import UIKit

/* A struct that makes it easy to send data from peer to peer because I can provide a description of what the data is as well as any data
content */
struct SentData: Codable {
    var description: SentDataDescription
    var content: Data

    enum SentDataDescription: Codable {
        case lobbyInfo
        case lobbyInfoRequest
    }
}

// The 'Player' class
class Player: NSObject {

    // Implicityly-unwrapped properties to hold an MCPeerID and an MCSession
    var peerID: MCPeerID!
    var session: MCSession!

    /* An optional array of a custom struct called 'PlayerInfo' to just hold basic information needed for each lobby member rather than
  many 'Player' objects */
    var lobbyMembers: [PlayerInfo]?

    /* A weak property to hold the lobby that the host is in. It is declared weak to avoid a strong-reference cyle. The LobbyViewController
  holds a strong reference to the player already */
    weak var lobby: LobbyViewController?

    // Properties to hold an avatar image and a role of the player
    var avatar: UIImage
    var role: Role

    // Enumeration to hold all currently possible roles a player may have
    enum Role: Codable {
        case player
        case banker
        case host
        case none

        /* In another file, I want to show the role of players with special roles in the UI, so I have a static function to easily get text for each
    role */
        static func roleTitle(from role: Role) -> String {
            switch role {
            case .banker:
                return "Banker"

            case .host:
                return "Host"

            default:
                return ""
            }
        }
    }

    /* Initializer of the 'Player' class where I assign values to all properties and assign the delegate of the MCSession to the current
  instance of the 'Player' class */
    init(displayName: String = UIDevice.current.name, avatar: UIImage, role: Role = .none) {
        self.avatar = avatar
        self.role = role
        self.peerID = MCPeerID(displayName: displayName)
        self.session = MCSession(peer: peerID, securityIdentity: nil, encryptionPreference: .required)

        super.init()
        self.session.delegate = self
    }

    /* A method where I request lobby information from another player. This is done because, when a player joins a lobby, they will need
  to know how many people are already in the lobby to be able to show them all in the UI. I'm not basing this off the MCPeerIDs
  in the current lobby session because I also want to know the role of each player */
    func requestLobbyInfo(from peer: MCPeerID) {

        print("From \(peerID.displayName): Attempting to request lobby info from \(peer.displayName)")

        /* Create a 'SentData' object with a description of lobby info request and an empty 'Data' object because, for this request, no
    important content is being given other than the description */
        let sentData = SentData(description: .lobbyInfoRequest, content: Data())

        do {
            // Try encoding the 'SentData' object with JSON and send it to the peer given in the parameter
            let jsonEncoder = JSONEncoder()
            let encodedRequest = try jsonEncoder.encode(sentData)

            try session.send(encodedRequest, toPeers: [peer], with: .reliable)
        } catch {
            print("Failed to request lobby info from \(peer).\n\(error)")
        }
    }

    // A method to send lobby info to the requesting peer
    func sendLobbyInfo(to peer: MCPeerID) {
        print("From \(peerID.displayName): Attempting to send lobby info to \(peer.displayName)")

        // Safely unwrap the lobby property before sending the lobby info
        guard let lobby = self.lobby else {
            /* THIS IS WHERE ERROR OCCURS (Read all the code before examining this, as I am sure this function is called on the wrong
      player): Lobby property is not supposed to be nil because the lobby host should already be in a lobby */
            fatalError("Lobby property is nil")
        }

        do {
            /* When sending the lobby members info, it should include all the lobby members including the host. The host should be the current
      instance if this function is being called. *NOTE: The 'lobbyMembers' array in the lobby is an array of 'PlayerInfo' structs. That's why
      I am converting the current instance to a 'PlayerInfo' struct */
            let lobbyMembersInfo = lobby.lobbyMembers + [PlayerInfo(fromPlayer: self)]

            let jsonEncoder = JSONEncoder()

            /* First, encode the lobby members info because the 'SentData' content must be a 'Data' object in order to conform to the
      'Codable' protocol */
            let encodedLobbyMembersInfo = try jsonEncoder.encode(lobbyMembersInfo)

            // Next, initialize and encode a new 'SentData' object with the correct description and content
            let sentData = SentData(description: .lobbyInfo, content: encodedLobbyMembersInfo)
            let encodedSentData = try jsonEncoder.encode(sentData)

            // Attempt to send the 'SentData' object to the peer who requested info
            try session.send(encodedSentData, toPeers: [peer], with: .reliable)
            print("From \(peerID.displayName): Successfully sent data to \(peer.displayName)")
        } catch {
            print("Failed to send lobby info to \(peer).\n\(error)")
        }
    }

    /* This method is unfinished. Right now, it just changes the role of the current instance of the 'Player' class to host. The actual
  advertising of the lobby happens in the 'LobbyViewController' as soon as the lobby is created */
    func hostLobby() {
        role = .host
    }

    /* This method is also unfinished. Right now, it just changes the role of the current instance of the 'Player' class to none since it won't
  be in a lobby anymore. It also changes the lobby to nil in case it had a value already */
    func leaveLobby() {
        role = .none
        lobby = nil
    }
}

// Conform the Player to 'MCSessionDelegate' protocol
extension Player: MCSessionDelegate {

    /* From my understanding, this method is called when the connection state to another session is changed. For example, when device
  A is connected to device B, device A is notified that it is connected to device B and this method is called. That's why device A is
  requesting info from device B, since device A would be the player joining and device B would be the host */
    func session(_ session: MCSession, peer: MCPeerID, didChange state: MCSessionState) {
        switch state {
        case .connected:

            // When the state is connected, request lobby info from the peer that was just connected
            print("From \(peerID.displayName): Connected to \(peer.displayName)")
            requestLobbyInfo(from: peer)

        case .connecting:
            print("From \(peerID.displayName): Connecting to \(peer.displayName)")

        case .notConnected:
            print("From \(peerID.displayName): \(peer.displayName) is not connected")

        default:
            print("From \(peerID.displayName): Unknown State - \(peer.displayName)")
        }
    }

    /* From my understanding, this method is called whenever a device receives data from another device. For example, if device A
  receives some data from device B, device A is notified and this method is called */
    func session(_ session: MCSession, didReceive data: Data, fromPeer peer: MCPeerID) {
        print("From \(peerID.displayName): Received data from \(peer.displayName)")
        let jsonDecoder = JSONDecoder()

        // Try to decode a 'SentData' object from received data.
        if let sentData = try? jsonDecoder.decode(SentData.self, from: data) {

            // Determine what kind of sent data was received
            switch sentData.description {
            case .lobbyInfoRequest:
                /* If the sent data is a request for lobby info, send the lobby info to that peer. THIS MIGHT BE WHERE ERROR OCCURS, but I am
        not sure why */
                sendLobbyInfo(to: peer)

            case .lobbyInfo:
                // If the sent data is actually the lobby info and not the request for it, try to decode the contents as an array of 'PlayerInfo' structs
                do {
                    let lobbyMembersInfo = try jsonDecoder.decode([PlayerInfo].self, from: sentData.content)

                    // If successfully decoded, assign the lobbyMembersInfo to the lobbyMembers property of the current instance of the 'Player' class
                    self.lobbyMembers = lobbyMembersInfo
                    print("Received lobby info")
                } catch {
                    print("Failed to retrieve lobby hosting info.\n\(error)")
                }
            }
        } else {
            // If unable to decode data as 'SentData', print the result
            print("Did not receive 'SentData' object")
        }
    }

    // These are protocol stubs required for conformance to MCSessionDelegate
    func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) { }

    func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) { }

    func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) { }
}

   

Solved the issue! The bug was below the Player class declaration, in the extension where I conform Player to MCSessionDelegate, in the session(_:peer:didChange:) method. I completely forgot that both devices would be notified when the connection state is changed 😅. That's why one of the players was sending the information when they weren't supposed to. To fix this, I added an if-statement to make sure that the player is NOT a host when they are requesting lobby information. Here is the updated code for session(_:peer:didChange:):

extension Player: MCSessionDelegate {

    // From my understanding, this method is called when the connection state to another session is changed. For example, if device A connects to device B, both devices will be notified and this method will be run for both
    func session(_ session: MCSession, peer: MCPeerID, didChange state: MCSessionState) {
        switch state {
        case .connected:

            print("From \(peerID.displayName): Connected to \(peer.displayName)")

            // When the state is connected, make sure the role of the current instance of 'Player' is not host and request lobby info from the host
            if role != .host {
                requestLobbyInfo(from: peer)
            }

        case .connecting:
            print("From \(peerID.displayName): Connecting to \(peer.displayName)")

        case .notConnected:
            print("From \(peerID.displayName): \(peer.displayName) is not connected")

        default:
            print("From \(peerID.displayName): Unknown State - \(peer.displayName)")
        }
    }

    ...

   

I can actually make the above code even simpler by omitting the requestLobbyInfo(from:) method in the Player class and, instead of checking if the current player is not the host and requesting info, I will check if the current player IS the host and sending info to the player joining. Then, I can also remove more code from the session(_:didReceive:fromPeer:) method.

   

Hacking with Swift is sponsored by Emerge

SPONSORED Optimize your app’s startup time, binary size, and overall performance using Emerge’s advanced app optimization and monitoring tools. Reliably measure app size, speed up your app's startup time with Emerge's Launch Booster, and much more. Emerge is actively used by many of the top mobile development teams in the world.

Find out more

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.