I hope I am not violating NDA by posting this question.
I am using the new multipeer connectivity to send using bluetooth some files to nearby devices. I have managed to send invitations, but I don't seem to get how to display a UIAlertView where the user can accept or decline the invite. Right now when a user sends, the file is automatically saved and there is no accept/decline alert.
The code is:
- (void) advertiser:(MCNearbyServiceAdvertiser *)advertiser
didReceiveInvitationFromPeer:(MCPeerID *)peerID
withContext:(NSData *)context
invitationHandler:(void(^)(BOOL accept,
MCSession *session))invitationHandler{
... save the data context
but with the alert:
- (void) advertiser:(MCNearbyServiceAdvertiser *)advertiser
didReceiveInvitationFromPeer:(MCPeerID *)peerID
withContext:(NSData *)context
invitationHandler:(void(^)(BOOL accept,
MCSession *session))invitationHandler{
DevicePeer = [MCPeerID alloc];
DevicePeer = peerID;
ArrayInvitationHandler = [NSArray arrayWithObject:[invitationHandler copy]];
// ask the user
UIAlertView *alertView = [[UIAlertView alloc]
initWithTitle:#""
message:#""
delegate:self
cancelButtonTitle:#"NO"
otherButtonTitles:#"YES", nil];
[alertView show];
alertView.tag = 2;
}
and the alert view method:
- (void) alertView:(UIAlertView *)alertView
clickedButtonAtIndex:(NSInteger)buttonIndex
{
// retrieve the invitationHandler
// get user decision
BOOL accept = (buttonIndex != alertView.cancelButtonIndex) ? YES : NO;
// respond
MCSession *session = [ArrayInvitationHandler objectAtIndex:0];
void (^invitationHandler)(BOOL, MCSession *) = [ArrayInvitationHandler objectAtIndex:0];
invitationHandler(accept, session);
}
When the user press YES the app crashes and I get the error:
[__NSMallocBlock__ nearbyConnectionDataForPeer:withCompletionHandler:]: unrecognized selector sent to instance 0x14d4e3b0'
I have looked up at the IOS developer library and there is no such method apart from
- (void)nearbyConnectionDataForPeer:(id)arg1 withCompletionHandler:(id)arg2{
}
which does no work. No info on the IOS developer forums. Any ideas?
Alessandro is right, this is not explained in the WWDC 2013 video. I struggled with this myself.
I think you're on the right track, you just have a couple logic errors.
I don't understand these two lines:
MCSession *session = [ArrayInvitationHandler objectAtIndex:0];
void (^invitationHandler)(BOOL, MCSession *) = [ArrayInvitationHandler objectAtIndex:0];
The object stored in your array is just your handler. The reason you're getting that crash is that the browser is seeing that accept is true and trying to connect the peer to the session, but the session you're giving it back is nil. To fix this, you want to pass back a new session that you create.
At first I was confused by the notion of creating a new session when one has already been created by the browser side, but then I realized that we don't get that session from the browser anywhere, and we can't really pass it back into the invitation handler if it doesn't exist!
So yeah, do this instead:
BOOL accept = (buttonIndex != alertView.cancelButtonIndex) ? YES : NO;
// respond
MCSession *session;
if(accept) {
session = [[MCSession alloc] initWithPeer:peer];
session.delegate = self;
}
void (^invitationHandler)(BOOL, MCSession *) = [ArrayInvitationHandler objectAtIndex:0];
invitationHandler(accept, session);
I suggest you to watch Nearby Networking with Multipeer Connectivity WWDC 2013 video on the Apple developers center. There is an example about this stuff and this is very well explained.
PS : Yes you break NDA (september 14) but now it's ok :)
Related
I’m working on an app that uses MPC. Sometimes it's working, A and B client connects like a charm but sometimes connection fails, I get the weird error from MCNearbyServiceBrowser.
First of all, I initialize advertiser, browser, and session on both A and B devices.
_peerID = [[MCPeerID alloc] initWithDisplayName:uniqueId];
session = [[MCSession alloc] initWithPeer:_peerID securityIdentity:nil encryptionPreference:MCEncryptionNone];
session.delegate = self;
NSDictionary *dict = #{#“uniqueId” : uniqueId};
_advertiser = [[MCNearbyServiceAdvertiser alloc] initWithPeer:_peerID discoveryInfo:dict serviceType:#“my-app”];
_advertiser.delegate = self;
_browser = [[MCNearbyServiceBrowser alloc] initWithPeer:_peerID serviceType:#“my-app”];
_browser.delegate = self;
[_advertiser startAdvertisingPeer];
[_browser startBrowsingForPeers];
A and B have a unique ID for deciding what device should invite the other, and what device should accept the invitation (it's necessary to prevent A and B inviting each other at the same time). After they found each other, found peer MCNearbyServiceBrowser delegate called. A device has less uniqueId, and it sends invitation request.
-(void)browser:(MCNearbyServiceBrowser *)browser foundPeer:(MCPeerID *)peerID withDiscoveryInfo:(NSDictionary<NSString *,NSString *> *)info {
if (![[session connectedPeers] containsObject:peerID]) {
NSInteger targetUniqueId = [[peerID displayName] integerValue];
NSInteger myUniqueId = [uniqueId integerValue];
if(myUniqueId<targetUniqueId){
NSLog(#“invitation sent”);
[browser invitePeer:peerID toSession:session withContext:nil timeout:inviteTimeout];
}
}
}
Accepting invitation (this called on B device):
-(void)advertiser:(MCNearbyServiceAdvertiser *)advertiser didReceiveInvitationFromPeer:(MCPeerID *)peerID withContext:(NSData *)context invitationHandler:(void (^)(BOOL, MCSession * _Nonnull))invitationHandler {
NSInteger targetUniqueId = [[peerID displayName] integerValue];
NSInteger myUniqueId = [uniqueId integerValue];
if(myUniqueId>targetUniqueId){
NSLog(#“accepting invitation”);
invitationHandler(YES, session);
}
}
Also implemented certificate handler like this (some post complaining about it, when not implemented it can cause connection problems w/o using security identity too):
-(void)session:(MCSession *)session didReceiveCertificate:(NSArray *)certificate fromPeer:(MCPeerID *)peerID certificateHandler:(void (^)(BOOL))certificateHandler {
certificateHandler(YES);
}
I logged both devices, then:
device A: invitation sent
device B: accepting invitation
device A: [MCNearbyServiceBrowser] Received an invitation response from [3362,090D4987], but we never sent it an invitation. Aborting!
Few secs after, when not connected I stop browsing peers, then start browsing again. After finding peer called I make same connection try again, re-invite peer on device B, what's accepting the invitation. The result can be the same or the connection state switches to Connected. These are the 2 options. Sometimes devices can connect in the first try or in less than 3 tries, but sometimes after many tries. Last time they could connect after about 40 abort message, it took about 15 mins when connection got established.
What I am doing wrong, why device A don't know anything about his own invitation?
An MCPeerID has a hashvalue member. You can compare them directly.
Two MCPeerID objects created using the same display name will not have the same hash value. This is to prevent name collisions.
If you want to recognize and be recognized by previously connected peers you must save and restore the actual MCPeerID objects.
Paste the following code into a playground and run it to see what I mean.
import MultipeerConnectivity
let hostName = "TestPlaygroundHostName"
let firstPeerID = MCPeerID(displayName: hostName)
let secondPeerID = MCPeerID(displayName: hostName)
firstPeerID.hashValue == secondPeerID.hashValue
I've done more than three days worth of research to fix my problem and I haven't seen anyone with a solution to my problem. The Browser invites an Advertiser, the Advertiser accepts, and the MCSession changes to a connected state. However, once the MCBrowserViewController is closed (by either the cancel or the done button), the MCSession disconnects. As long as I don't close the MCBrowserViewController, the MCSession stays connected. I don't understand why or how this works and I've even tried debugging the process, but it got too deep into threads for me to understand.
Please tell me it's just something wrong with my code.
-(void)setUpMultiPeer{
self.myPeerID = [[MCPeerID alloc] initWithDisplayName:pos];
self.mySession = [[MCSession alloc] initWithPeer:self.myPeerID];
self.browserVC = [[MCBrowserViewController alloc] initWithServiceType:#"svctype" session:self.mySession];
self.advertiser = [[MCAdvertiserAssistant alloc] initWithServiceType:#"svctype" discoveryInfo:nil session:self.mySession];
self.browserVC.delegate = self;
self.mySession.delegate = self;
}
-(void)dismissBrowserVC{
[self.browserVC dismissViewControllerAnimated:YES completion:nil];
}
-(void)browserViewControllerDidFinish:(MCBrowserViewController *)browserVC{
[self dismissBrowserVC];
}
-(void)browserViewControllerWasCancelled:(MCBrowserViewController *)browserViewController{
[self dismissBrowserVC];
}
-(void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state{
if (state == MCSessionStateConnected) {
NSLog(#"Connected!");
//Not entirely sure about this next line...
self.mySession = session;
}
else if (state == MCSessionStateNotConnected){
NSLog(#"Disconnected");
dispatch_async(dispatch_get_main_queue(), ^(void) {
UIAlertView *alert = [[UIAlertView alloc]initWithTitle: #"Somebody Left!"
message: [[NSString alloc] initWithFormat:#"%#", [peerID displayName]]
delegate: nil
cancelButtonTitle:#"Got it"
otherButtonTitles:nil];
[alert show];
});
}
}
//Called by a UIButton
-(IBAction)browseGO:(id)sender {
[self setUpMultiPeer];
[self presentViewController:self.browserVC animated:YES completion:nil];
}
//Called by a UISwitch
-(IBAction)advertiseSwitch:(id)sender {
if (_advertiseSwitcher.on) {
[self setUpMultiPeer];
[self.advertiser start];
}
else{
[self.advertiser stop];
}
}
I have also attempted using a unique MCSession for each the Browser and the Advertiser, but with no success.
What I did to solve my problem was start over from ground zero. Waiting on an answer from StackOverflow and from the Apple Developer Forum was taking too long, so I went back to what worked at the very beginning and I will build up from there once more.
Here is a link for an awesome tutorial that I found. I hope this helps someone solve their problem.
However, if anyone sees anything utterly wrong with my code in the question PLEASE DO TELL! I want to know what was causing that bug so that I can learn from my mistakes.
Thank you for stopping by and reading this question.
I am using this method to ask a nearby device to join the session:
When I do it I also start spinning an indicator
[browser invitePeer:key
toSession:session
withContext:nil
timeout:30];
Is there a method called in the moment of timeout? what if the other device goes out of range?
EDIT:
I notice that this state is never called:
if (state == MCSessionStateConnecting) {
NSLog(#"CONNECTING %#", peerID);
}
in case of timeouts on the browser side, you need to watch for the MCSessionStateNotConnected state. i do something like this:
- (void)session:(MCSession *)session
peer:(MCPeerID *)peerID
didChangeState:(MCSessionState)state
{
if (state == MCSessionStateNotConnected)
{
if (self.isWaitingForInvitation)
{
UIAlertView *alertView = [[UIAlertView alloc]
initWithTitle:NSLocalizedString(#"ERROR_TITLE", nil)
message:NSLocalizedString(#"ERROR_TEXT", nil)
delegate:self
cancelButtonTitle:NSLocalizedString(#"NO", #"Não")
otherButtonTitles:NSLocalizedString(#"YES", #"Sim"),
nil];
dispatch_sync(dispatch_get_main_queue(), ^{
[alertView show];
});
self.isWaitingForInvitation = NO;
}
}
use the dispatch_sync to make the alert popup right away.
Using a timer with a timer interval matching timeout parameter could be better idea.
How can i create a custom invitation which will display the discoveryInfo from the advertiser?
Here is the code from my advertiser:
// create Discovery Info
NSArray *objects=[[NSArray alloc] initWithObjects:#"datguy",#"28", nil];
NSArray *keys = [[NSArray alloc] initWithObjects:#"Name",#"Age", nil];
self.dictionaryInfo = [[NSDictionary alloc] initWithObjects:objects forKeys:keys];
// Setup Advertiser
self.advertiser = [[MCAdvertiserAssistant alloc] initWithServiceType:#"txt_msg_service" discoveryInfo:self.dictionaryInfo session:self.advertiseSession];
[self.advertiser start];
i think you got it backwards. IFAIK, the discoveryInfo of the MCAdvertiserAssistant is the information passed from the advertiser for the browsers.
This information is passed to:
- (void)browser:(MCNearbyServiceBrowser *)browser foundPeer:(MCPeerID *)peerID withDiscoveryInfo:(NSDictionary *)info
which is a delegate method for the MCNearbyServiceBrowser class. And to this:
– browserViewController:shouldPresentNearbyPeer:withDiscoveryInfo:
Which is the delegate for the MCBrowserViewController class.
This information should be used to check if this advertiser should be displayed for your user.
If you are interested in intercepting an invitation attempt, you should consider using the MCNearbyServiceAdvertiser class instead of the assistant. This way you can delegate the didReceiveInvitationFromPeer method and add some custom logic to it:
// pedido para entrar na sala.
- (void)advertiser:(MCNearbyServiceAdvertiser *)advertiser didReceiveInvitationFromPeer:(MCPeerID *)peerID
withContext:(NSData *)context
invitationHandler:(void (^)(BOOL accept, MCSession *session))invitationHandler {
// get the browser's info
NSDictionary *peerInfo = (NSDictionary *) [NSKeyedUnarchiver unarchiveObjectWithData:context];
//check if peer is valid for this room (add your custom logic in this method)
if ([self isValidForThisRoom:peerInfo]) {
//create an alert
NSObject *clientName = [peerInfo valueForKey:#"playerName"];
NSString *clientMessage = [[NSString alloc] initWithFormat:#"%# wants to connect. Accept?", clientName];
UIAlertView *alertView = [[UIAlertView alloc]
initWithTitle:#"Accept Connection?"
message:clientMessage
delegate:self
cancelButtonTitle:#"No"
otherButtonTitles:#"Yes", nil];
[alertView show];
// copy the invitationHandler block to an array to use it later
ArrayInvitationHandler = [NSArray arrayWithObject:[invitationHandler copy]];
} else {
// if the peer is not valid, decline the invitation
invitationHandler(NO, _session);
}
}
On the clickedButtonAtIndex delegate for the alertview, you can do something like this:
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
//obtem a decisao do usuario
BOOL accept = (buttonIndex != alertView.cancelButtonIndex) ? YES : NO;
// obtem o invitationHandler que foi guardado do didReceiveInvitationFromPeer
void (^invitationHandler)(BOOL, MCSession *) = [ArrayInvitationHandler objectAtIndex:0];
// chama o invitationHandler passando a nossa session
invitationHandler(accept, _session);
}
Two important things to notice is that:
Your session object should be an instance variable, not local as i saw in some code samples. In my experience, i just created a static shared instance to store the session object. This is also a good plan if you want to pass the session from one screen to another.
Notice that you should create an NSArray *ArrayInvitationHandler; variable to store the block code. I tried to do Block_copy but it had some conversion errors going on so someone here in stackoverflow decided to store in an array, which i think is elegant enough.
Anyway, i got it working using this setup. Hope it helps you :D
Alright, this involves a lot of network coding from this part of a multiplayer tutorial.
Basically, I'm trying to implement a multiplayer game using GameKit as per the tutorial linked above. I put in all of the necessary network coding and more or less understand it, however I've hit a snag somewhere along the line of method calls. Basically, the setup that I have is that one device acts as the host and the rest act as the clients. I have two separate UIViewcontrollers for the host and clients respectively where the connection is established.
Now the thing is, the connection gets established, but it's only the host that recognizes the connection, not the client. The problem is here:
- (void)sendPacketToAllClients:(Packet *)packet
{
[_players enumerateKeysAndObjectsUsingBlock:^(id key, Player *obj, BOOL *stop)
{
obj.receivedResponse = [_session.peerID isEqualToString:obj.peerID];
}];
GKSendDataMode dataMode = GKSendDataReliable;
NSData *data = [packet data];
NSError *error;
if (![_session sendDataToAllPeers:data withDataMode:dataMode error:&error])
{
NSLog(#"Error sending data to clients: %#", error);
}
}
This is implemented in GameMultiplayer, where the actual game will be implemented. What this method is supposed to be doing is sending data packets to each of the clients saying that the host received the connection request and is able to connect with them. After [_session sendDataToAllPeers:data withDataMode:dataMode error:&error] is called (the method in the if statement), this method is supposed to be triggered:
- (void)receiveData:(NSData *)data fromPeer:(NSString *)peerID inSession:(GKSession *)session context:(void *)context
{
#ifdef DEBUG
NSLog(#"Game: receive data from peer: %#, data: %#, length: %d", peerID, data, [data length]);
#endif
Packet *packet = [Packet packetWithData:data];
if (packet == nil)
{
NSLog(#"Invalid packet: %#", data);
return;
}
Player *player = [self playerWithPeerID:peerID];
if (player != nil)
{
player.receivedResponse = YES; // this is the new bit
}
if (self.isServer)
[self serverReceivedPacket:packet fromPlayer:player];
else
[self clientReceivedPacket:packet];
}
This method is in the next part of the tutorial I linked above (which is here) and is supposed to receive the packets that the host sends to all clients and implement the next methods in this networking chain. However, the method never gets called. No debug breakpoints are triggered and I get nothing in the console.
I understand if I need to provide more source material, but there is a lot of network coding already implemented, so I want to keep it down to what people need to see. Also, [_session setDataReceiveHandler:self withContext:nil] and _session.delegate = self are written in another method that is called in GameMultiplayer, so that's not the problem. Does anyone know what I need to fix?
EDIT: As requested, here's where GKSession is initialized:
#property (nonatomic, strong, readonly) GKSession *session; //This is done in the header file
#synthesize session = _session; //This is done in the main file
- (void)startAcceptingConnectionsForSessionID:(NSString *)sessionID
{
if (_serverState == ServerStateIdle)
{
_serverState = ServerStateAcceptingConnections;
_connectedClients = [NSMutableArray arrayWithCapacity:self.maxClients];
_session = [[GKSession alloc] initWithSessionID:sessionID displayName:nil sessionMode:GKSessionModeServer];
_session.delegate = self;
_session.available = YES;
}
}
The session is initialized in MatchmakingServer, which is used in the host view controller. The session is then passed on to the main view controller of the app, which then initializes GameMultiplayer and sends the GKSession to it. Here's where the host view controller sends it to the main view controller:
- (IBAction)startAction:(id)sender
{
if (_matchmakingServer != nil && [_matchmakingServer connectedClientCount] > 0)
{
NSString *name = [self.nameTextField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if ([name length] == 0)
name = _matchmakingServer.session.displayName;
[_matchmakingServer stopAcceptingConnections];
[self.delegate hostViewController:self startGameWithSession:_matchmakingServer.session playerName:name clients:_matchmakingServer.connectedClients];
}
}
and then the main view controller handles that method call here:
- (void)hostViewController:(MatchmakerHost *)controller startGameWithSession:(GKSession *)session playerName:(NSString *)name clients:(NSArray *)clients
{
[self dismissViewControllerAnimated:NO completion:^
{
[self startGameWithBlock:^(GameMultiplayer *aGame)
{
[aGame startServerGameWithSession:session playerName:name clients:clients];
}];
}];
}
and finally, this is where that method call is implemented in GameMultiplayer:
- (void)startServerGameWithSession:(GKSession *)session playerName:(NSString *)name clients:(NSArray *)clients
{
_clients = clients;
const char* className = class_getName([[_clients objectAtIndex:0] class]);
NSLog(#"yourObject is a: %s", className);
self.isServer = YES;
_session = session;
_session.available = NO;
_session.delegate = self;
[_session setDataReceiveHandler:self withContext:nil];
_state = GameStateWaitingForSignIn;
[self.delegate gameWaitingForClientsReady:self];
// Create the Player object for the server.
Player *player = [[Player alloc] init];
player.name = name;
player.peerID = _session.peerID;
player.position = PlayerPositionBottom;
[_players setObject:player forKey:player.peerID];
// Add a Player object for each client.
int index = 0;
for (NSString *peerID in clients)
{
Player *player = [[Player alloc] init];
player.peerID = peerID;
[_players setObject:player forKey:player.peerID];
if (index == 0)
player.position = ([clients count] == 1) ? PlayerPositionTop : PlayerPositionLeft;
else if (index == 1)
player.position = PlayerPositionTop;
else
player.position = PlayerPositionRight;
index++;
}
NSLog(#"Players:");
Packet *packet = [Packet packetWithType:PacketTypeSignInRequest];
[self sendPacketToAllClients:packet];
// for (int i = 0; i < [_players count]; i++) {
// NSLog([NSString stringWithFormat:#"%#", [clients objectAtIndex:i]]);
// }
}
I think you are calling send to fast. When server realize about connection it will send confirmation to client to really establish connection - so client knows about it succeed.
If you are sending packets before that happens - it will be lost.
Just do this:
[self performSelector:#selector(sendPacketToAllClients) withObject:nil afterDelay:1.0];
instead of:
[self sendPacketToAllClients];
I had the same problem that connection is established in different moment with small delay on client. The best is to send first packet from client that he is ready to receive packets from server - and than proceed normally from there.
Also try debugging:
- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state
On both devices (server and client).
I have also had my troubles with GKSession. I was interested to learn (on this site) today that GKSession is being deprecated in favor of using the Multipeer Connectivity Framework. With luck, Wenderlich et al. will do a tutorial using the new technology. :)
The system has some similarities to GKSession, so is not too hard to wrap your head around.
Apple's doc link.