Not being able to keep connection stable and send data across devices using MultipeerConnectivity
Asked Answered
D

1

2

I'm learning the basics of MultipeerConnectivity framework. I was trying to just send some data between two simulators, so I set up a small project and I've implemented the MCBrowserViewControllerDelegate, MCNearbyServiceAdvertiserDelegate and MCSessionDelegate protocols. I added three buttons, with a corresponding action each - names are pretty self-explanatory I guess:

  • advertiseButton, with a advertise() associated method;
  • joinButton, with a join() associated method;
  • sendDataButton, with a sendData() associated method.

With my code, I was actually able to connect two simulator devices, since I get a "Connected" output printed in the console, however after a few seconds from the connection I get a bunch of errors logged in the console:

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

I might be wrong, but I guess it means that the connection was lost.

This happens every time, so it might be some implementation in my code done wrong. My first question is: why does it happen and how can I fix it?

Moreover, if I tap on the sendData button on one simulator no data is received on the other simulator device, the didReceive data callback of MCSessionDelegate is never called. I'm almost sure it is due to the above log, however this occurs also if I press the button after the connection and slightly prior of the warning.

So, basically, what am I doing wrong in my code?

I'm using XCode 14.3, testing on simulators iPhone 14 and iPhone 14 Pro.

EDIT: just tested on real devices, same issue.

Here is the full sample project, just copy-paste it, add privacy settings in Info.plist if needed, then build&run:

import UIKit
import MultipeerConnectivity

final class ViewController: UIViewController {
    
    // MARK: - Connectivity
    private var peerID: MCPeerID!
    private var session: MCSession!
    private var nearbyServiceAdvertiser: MCNearbyServiceAdvertiser!

    // MARK: - Buttons
    private lazy var advertiseButton: UIButton = {
        let button = UIButton()
        button.addTarget(self, action: #selector(advertise), for: .touchUpInside)
        button.setTitle("Advertise", for: .normal)
        button.setTitleColor(.blue, for: .normal)
        button.titleLabel?.font = UIFont(name: "Arial", size: 24)
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()
    
    private lazy var joinButton: UIButton = {
        let button = UIButton()
        button.addTarget(self, action: #selector(join), for: .touchUpInside)
        button.setTitle("Join", for: .normal)
        button.setTitleColor(.blue, for: .normal)
        button.titleLabel?.font = UIFont(name: "Arial", size: 24)
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()
    
    private lazy var sendDataButton: UIButton = {
        let button = UIButton()
        button.addTarget(self, action: #selector(sendData), for: .touchUpInside)
        button.setTitle("Send Data", for: .normal)
        button.setTitleColor(.blue, for: .normal)
        button.titleLabel?.font = UIFont(name: "Arial", size: 24)
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()

    // MARK: - Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        view.addSubview(advertiseButton)
        view.addSubview(joinButton)
        view.addSubview(sendDataButton)
        peerID = MCPeerID(displayName: UIDevice.current.name)
        session = MCSession(peer: peerID, securityIdentity: nil, encryptionPreference: .required)
        session.delegate = self
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        advertiseButton.frame = CGRect(x: 0, y: 500, width: view.frame.size.width, height: 50)
        joinButton.frame = CGRect(x: 0, y: 600, width: view.frame.size.width, height: 50)
        sendDataButton.frame = CGRect(x: 0, y: 700, width: view.frame.size.width, height: 50)
    }
    
    // MARK: - Selectors
    @objc private func advertise() {
        nearbyServiceAdvertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: nil, serviceType: "test")
        nearbyServiceAdvertiser.delegate = self
        nearbyServiceAdvertiser.startAdvertisingPeer()
    }
    
    @objc private func join() {
        let browser = MCBrowserViewController(serviceType: "test", session: session)
        browser.delegate = self
        present(browser, animated: true)
    }
    
    @objc private func sendData() {
        let stringToSend: String = "randomstring"
        if let data = stringToSend.data(using: .utf8) {
            try? session.send(data, toPeers: session.connectedPeers, with: .reliable)
        } else {
            print("error converting data from string")
        }
    }
}

// MARK: - MCSessionDelegate
extension ViewController: MCSessionDelegate {
    func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
        switch state {
        case .notConnected:
            print("Not connected: \(peerID.displayName).")
        case .connecting:
            print("Connecting: \(peerID.displayName).")
        case .connected:
            print("Connected: \(peerID.displayName).")
        @unknown default:
            fatalError()
        }
    }
    
    func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
        if let receivedString = String(data: data, encoding: .utf8) {
            print(receivedString)
        } else {
            print("error converting string from data")
        }
    }
    
    func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {
        // no operations
    }
    
    func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {
        // no operations
    }
    
    func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) {
        // no operations
    }
}

// MARK: - MCBrowserViewControllerDelegate
extension ViewController: MCBrowserViewControllerDelegate {
    func browserViewControllerDidFinish(_ browserViewController: MCBrowserViewController) {
        browserViewController.dismiss(animated: true)
    }
    
    func browserViewControllerWasCancelled(_ browserViewController: MCBrowserViewController) {
        browserViewController.dismiss(animated: true)
    }
}

// MARK: - MCNearbyServiceAdvertiserDelegate
extension ViewController: MCNearbyServiceAdvertiserDelegate {
    func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
        invitationHandler(true, session)
    }
}
Dairying answered 10/7, 2023 at 19:25 Comment(0)
D
2

As suspected, it was indeed an issue related to my code. Apparently I have to stop browsing for peers once a connection is enstablished, otherwise the devices might disconnect if they keep doing so.

So, first of all I need to declare a new variable, and change nearbyServiceAdvertiser to optional also:

private var browserViewController: MCBrowserViewController?
private var nearbyServiceAdvertiser: MCNearbyServiceAdvertiser?

And of course join() becomes:

@objc private func join() {
    browserViewController = MCBrowserViewController(serviceType: "test", session: session)
    browserViewController?.delegate = self
    present(browserViewController!, animated: true)
}

Then in didChange state of MCSessionDelegate, in the .connected case:

nearbyServiceAdvertiser?.stopAdvertisingPeer()
browserViewController?.browser?.stopBrowsingForPeers()

These steps fix the issue.

Optional: just for the sake of it, in order to better understand possible errors, let's refactor furthermore the code:

do {
    try session.send(data, toPeers: session.connectedPeers, with: .reliable)
} catch let error {
    print(error.localizedDescription)
}

However, the console logs I got are not related to the fact that I could not send data, in fact they are still present. Why and what do they mean is still unclear to me, as the devices are connected and I'm able to send data even after the logs. If someone is able to shine a light on this I'm thankful.

Dairying answered 10/7, 2023 at 22:0 Comment(5)
Does this mean that it's impossible to browse/'wait' for multiple peers to advertise themselves and connect? – Asthenosphere
@Asthenosphere - am just digging into this myself, but apparently you're limited to 8 connected peers... πŸ˜… – Beedon
@JamesWilliam I'm aware the maximum is 8. But if stopAdvertisingPeer() and stopBrowsingForPeers() is called as soon as the first is connnected, as is suggested here, then only one can connect. I tried this actually yesterday and I still get console errors followed by a disconnected message. Did you manage to keep a connection up? – Asthenosphere
@Asthenosphere just saw this, so you weren't able to keep the connection up even after stopping browsing? It's a old project I was playing with, so I don't remember much, and I had not digged into multiple connections since it wasn't required for me. What I can tell you for sure is that last July I was able to fix it that way. Let me know anyway if you find a better solution please. – Dairying
@Dairying I plan to spend some more time over the weekend and if I don't figure it out will ask Apple. Will let you know! – Asthenosphere

© 2022 - 2024 β€” McMap. All rights reserved.