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?) { }
}