This is the first part in a series about the iOS Multipeer Connectivity Framework. This series addresses the following question: Is it possible to set up a session between peers and exchange data without any user interaction? This way you can start ad hoc peer connections: All nearby peers that run the same app on the foreground are instantaneously connected, and can start exchanging data immediately.
I will explore this ad hoc peer connection scenario, and describe the issues that I encountered during implementation and testing. The series goes accompanied by a Github repository, so you can reproduce, verify or improve my experiments and conclusions as you like. You can download the sources associated with this post here.
This is not a beginners tutorial. If you are new to the iOS Multipeer Connectivity Framework I suggest that you read some tutorials that explain the basic usage scenario’s for multipeer connectivity before you continue reading.
Many of these tutorials describe the interaction between a peer that is advertising and another peer that is browsing. The browsing peer discovers the advertisers that are nearby. The browser presents the nearby advertisers to the user, and the user selects one of them, indicating that he wants to connect. The selected advertiser receives an invitation to connect and asks its user to confirm the connection. After the user has accepted, a session is established and the peers are ready to exchange data. The ad hoc peer connection scenario does the same, but without any user involvement.
In this part of the Multipeer Connectivity series I will introduce the test app that I am using. In the next parts I will zoom in on the issues that I encountered while implementing the ad hoc peer connection scenario and how they can be solved.
Multipeer Connectivity Test Application
The test application consists of 3 main concepts:
- The PFLoggingConsole: Logs NSLog statements to the screen of a device. This way we can read NSLogs not only on the simulator, but on any device. Very convenient if you are testing with multiple devices.
- The PFPeerNameViewController: Presents a screen that allows the user to provide a peer name for the device. By default the current name of the device is chosen. It is shown at first-time startup of the app.
- The PFPeerConnector: This is where the multipeer connectivity logic is implemented.
Let’s see it in action!
At first-time app startup the PFPeerNameController is presented, and you’ll see the following screen:
You can give the device the name you like or accept the default name. After pressing Done the name is saved and the PFLoggingConsole screen is presented:
The text on the console is produced by ordinary NSLog statements in the code.
I have written a separate post about the PFLoggingConsole. Take a look if you are interested, otherwise feel free to skip it, you don’t need it to understand the remainder of this post.
Multipeer Connectivity classes
Now that we’ve dealt with the configuration of peer names and presentation of logs to the user, let’s start talking about the real topic of this series of posts, multipeer connectivity.
The class PFPeerConnector contains all logic to connect peers. Let’s take a look at the header PFPeerConnector.h first:
1 2 3 4 5 6 7 8 9 10 11 |
@interface PFPeerConnector : NSObject @property (strong, nonatomic) NSString *displayName; - (instancetype)initWithDisplayName:(NSString*)name; - (void)connect; - (void)disconnect; @end |
The method connect is called by the PFAppDelegate when the app becomes active and disconnect is called when the app resigns active.
The implementation code in PFPeerConnector.m is shown below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 |
#import <MultipeerConnectivity/MultipeerConnectivity.h> #import "PFPeerConnector.h" #import "PFLoggingConsole.h" NSString *const kServiceType = @"pf-connector"; @interface PFPeerConnector () <MCSessionDelegate, MCNearbyServiceAdvertiserDelegate, MCNearbyServiceBrowserDelegate> @property (strong, nonatomic) MCNearbyServiceAdvertiser *advertiser; @property (strong, nonatomic) MCNearbyServiceBrowser *browser; @property (strong, nonatomic) MCSession *session; @property (strong, nonatomic) MCPeerID *peerId; @end @implementation PFPeerConnector #pragma mark - Lifecycle - (instancetype)initWithDisplayName:(NSString*)name { self = [super init]; if (self) { self.displayName = name; self.peerId = [[MCPeerID alloc] initWithDisplayName:self.displayName]; } return self; } #pragma mark - Public - (void) connect { NSAssert(self.displayName != nil, @"You must set a displayname"); NSAssert(self.peerId != nil, @"self.peerId must not be nil"); NSLog(@"Peer %@ is connecting", self.displayName); self.session = [[MCSession alloc] initWithPeer:self.peerId securityIdentity:nil encryptionPreference:MCEncryptionNone]; self.session.delegate = self; self.advertiser = [[MCNearbyServiceAdvertiser alloc] initWithPeer:self.peerId discoveryInfo:nil serviceType:kServiceType]; self.advertiser.delegate = self; [self.advertiser startAdvertisingPeer]; self.browser = [[MCNearbyServiceBrowser alloc] initWithPeer:self.peerId serviceType:kServiceType]; self.browser.delegate = self; [self.browser startBrowsingForPeers]; } - (void) disconnect { NSLog(@"Peer %@ is disonnecting", self.displayName); [self.advertiser stopAdvertisingPeer]; self.advertiser.delegate = nil; self.advertiser = nil; [self.browser stopBrowsingForPeers]; self.browser.delegate = nil; self.browser = nil; [self.session disconnect]; self.session.delegate = nil; self.session = nil; } #pragma mark - Private - (void) logPeers { NSArray *peers = self.session.connectedPeers; NSMutableArray *displayNames = [[NSMutableArray alloc]init]; for (MCPeerID *peer in peers) { [displayNames addObject:peer.displayName]; } NSLog(@"%@ peers: %@", self.peerId.displayName, displayNames); } #pragma mark - MCSessionDelegate - (void)session:(MCSession *)session didReceiveData:(NSData *)data fromPeer:(MCPeerID *)peerID { } - (void)session:(MCSession *)session didReceiveStream:(NSInputStream *)stream withName:(NSString *)streamName fromPeer:(MCPeerID *)peerID { } - (void)session:(MCSession *)session didFinishReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID atURL:(NSURL *)localURL withError:(NSError *)error { } - (void)session:(MCSession *)session didStartReceivingResourceWithName:(NSString *)resourceName fromPeer:(MCPeerID *)peerID withProgress:(NSProgress *)progress { } - (void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state { if (state == MCSessionStateConnecting) { NSLog(@"%@ received MCSessionStateConnecting for %@", self.peerId.displayName, peerID.displayName); } else if (state == MCSessionStateConnected) { NSLog(@"%@ received MCSessionStateConnected for %@", self.peerId.displayName, peerID.displayName); } else if (state == MCSessionStateNotConnected) { NSLog(@"%@ received MCSessionStateNotConnected for %@", self.peerId.displayName, peerID.displayName); } [self logPeers]; } #pragma mark - MCNearbyServiceAdvertiserDelegate - (void)advertiser:(MCNearbyServiceAdvertiser *)advertiser didNotStartAdvertisingPeer:(NSError *)error { NSLog(@"Advertiser %@ did not start advertising with error: %@", self.peerId.displayName, error.localizedDescription); } - (void)advertiser:(MCNearbyServiceAdvertiser *)advertiser didReceiveInvitationFromPeer:(MCPeerID *)peerID withContext:(NSData *)context invitationHandler:(void (^)(BOOL, MCSession *))invitationHandler { NSLog(@"Advertiser %@ received an invitation from %@", self.peerId.displayName, peerID.displayName); invitationHandler(YES, self.session); NSLog(@"Advertiser %@ accepted invitation from %@", self.peerId.displayName, peerID.displayName); [self logPeers]; } #pragma mark - MCNearbyServiceBrowserDelegate - (void)browser:(MCNearbyServiceBrowser *)browser didNotStartBrowsingForPeers:(NSError *)error { NSLog(@"Browser %@ did not start browsing with error: %@", self.peerId.displayName, error.localizedDescription); } - (void)browser:(MCNearbyServiceBrowser *)browser foundPeer:(MCPeerID *)peerID withDiscoveryInfo:(NSDictionary *)info { NSLog(@"Browser %@ found %@", self.peerId.displayName, peerID.displayName); // Should I invite the peer or should the peer invite me? Let the decision be based on the comparison of the hash values of the peerId. BOOL shouldInvite = self.peerId.hash < peerID.hash; if (shouldInvite) { // I will invite the peer, the remote peer will NOT invite me. NSLog(@"Browser %@ invites %@ to connect", self.peerId.displayName, peerID.displayName); [self.browser invitePeer:peerID toSession:self.session withContext:nil timeout:10]; } else { // I will NOT invite the peer, the remote peer will invite me. NSLog(@"Browser %@ does not invite %@ to connect", self.peerId.displayName, peerID.displayName); } } - (void)browser:(MCNearbyServiceBrowser *)browser lostPeer:(MCPeerID *)peerID { NSLog(@"Browser %@ lost %@", self.peerId.displayName, peerID.displayName); [self logPeers]; } @end |
Assuming that you already have some experience with the basics of the Multipeer Connectivity Framework, a lot of this code will look familiar to you. You will recognise the MCNearbyServiceAdvertiser, MCNearbyServiceBrowser, MCSession as well as their delegates.
In line 3 you see the import statement
1 |
#import "PFLoggingConsole.h" |
As explained earlier this will cause all NSLog output to be written to the device screen.
The connect() method creates a session, and starts the browser as well as the advertiser. The disconnect() methods neatly stops them, and calls disconnect on the session.
Now remember what this tutorial was all about: Is it possible to set up a session between peers and exchange data without any user interaction?
The implementation of this feature is in 2 snippets of code:
The first snippet is in the MCNearbyServiceBrowserDelegate, line 132
1 2 3 4 5 6 |
- (void)browser:(MCNearbyServiceBrowser *)browser foundPeer:(MCPeerID *)peerID withDiscoveryInfo:(NSDictionary *)info { NSLog(@"Browser %@ found %@", self.peerId.displayName, peerID.displayName); NSLog(@"Browser %@ invites %@ to connect", self.peerId.displayName, peerID.displayName); [self.browser invitePeer:peerID toSession:self.session withContext:nil timeout:10]; } |
This method is called when a browser finds a peer. In most tutorials the app stores each peer that it has found and presents the peer names in a table view. In the tableview a user can select a peer that he wants to connect with. In our ad hoc peer connection scenario we want to refrain from this user interaction. We do not even want to present the peer to the user. So as soon as a browser has found a peer it invites the peer to connect. That’s exactly what happens in the snippet above.
Below is the second snippet copied from the MCNearbyServiceAdvertiserDelegate, line 117
1 2 3 4 5 6 |
- (void)advertiser:(MCNearbyServiceAdvertiser *)advertiser didReceiveInvitationFromPeer:(MCPeerID *)peerID withContext:(NSData *)context invitationHandler:(void (^)(BOOL, MCSession *))invitationHandler { NSLog(@"Advertiser %@ received an invitation from %@", self.peerId.displayName, peerID.displayName); invitationHandler(YES, self.session); NSLog(@"Advertiser %@ accepted invitation from %@", self.peerId.displayName, peerID.displayName); } |
This method is called when an advertiser has received an invitation from a peer. In most tutorials this call is used to present a dialog to the user, offering the possibility to accept or decline the invitation. In our scenario we do not involve the user, therefore we blindly accept the invitation, by calling the invitationHandler with YES.
Create a session between 2 devices
Now we know all about the test app, let’s take a look what happens when we connect 2 peers. I will use the simulator and my iPhone. Below you can see the output of the apps.
- The simulator starts first. The connect method starts the simulator-browser as well as the simulator-advertiser. There are no other peers around, so the simulator stays quietly in the connecting state.
- The iPhone launches the app. The connect method starts the iPhone-browser as well as the iPhone-advertiser.This time there is another peer around: the simulator.
- The iPhone-browser discovers the simulator on the network. It invites the simulator to connect.
- The simulator-advertiser receives the invitation and accepts it. At that moment there are no peers yet in its session.
- Based on the acceptation by the simulator-advertiser the iPhone receives a call that a session has been established with the simulator.
- The simulator now also receives a call that a session was established with the iPhone. Both peers now have a shared session and the can start exchanging data. So far so good …..
- …… but wait…… The simulator is still browsing for peers, and it discovers the iPhone. And it does invite the iPhone. But it already has a session with the iPhone, so why invite again? I would expect that the multipeer framework would recognise this situation and deal with it by not sending out a new invitation. Is this a bug? Or is it a feature for a use case that I am unaware of?
- Anyway….. The iPhone friendly accepts the invitation, and the result is that it still has a session with the simulator. Steps 7 and 8 seem like overhead, because they do not change anything in the state of both peers.
- The simulator receives a call that the session with the iPhone was no longer connected. Why? There is obviously something wrong here.
Conclusion
We are not able to setup a session between 2 peers without user interaction. One of the peers is almost immediately disconnected. At point 6 everything seems to be fine, but at point 7 the simulator continues to invite a peer that it was already connected with. In the next part of this series I will show a way to prevent this mutual invitation problem.”
5 Comments on “Multipeer Connectivity (part 1)”
Is there any way to find nearby devices with wifi that doesnt run the same app? That is to discover all devices nearby in a public place.
I don’t think that is possible
Thanks so much for publishing this, I have been having trouble enabling this for ages, all the other blog posts I have read have not helped, until this one. Legend!!!
Pingback: Flatiron School Presents – The Multipeer Connectivity Framework (Lattice) | Xida Zheng
Pingback: Flatiron School Presents – The Multipeer Connectivity Framework (Lattice) | Xida Codes iOS