I have gone over the documentation, but there isn't much information on Multipeer Connectivity related to choosing a possible medium for peers to connect.
Multipeer Connectivity automatically discovers peers based on WiFi, or Bluetooth. Is there a way to limit this to only Bluetooth?
As #kdogisthebest correctly states, there's no way to force Multipeer Connectivity to use a particular network technology, but as your question relates to a particular problem with WiFi, this answer details what I'm doing to work around that.
I've worked around the issue of 'phantom' peers over WiFi by sending a shortened timestamp in the discoveryInfo when creating the MCNearybyServiceAdvertiser. There are several caveats here:
1) This solution assumes both devices have the same time. I ensure this by using a modified version of ios-ntp as the app's time source.
2) It also assumes that Advertising and Browsing do not run for too long. I have a set length of 60 seconds for discovery phases, and I completely re-init the browser/advertiser on each restart.
3) MPC doesn't seem to like too many bytes in the discoveryInfo so sending an NSTimeInterval based on epoch doesn't work. I had to truncate them.
So when my app enters discovery mode, it starts browsing and advertising simultaneously. The advertising code looks like:
- (void)startAdvertising {
if (_advertising){
NSLog(#"Already advertising");
return;
}
self.acceptedPeerIDNameMap = [NSMutableDictionary dictionary];
NSInteger timeStamp = [self shortenedNetworkTimeStamp];
NSDictionary *discoveryInfo = #{kAdvertisingDiscoveryInfoTimestampKey:[NSString stringWithFormat:#"%ld",(long)timeStamp]};
NSLog(#"Starting advertiser");
self.serviceAdvertiser = [[MCNearbyServiceAdvertiser alloc] initWithPeer:_myPeerID
discoveryInfo:discoveryInfo
serviceType:kServiceType];
_serviceAdvertiser.delegate = self;
[_serviceAdvertiser startAdvertisingPeer];
self.advertising = YES;
}
The method shortenedNetworkTimestamp just takes an NSTimeInterval (either using the ntp framework or timeIntervalSinceReferenceDate and removing 1400000000 from it.
Then when the browser discovers a peer, it checks whether the advertiser's timestamp is within the known discovery duration (in my case 60 seconds):
- (void)browser:(MCNearbyServiceBrowser *)browser foundPeer:(MCPeerID *)peerID withDiscoveryInfo:(NSDictionary *)info {
DLog(#"Browser found peer ID %#",peerID.displayName);
//Only one peer should invite the other
BOOL shouldInvite = [peerID.displayName compare:_myPeerID.displayName]==NSOrderedAscending;
//Don't re-send invitations
if (_peerInfoDisplayNameMap[peerID.displayName]){
DLog(#"Already connected to peerID %#",peerID.displayName);
shouldInvite = NO;
}
else if (_invitedPeerIDNameMap[peerID.displayName]){
DLog(#"Already invited peerID %#",peerID.displayName);
shouldInvite = NO;
}
//Invite if discovery info is valid
if (shouldInvite && [self discoveryInfoIsValid:info]) {
DLog(#"Inviting");
_invitedPeerIDNameMap[peerID.displayName] = peerID;
MCSession *session = [self availableSession];
[_serviceBrowser invitePeer:peerID toSession:session withContext:nil timeout:0];
}
else {
DLog(#"Not inviting");
}
}
The discovery info validity check is pretty simple - just make sure the timestamp sent in the info is inside of the discovery time range (in my case kDiscoveryPhaseDuration is 60 seconds):
- (BOOL)discoveryInfoIsValid:(NSDictionary *)info {
BOOL isValid = YES;
NSString *infoTimeStamp = info[kAdvertisingDiscoveryInfoTimestampKey];
NSTimeInterval sentTimeStamp = (infoTimeStamp) ? [infoTimeStamp doubleValue] : -1;
NSTimeInterval currentTimeStamp = [self shortenedNetworkTimeStamp];
if (sentTimeStamp==-1 || (currentTimeStamp - sentTimeStamp) > kDiscoveryPhaseDuration){
DLog(#"Expired discovery info (current=%f, sent=%f)",currentTimeStamp,sentTimeStamp);
isValid = NO;
}
return isValid;
}
Hopefully this helps. There are many other quirks in MPC that I'm handling in my own code but I think the above covers this specific problem.
This isn't possible with Multipeer Connectivity. There are no methods Apple puts in place to limit the connection to Bluetooth.
One answer here: Multipeer connectivity over Bluetooth? states "There is no explicit setting for bluetooth or Wifi, It will connect devices in whatever possible way they are available."
Related
My project need to receive BLE advertisement package,and handle the kCBAdvDataManufacturerData without connecting the BLE device.The BLE device send advertisement package 1 time per second.In an empty viewcontroller ,I receive advertisement package 1 time per second,but in my controller where I display the BLE advertisement package data ,the frequency I receive advertisement package reduce to 1 time/4 sec or lower.In this controller ,I send some http request and update UI using NSTimer 1 time per second.I handle kCBAdvDataManufacturerData in dispatch_sync(dispatch_get_global_queue(0, 0),and update UI in mainqueue .
Anybody have any idea to increase the frequency to receive the BLE advertisement package?
-1. First you need to startDiscovery.
-(void)startDiscovery
{
if (self.mBtmanager == nil) {
_mScanState = false;
self.mBtmanager = [[CBCentralManager alloc] initWithDelegate:self queue:nil];
self.mBtmanager.delegate = self;
}
if (self.mBtmanager.state != CBCentralManagerStatePoweredOn) {
_mScanState = true;
}
NSDictionary *scanOption = #{CBCentralManagerScanOptionAllowDuplicatesKey:#YES};
[self.mBtmanager scanForPeripheralsWithServices:[[NSArray alloc] initWithObjects:[CBUUID UUIDWithString:JAALEE_ADV_SERVICE_UUID_STRING], nil] options:scanOption];
}
- 2. You need to make connection to Beacon.
-(void)startConnectDevice:(CBPeripheral*)peripheral
{
peripheral.delegate = self;
[self.mBtmanager connectPeripheral:peripheral options:nil];
}
-3. Then change the broadcast interval using writeServicesUUID
[peripheral writeValue:value forCharacteristic:characteristic type:CBCharacteristicWriteWithResponse];
-finally CONFIGURE_BROADCAST_INTERVAL_CHARACTERISTIC_ will help you.
I am making a multiplayer game for iOS and I read the material in Apple Developer Center, specifically this one. Here is my code for custom matchmaking, which is pretty straightforward:
- (void)findProgrammaticMatch {
GKMatchRequest *request = [[GKMatchRequest alloc] init];
request.minPlayers = 2;
request.maxPlayers = 2;
request.defaultNumberOfPlayers = 2;
request.playersToInvite = nil;
request.playerAttributes = 0;
request.playerGroup = 0;
UILabel *loading = (UILabel *)[aiw viewWithTag:792];
[[GKMatchmaker sharedMatchmaker] findMatchForRequest:request withCompletionHandler:^(GKMatch *match, NSError *error) {
if (error){
//error handling
[loaderLayer stopAnimating];
UIButton *cancelButton = (UIButton *)[loaderLayer viewWithTag:442];
[cancelButton setTitle:#"Go Back" forState:UIControlStateNormal];
loading.text = #"Cannot find any players. Please try again later.";
} else if (match != nil) {
//save match
self.match = match;
self.match.delegate = self;
loading.text = #"Found a player. Preparing session...";
if (!self.matchStarted && match.expectedPlayerCount == 0) {
self.matchStarted = YES;
//begin game logic
[self.scene setState:1];
self.myTicket = 1000+arc4random_uniform(999);
[self.scene send:self.myTicket];
[self stopLoading];
}
}
}];
}
However, matchmaking fails when one or more devices are connected to the internet via cellular networks. When I investigated the underlying error I found out that even if it is a wifi to wifi case, the completion handler does not work as intended. That is, match.expectedPlayerCount is never 0. Instead, the game starts when - (void)match:(GKMatch *)match player:(NSString *)playerID didChangeState:(GKPlayerConnectionState)state handler is invoked after the completion handler as following:
...
- (void)match:(GKMatch *)match player:(NSString *)playerID didChangeState:(GKPlayerConnectionState)state {
switch (state) {
case GKPlayerStateConnected:
self.matchStarted = YES;
//begin game logic
[self.scene setState:1];
self.myTicket = 1000+arc4random_uniform(999);
[self.scene send:self.myTicket];
[self stopLoading];
break;
...
The problem now is if a device with 3g is connected (and matched-sort of) didChangeState is never invoked. I checked for several other related questions on the internet and this site, although they are far from being satisfactory. I also read that sandbox servers of Game Center are not reliable and for some people production version worked perfectly(it just works!) despite the errors in sandbox mode, but I don't want to take that risk. Has anybody have experienced similar problem with their multiplayer game?
Hgeg,
There is nothing wrong with your code.
You have to allow cellular data usage to your app which needs users permission.
The following paragraph is selected from Apple's support website :
At the Foundation layer, you can use the setAllowsCellularAccess:
method on NSMutableURLRequest to specify whether a request can be sent
over a cellular connection. You can also use the allowsCellularAccess
to check the current value.
At the Core Foundation layer, you can achieve the same thing by
setting the kCFStreamPropertyNoCellular property before opening a
stream obtained from the CFSocketStream or CFHTTPStream APIs.
In older versions of iOS, you can continue to use the
kSCNetworkReachabilityFlagsIsWWAN as a best-effort way of determining
whether traffic will be sent over a cellular connection, but you
should be aware of its limitations.
Good luck
Iman
According to the latest apple news, from iOS 9, the sand box mode will no longer exist, instead of the sandbox you'll have one unified environment.
So you'll have just one unified environments where you can share the same accounts, this should solve all the usual problems from the SandBox mode.
The new Unified System it's also compatible with TestFlight so you'll be able to test you code across multiple device and accounts.
All of these changes will be made directly by apple, so the only think that you can do it's to wait until they update to the new system, so far it's the only way to be sure that it's not a Sand Box problem.
For more info please have a loot at the WWDC video
Based on the code that you have shown us, there should'nt be any issue regardless of the connection type, 3G or otherwise; however, if you previously interspersed code for exception handling that was tied back to connection status, or for graphics which represent a loading state, something could be tied up elsewhere logically and produce this error at this point in the game logic. Even a corrupt spinner graphic can become an issue.
Did you have any other exception handlers in the code that called the following:
request.playersToInvite
or
request.playerGroup
or
that changed a loader layer characteristic?
If I leave my device untouched for a while, the radios sleep to conserve power. If I check reachability in this state, I learn that the host I am checking for is unreachable. How can I wake the radio back up? Do I need to explicitly make a network call? Can I detect that the reason the network is unreachable is that the radio is asleep?
The basic check I'm doing works like this (taken from Apple's Reachability):
// The setup
- (id)init
{
self = [super init];
if (self)
{
_reachabilityRef = SCNetworkReachabilityCreateWithName(NULL, [#"myserver.example.com" UTF8String]);
}
return self;
}
// The check
- (BOOL)isReachable
{
return [self currentReachabilityStatus] != NotReachable;
}
// For reference
- (NetworkStatus)currentReachabilityStatus
{
NSAssert(_reachabilityRef != NULL, #"currentNetworkStatus called with NULL SCNetworkReachabilityRef");
NetworkStatus returnValue = NotReachable;
SCNetworkReachabilityFlags flags;
if (SCNetworkReachabilityGetFlags(_reachabilityRef, &flags))
{
returnValue = [self networkStatusForFlags:flags];
}
return returnValue;
}
You don't manage the device's radio. iOS does that for you.
Reachability says if the internet is reachable or not, not what radios are on or off. You can be reachable by cell, even if wifi is on (or just as easily, reachable by wifi, even if cell is available).
For the most part, radio power usage will be dwarfed by the screen's power usage. NSURLSessionConfiguration has a discretionary property that can be a hint to the OS that you don't need this request to start right away, but, that property is already ignored by background session tasks, to opt people into helping to save power.
I wonder if this Multipeer Connectivity framework is ready for use in the real world, given all the bugs that have been encountered by the community. I think I'm setting it up right, but all the other sample projects I've tried encounter similar issues.
The problem I'm having may be tied to some issue inherent to Bonjour or something, I can't figure it out, but basically the problem is as follows:
I have an active MCSession with a number of peers.
Now, if a device is in a session, and then force quits out, that "Peer" stays connected for an indefinite amount of time.
There's nothing I can do to force that user out, even though the browser:lostPeer: method is called for that peer and is no longer even showing up in the browser as
"Nearby".
The session:peer:didChangeState: method is not called for that peer.
When that peer that force quitted comes back to the app, they are "Found" again by the browser:foundPeer:withDiscoveryInfo: but still also exist in the session.connectedPeers NSArray. Obviously they don't receive any data or updates about the session still and are not actually connected.
The only thing that seems to work to register that original peer as MCSessionStateNotConnected to the session is by reconnecting that peer to the original session. Then there is a duplicate call to session:peer:didChangeState: where the new instance of the peerID is MCSessionStateConnected and shortly after the old instance of the peerID calls with MCSessionStateNotConnected.
The sample chat application demonstrates this issue well: https://developer.apple.com/library/ios/samplecode/MultipeerGroupChat/Introduction/Intro.html
Since there doesn't seem to be any way to manually force remove a peer from the session, what should I do? Should I try and rebuild the session somehow?
This Framework seems like a bit of a mess, but I'm trying to reserve judgement!
My only workaround to this type of issue has been to have a 1-1 relationship between sessions and peers. It complicates the sending of broadcasts, but at least allows for peer-level disconnects and cleanup through disconnecting/removing the session itself.
Update
To elaborate on my original answer, in order to be able to send data to connected peers it's necessary to maintain a reference to the session that was created for each peer. I've been using a mutable dictionary for this.
Once the invitation has been sent/accepted with a new session, use the MCSession delegate method to update the dictionary:
- (void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state {
if (state==MCSessionStateConnected){
_myPeerSessions[peerID.displayName] = session;
}
else if (state==MCSessionStateNotConnected){
//This is where the session can be disconnected without
//affecting other peers
[session disconnect];
[_myPeerSessions removeObjectForKey:peerID.displayName];
}
}
All peers can be accessed with a method that returns all values of the dictionary, and in turn all connectedPeers (in this case one) for each MCSession:
- (NSArray *)allConnectedPeers {
return [[_myPeerSessions allValues] valueForKey:#"connectedPeers"];
}
Sending data to a particular peer or via broadcast can be done with a method like this:
- (void)sendData:(NSData *)data toPeerIDs:(NSArray *)remotePeers reliable:(BOOL)reliable error:(NSError *__autoreleasing *)error {
MCSessionSendDataMode mode = (reliable) ? MCSessionSendDataReliable : MCSessionSendDataUnreliable;
for (MCPeerID *peer in remotePeers){
NSError __autoreleasing *currentError = nil;
MCSession *session = _myPeerSessions[peer.displayName];
[session sendData:data toPeers:session.connectedPeers withMode:mode error:currentError];
if (currentError && !error)
*error = *currentError;
}
}
Have you tried disconnecting the session before the application closes? This should remove the peer from the session properly and cleanup any resources allocated for the peer.
Specifically I mean something like [self.peer disconnect] in applicationWillTerminate:
I've been having similar problems. It seems though that if I have run my app on one iOS device, and connected to another, then quit and relaunch (say when I rerun from Xcode), then I am in a situation where I get a Connected message and then a Not Connected message a little later. This was throwing me off. But looking more carefully, I can see that the Not Connected message is actually meant for a different peerId than the one that has connected.
I think the problem here is that most samples I've seen just care about the displayName of the peerID, and neglect the fact that you can get multiple peerIDs for the same device/displayName.
I am now checking the displayName first and then verifying that the peerID is the same, by doing a compare of the pointers.
- (void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state {
MyPlayer *player = _players[peerID.displayName];
if ((state == MCSessionStateNotConnected) &&
(peerID != player.peerID)) {
NSLog(#"remnant connection drop");
return; // note that I don't care if player is nil, since I don't want to
// add a dictionary object for a Not Connecting peer.
}
if (player == nil) {
player = [MyPlayer init];
player.peerID = peerID;
_players[peerID.displayName] = player;
}
player.state = state;
...
I couldn't get the accepted answer to ever work, so what i did instead is have a timer that would fire to reset the connection when the browser would report not connected and there were no other connected peers.
-(void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state{
//DebugLog(#"session didChangeState: %ld",state);
if(resetTimer != nil){
[resetTimer invalidate];
resetTimer = nil;
}
if(state == MCSessionStateNotConnected){
[session disconnect];
[peerSessions removeObjectForKey:peerID.displayName];
[self removeGuidyPeerWithPeerID:peerID];
//DebugLog(#"removing all guides from peer %#",peerID);
if([localSession connectedPeers].count == 0){
DebugLog(#"nothing found... maybe restart in 3 seconds");
dispatch_async(dispatch_get_main_queue(), ^{
resetTimer = [NSTimer
scheduledTimerWithTimeInterval:3.0
target:self selector:#selector(onResetTimer:)
userInfo:nil
repeats:NO];
}
);
}
}
...
}
You can delete the peer from the MCBrowserViewController with following code in Swift 3:
self.mySession.cancelConnectPeer(self.myPeerID)
I'm trying to create an app that transfers data between 2+ phones using GKSession. Thing is there are two options:
First: using the GKPeerPicker.. However here I get stuck at the point where I have to implement my own WIFI interface.. apple provides no instructions on how to do that:
- (void)peerPickerController:(GKPeerPickerController *)picker didSelectConnectionType: (GKPeerPickerConnectionType)type {
if (type == GKPeerPickerConnectionTypeOnline) {
picker.delegate = nil;
[picker dismiss];
[picker autorelease];
// Implement your own internet user interface here.
}
}
Second: Skipping GKPeerPicker and doing the whole thing my self, like in this example. However the app dev documentation doesn't provide any instructions on how to send/receive data without using GKPeerPicker.. (nor could I find any example of that on thew web)
I just figured out how to connect devices without the peerpicker. It was a bit of a guessing game because the documentation is pretty unclear and I've looked for so long on the internet for any info about this. I'll try to explain everything here to clear up any questions anyone in the future might have.
From the documentation:
A GKSession object provides the ability to discover and connect to
nearby iOS devices using Bluetooth or Wi-fi.
This was the first step to understand it for me. I thought the GKPeerPickerController was responsible of the advertising and connecting but GKSession actually does all that.
The second thing to understand is that what is referred to as peers are not necessarily connected to you. They can just be nearby waiting to be discovered and connected to. All peers have a state
GKPeerStateAvailable (this is what's useful!)
GKPeerStateUnavailable
GKPeerStateConnected
GKPeerStateDisconnected
GKPeerStateConnecting
So how do we actually connect? Well first we have to create a GKSession object to be able to find peers around us and see when they become available:
// nil will become the device name
GKSession *gkSession = [[GKSession alloc] initWithSessionID:#"something.unique.i.use.my.bundle.name" displayName:nil sessionMode:GKSessionModePeer];
[gkSession setDataReceiveHandler:self withContext:nil];
gkSession.delegate = self;
gkSession.available = YES; // I'm not sure this if this is the default value, this might not be needed
Now we have some delegate calls to respond to. session:didReceiveConnectionRequestFromPeer: and session:peer:didChangeState (you should also handle the calls of GKSessionDelegate for disconnection and failure appropriately)
-(void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state
{
if(state == GKPeerStateDisconnected)
{
// A peer disconnected
}
else if(state == GKPeerStateConnected)
{
// You can now send messages to the connected peer(s)
int number = 1337;
[session sendDataToAllPeers:[NSData dataWithBytes:&number length:4] withDataMode:GKSendDataReliable error:nil];
}
else if (state == GKPeerStateAvailable)
{
// A device became available, meaning we can connect to it. Lets do it! (or at least try and make a request)
/*
Notice: This will connect to every iphone that's nearby you directly.
You would maybe want to make an interface similar to peerpicker instead
In that case, you should just save this peer in a availablePeers array and
call this method later on. For your UI, the name of the peer can be
retrived with [session displayNameForPeer:peerId]
*/
[session connectToPeer:peerID withTimeout:10];
}
}
The other peer now received a request that he should respond to.
-(void)session:(GKSession *)session didReceiveConnectionRequestFromPeer:(NSString *)peerID
{
// We can now decide to deny or accept
bool shouldAccept = YES;
if(shouldAccept)
{
[session acceptConnectionFromPeer:peerID error:nil];
}
else
{
[session denyConnectionFromPeer:peerID];
}
}
Finally to receive our little 1337 message
-(void)receiveData:(NSData *)data fromPeer:(NSString *)peer inSession:(GKSession*)session context:(void *)context
{
int number = 1337;
if([data isEqualToData:[NSData dataWithBytes:&number length:4]])
{
NSLog(#"Yey!");
}
}