How To Make a Simple Playing Card Game with Multiplayer and Bluetooth, Part 2

This is a post by iOS Tutorial Team member Matthijs Hollemans, an experienced iOS developer and designer. You can find him on Google+ and Twitter. Welcome back to our monster 7-part tutorial series on creating a multiplayer card game over Bluetooth or Wi-Fi using UIKit! If you are new to this series, check out the […] By Matthijs Hollemans.

Leave a rating/review
Save for later
Share
You are currently viewing page 5 of 6 of this article. Click here to view the first page.

"No Network" Errors

Game Kit only lets you set up a peer-to-peer connection over Bluetooth or a Wi-Fi network. If neither Bluetooth nor Wi-Fi are available, you should give the user a nice error message. Fatal errors with GKSession such as these are reported in session:didFailWithError:, so replace that method in MatchmakingClient.m with the following:

- (void)session:(GKSession *)session didFailWithError:(NSError *)error
{
	#ifdef DEBUG
	NSLog(@"MatchmakingClient: session failed %@", error);
	#endif

	if ([[error domain] isEqualToString:GKSessionErrorDomain])
	{
		if ([error code] == GKSessionCannotEnableError)
		{
			[self.delegate matchmakingClientNoNetwork:self];
			[self disconnectFromServer];
		}
	}
}

The actual error is reported in an NSError object, and if that is a GKSessionCannotEnableError, then the network simply isn't available. In that case, you tell your delegate (with a new method) and disconnect from the server.

Add this new delegate method to the protocol in MatchmakingClient.h:

- (void)matchmakingClientNoNetwork:(MatchmakingClient *)client;

And its implementation in JoinViewController.m:

- (void)matchmakingClientNoNetwork:(MatchmakingClient *)client
{
	_quitReason = QuitReasonNoNetwork;
}

That was pretty simple: you just set the quit reason to "no network." Because the MatchmakingClient calls disconnectFromServer, the JoinViewController also gets a didDisconnectFromServer message and tells the MainViewController about it. All you have to do now is make MainViewController handle this new quit reason.

Replace the following method in MainViewController.m:

- (void)joinViewController:(JoinViewController *)controller didDisconnectWithReason:(QuitReason)reason
{
	if (reason == QuitReasonNoNetwork)
	{
		[self showNoNetworkAlert];
	}
	else if (reason == QuitReasonConnectionDropped)
	{
		[self dismissViewControllerAnimated:NO completion:^
		{
			[self showDisconnectedAlert];
		}];
	}
}

The code for showNoNetworkAlert is:

- (void)showNoNetworkAlert
{
	UIAlertView *alertView = [[UIAlertView alloc] 
		initWithTitle:NSLocalizedString(@"No Network", @"No network alert title")
		message:NSLocalizedString(@"To use multiplayer, please enable Bluetooth or Wi-Fi in your device's Settings.", @"No network alert message")
		delegate:nil
		cancelButtonTitle:NSLocalizedString(@"OK", @"Button: OK")
		otherButtonTitles:nil];

	[alertView show];
}

To test this code, run the app on a device in Airplane Mode (with Wi-Fi and Bluetooth turned off).

Note: On my devices, I have to go into the Join Game screen (where nothing happens), press the exit button to go back to main, and then go into the Join Game screen again. I'm not sure why Game Kit doesn't recognize this problem on the first try. Maybe a more robust solution would use the Reachability API to check for Bluetooth and Wi-Fi availability.

The debug output should say:

MatchmakingClient: session failed Error Domain=com.apple.gamekit.GKSessionErrorDomain Code=30509 "Network not available." UserInfo=0x1509b0 {NSLocalizedFailureReason=WiFi and/or Bluetooth is required., NSLocalizedDescription=Network not available.}

And the screen shows an alert view:

The no-network alert

For this "no network" error, you don't actually leave the Join Game screen, even though you stop the session and any networking activity. I think that jumping back to the main screen would be too disorienting for the user.

Note: The code that displays the alert views – and in fact any code that displays text in this app – uses the NSLocalizedString() macro for internationalization. Even if your apps only do English at first, it's smart to prepare your code for localizations that you may do later. For more information, see this tutorial.

There is one more situation you need to handle on the client side. In my testing, I found that sometimes a server becomes unavailable while a client is trying to connect to it. In that case, the client receives a callback with state GKPeerStateUnavailable.

If you didn't handle this situation, then eventually the client would timeout, and the user would get some kind of error message. But you can code the app to check for this type of disconnect, too.

In MatchmakingClient.m, change the case for GKPeerStateUnavailable to:

		// The client sees that a server goes away.
		case GKPeerStateUnavailable:
			if (_clientState == ClientStateSearchingForServers)
			{
				// ... existing code here ...
			}
			
			// Is this the server we're currently trying to connect with?
			if (_clientState == ClientStateConnecting && [peerID isEqualToString:_serverPeerID])
			{
				[self disconnectFromServer];
			}			
			break;

Handling Errors On the Server

On the server, dealing with disconnects and errors is very similar. You already have the code in place to deal with clients who disconnect, so that's easy.

First, handle the "no network" situation. In MatchmakingServer.m, change session:didFailWithError: to:

- (void)session:(GKSession *)session didFailWithError:(NSError *)error
{
	#ifdef DEBUG
	NSLog(@"MatchmakingServer: session failed %@", error);
	#endif

	if ([[error domain] isEqualToString:GKSessionErrorDomain])
	{
		if ([error code] == GKSessionCannotEnableError)
		{
			[self.delegate matchmakingServerNoNetwork:self];
			[self endSession];
		}
	}
}

This is almost identical to what you did with the MatchmakingClient, except that now you call a method named endSession to clean up after yourself. Add endSession:

- (void)endSession
{
	NSAssert(_serverState != ServerStateIdle, @"Wrong state");

	_serverState = ServerStateIdle;

	[_session disconnectFromAllPeers];
	_session.available = NO;
	_session.delegate = nil;
	_session = nil;

	_connectedClients = nil;

	[self.delegate matchmakingServerSessionDidEnd:self];
}

No big surprises here. You call two new delegate methods, matchmakingServerNoNetwork: and matchmakingServerSessionDidEnd:. Add these to your protocol in MatchmakingServer.h, then implement them in the HostViewController.

First, the new method signatures for the protocol:

- (void)matchmakingServerSessionDidEnd:(MatchmakingServer *)server;
- (void)matchmakingServerNoNetwork:(MatchmakingServer *)server;

Then, the corresponding implementations in HostViewController.m:

- (void)matchmakingServerSessionDidEnd:(MatchmakingServer *)server
{
	_matchmakingServer.delegate = nil;
	_matchmakingServer = nil;
	[self.tableView reloadData];
	[self.delegate hostViewController:self didEndSessionWithReason:_quitReason];
}

- (void)matchmakingServerNoNetwork:(MatchmakingServer *)server
{
	_quitReason = QuitReasonNoNetwork;
}

Again, you've seen this logic before. To make this work, add the _quitReason instance variable to HostViewController:

@implementation HostViewController
{
	. . .
	QuitReason _quitReason;
}

And add a new method to its delegate protocol in HostViewController.h:

- (void)hostViewController:(HostViewController *)controller didEndSessionWithReason:(QuitReason)reason;

Finally, implement this method in MainViewController.m:

- (void)hostViewController:(HostViewController *)controller didEndSessionWithReason:(QuitReason)reason
{
	if (reason == QuitReasonNoNetwork)
	{
		[self showNoNetworkAlert];
	}
}

Run the app on the device in Airplane Mode and try to host a game. You should get the "no network" error. (If you don't get it the first time, exit to the main menu and tap the Host Game button again.) Go to the Settings app, turn off Airplane Mode, and switch back to Snap! again. Tap the Host Game button once more, and now clients should be able to find the server.

Just to play nice, you should also end the session when the user taps the exit button from the Host Game screen, so replace exitAction: in HostViewController.m with the following:

- (IBAction)exitAction:(id)sender
{
	_quitReason = QuitReasonUserQuit;
	[_matchmakingServer endSession];
	[self.delegate hostViewControllerDidCancel:self];
}

Of course, endSession is not a public method yet, so add it to the @interface of MatchmakingServer as well:

- (void)endSession;

Phew, what a lot of work just to get the server and clients to find each other! (Believe me, it would have been a ton more work if you didn't have GKSession!)

The cool thing is, you can drop the MatchmakingServer and MatchmakingClient classes into other projects and get all this functionality for free! Because these classes are designed to be independent of any view controllers, they are easy to reuse in other projects.

Contributors

Over 300 content creators. Join our team.