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 4 of 6 of this article. Click here to view the first page.

Handling Errors and Disconnects

Here's something to remember when it comes to writing networked code: it is extremely unpredictable. At any given moment, the connection may be broken and you need to gracefully handle that on either side, both client and server.

Here's how to handle the client side. Say the client is waiting to be connected, or that the connection has just been established, and the server suddenly goes away. What you do next depends on your app, but in the case of Snap! you'll return the player to the main screen.

To handle this situation, you have to check for the GKPeerStateDisconnected state in your GKSession delegate methods. Add the following to MatchmakingClient.m:

- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state
{
	. . .

	switch (state)
	{
		. . .

		// You're now connected to the server.
		case GKPeerStateConnected:
			if (_clientState == ClientStateConnecting)
			{
				_clientState = ClientStateConnected;
			}		
			break;

		// You're now no longer connected to the server.
		case GKPeerStateDisconnected:
			if (_clientState == ClientStateConnected)
			{
				[self disconnectFromServer];
			}
			break;

		case GKPeerStateConnecting:
			. . .
	}
}

Earlier, you didn't implement anything in the cases for GKPeerStateConnected and GKPeerStateDisconnected, but now you'll move the state machine ahead to the "connected" state for the former, and call the new disconnectFromServer method for the latter. Add this method to the class:

- (void)disconnectFromServer
{
	NSAssert(_clientState != ClientStateIdle, @"Wrong state");

	_clientState = ClientStateIdle;

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

	_availableServers = nil;

	[self.delegate matchmakingClient:self didDisconnectFromServer:_serverPeerID];
	_serverPeerID = nil;
}

Here you return the MatchmakingClient to the "idle" state, and clean up and destroy the GKSession object. You also call a new delegate method to let the JoinViewController know that the client is now disconnected.

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

- (void)matchmakingClient:(MatchmakingClient *)client didDisconnectFromServer:(NSString *)peerID;

This takes care of the scenario where a client who is already connected to the server gets disconnected, but it's slightly different for a client who is still in the process of connecting. That situation gets handled by yet another GKSessionDelegate method. Replace the following method in MatchmakingClient.m:

- (void)session:(GKSession *)session connectionWithPeerFailed:(NSString *)peerID withError:(NSError *)error
{
	#ifdef DEBUG
	NSLog(@"MatchmakingClient: connection with peer %@ failed %@", peerID, error);
	#endif

	[self disconnectFromServer];
}

There's nothing special here. You simply call disconnectFromServer whenever this happens. Note that this delegate method will also be called when a client tries to connect and the server explicitly calls denyConnectionFromPeer:, such as when there are already three clients active.

Because you added a new method to the MatchmakingClientDelegate protocol, you also have to implement this method in JoinViewController.m:

- (void)matchmakingClient:(MatchmakingClient *)client didDisconnectFromServer:(NSString *)peerID
{
	_matchmakingClient.delegate = nil;
	_matchmakingClient = nil;
	[self.tableView reloadData];
	[self.delegate joinViewController:self didDisconnectWithReason:_quitReason];
}

That's pretty simple, except maybe for the last line. Because you want to return the player to the main screen, the JoinViewController has to let the MainViewController know that the player got disconnected. There are different reasons why a player can get disconnected, and you need to let the main screen know why, so it can display an alert view if necessary.

For example, if the player quit the game on purpose, then no alert should be shown, because the player already knows why he got disconnected – after all, he pressed the exit button himself. But in case of a networking error, it's good to show some kind of explanation.

That means there are two more things to do here: add this new delegate method to JoinViewControllerDelegate, and add the _quitReason variable.

First, the delegate method. Add the following signature in the appropriate place in JoinViewController.h:

- (void)joinViewController:(JoinViewController *)controller didDisconnectWithReason:(QuitReason)reason;

Xcode will complain now because it doesn't know the symbol QuitReason. This is a typedef that you'll use in a few different classes, so add it to Snap-Prefix.pch so it will be visible in all your code:

typedef enum
{
	QuitReasonNoNetwork,          // no Wi-Fi or Bluetooth
	QuitReasonConnectionDropped,  // communication failure with server
	QuitReasonUserQuit,           // the user terminated the connection
	QuitReasonServerQuit,         // the server quit the game (on purpose)
}
QuitReason;

Those are the four reasons that Snap! recognizes. The JoinViewController needs an instance variable that stores the reason for quitting. You will set this variable to the proper value in a few different places, and then when the client truly disconnects, you'll pass it along to your own delegate.

Add the instance variable to JoinViewController:

@implementation JoinViewController
{
	. . .
	QuitReason _quitReason;
}

You'll give this variable its initial value in viewDidAppear:

- (void)viewDidAppear:(BOOL)animated
{
	[super viewDidAppear:animated];

	if (_matchmakingClient == nil)
	{
		_quitReason = QuitReasonConnectionDropped;

		// ... existing code here ...
	}
}

The default value for _quitReason is "connection dropped." Unless the user quits for another reason – for example, by pressing the exit button – a server disconnect will be regarded as a networking problem, rather than something that happened intentionally.

Because you've added a new method to JoinViewController's own delegate protocol, you also have to do some work in MainViewController. Add the following method to MainViewController.m, in the JoinViewControllerDelegate section:

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

If the disconnect happened because of a network error, then you'll close the Join Game screen and show an alert. The code for showDisconnectedAlert is as follows:

- (void)showDisconnectedAlert
{
	UIAlertView *alertView = [[UIAlertView alloc] 
		initWithTitle:NSLocalizedString(@"Disconnected", @"Client disconnected alert title")
		message:NSLocalizedString(@"You were disconnected from the game.", @"Client disconnected alert message")
		delegate:nil
		cancelButtonTitle:NSLocalizedString(@"OK", @"Button: OK")
		otherButtonTitles:nil];

	[alertView show];
}

Try it out. Connect a client to the server and tap the Home button on the server device (or exit the app completely). After a second or two, the server will become unreachable and the connection will be dropped at the client end. (It will also be dropped at the server end, but because the server app is now suspended, the server will not see any of this until the app comes to the foreground again.)

The debug output for the client says:

Snap[98048:1bb03] MatchmakingClient: peer 1700680379 changed state 3
Snap[98048:1bb03] dealloc <JoinViewController: 0x9570ee0>

State 3 is, of course, GKPeerStateDisconnected. The app returns to the main screen with an alert message:

The disconnected alert

As you can see in the debug output, the JoinViewController got properly deallocated. Along with that view controller, the MatchmakingClient object should have gotten deallocated as well. If you want to know for sure, add an NSLog() to dealloc:

- (void)dealloc
{
	#ifdef DEBUG
	NSLog(@"dealloc %@", self);
	#endif
}

This is pretty good, but what if the player taps the exit button after she's been connected to the server? In that case, the client should be the one to break the connection and no alert should be displayed. You can make that happen by fixing exitAction: in JoinViewController.m:

- (IBAction)exitAction:(id)sender
{
	_quitReason = QuitReasonUserQuit;
	[_matchmakingClient disconnectFromServer];
	[self.delegate joinViewControllerDidCancel:self];
}

First you set the quit reason to "user quit," and then you tell the client to disconnect. Now when you receive the matchmakingClient:didDisconnectFromServer: callback message, it will tell the MainViewController that the reason is "user quit," and no alert message is shown.

Xcode complains that the "disconnectFromServer" method is unknown, but that's only because you didn't put it in MatchmakingClient.h yet. Do that now:

- (void)disconnectFromServer;

Run the app again, make a connection, and then press the exit button on the client. In the server debug output you should see that the client disconnected. The client's name should also disappear from the table view.

Note: If you restore the server app after putting it into the background with the Home button, then you need to go back to the main screen and press Host Game again. The GKSession object is no longer valid after the app has been suspended.

Note: If you restore the server app after putting it into the background with the Home button, then you need to go back to the main screen and press Host Game again. The GKSession object is no longer valid after the app has been suspended.

Contributors

Over 300 content creators. Join our team.