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.