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

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. Card games are quite popular on the App Store – over 2,500 apps and counting – so it’s about time that raywenderlich.com shows you how to make one! In addition, […] By Matthijs Hollemans.

Leave a rating/review
Save for later
Share

This is a post by iOS Tutorial Team member Matthijs Hollemans, an experienced iOS developer and designer. You can find him on and Twitter.

Card games are quite popular on the App Store – over 2,500 apps and counting – so it’s about time that raywenderlich.com shows you how to make one!

In addition, this monster 7-part tutorial will demonstrate how to make the game multiplayer, so you can play against your friends over Bluetooth or Wi-Fi using the peer-to-peer features of Game Kit.

Even though you’re making a game in this tutorial, you won’t be using OpenGL or a game framework like Cocos2D. Instead, you’ll be making it with nothing more than standard UIImageViews and UIView-based animation!

The reason for not using OpenGL or Cocos2D is that you don’t really need them! UIKit is fast enough for what you’re going to do here, and excels for simple card/board games like this. Most of the content on the screen is static and you’ll only be animating a few views at a time.

To follow along with this tutorial, you will need Xcode 4.3 or later. If you still have Xcode 4.2, then it’s time to upgrade!

Also, to test the multiplayer functionality, you will need at least two devices running iOS 5 or better. If you have a home Wi-Fi network, then you can make do with a single device, but ideally you’ll have more than one (I used four different devices while writing the code for this tutorial).

If you are new to this series, check out the introduction first. There you can see a video of the game, and we’ll invite you to our special Reader’s Challenge!

Then keep reading to impress your friends with the best card trick of all – your own multiplayer card game app!

Introducing: Snap!

The card game that you’ll be making is a children’s game called Snap!. This is what it looks like when it’s done:

The finished game of Snap!

In case you’re not familiar with the rules of Snap!, the game is played with 2 to 4 players using a standard 52-card deck. The goal is to win all the cards. You win cards by spotting a matching pair.

At the start of each round, the dealer shuffles the deck and deals out the cards clockwise around the table until there are none left in the deck. The cards are placed face down in front of the players.

The players take turns in clockwise order. If it’s your turn, you turn over the top card from your pile. The idea is to yell “Snap!” as quickly as possible when you see that any of the open cards form a matching pair. Two cards match if they have the same value, for example two kings. The suit does not matter.

The player who yells “Snap!” the quickest wins both piles and adds them to his own stack of face-down cards. This continues until one player has all the cards. If a player yells “Snap!” when there is no match on the table, he’ll have to pay one card to each of the other players.

The Application Flow

This is a multiplayer game that can be played over Bluetooth or Wi-Fi, and you’ll be using the Game Kit framework to make this happen. Note that you’re only going to use the peer-to-peer connectivity features of Game Kit – this app will not use Game Center at all. In fact, this tutorial restricts itself to a single class from Game Kit, GKSession.

In the first part of this tutorial, you’ll learn how to connect the players’ devices so that they can communicate over Bluetooth or Wi-Fi. With Snap! there will always be one player who hosts the game, also referred to as the “server.” The other players will join the session that this host has set up. These players are the “clients.”

The flow of the application is roughly as follows:

Mockup of the main screen

Above is the main screen of the app, and the first thing a player sees. She can decide to host a game that others can join, join a game hosted by someone else, or play a single-player game against the computer.

Mockup of the Host Game screen

The “Host Game” screen contains a table view that lists the players that have joined this session. Pressing the Start button will begin the game; from that point on, no new players can join. Usually the players decide between themselves beforehand who will host the game, and everybody else then joins that game.

Mockup of the Join Game screen

The “Join Game” screen looks very similar to the Host Game screen, except that there is no Start button. The table view lists the available games (there may very well be multiple people hosting a game). Here you tap on the game you want to join and wait until the host taps his Start button.

Mockup of the game screen

The game screen shows the players sitting around the table with their piles of face-up and face-down cards. The button in the lower-right corner lets the local player yell “Snap!” (there’s no actual need to run around the room screaming when you’re playing the iPhone game). If any of the other players yell “Snap!”, you’ll see a speech bubble next to their name.

Getting Started

To save you some time, I’ve already prepared a basic Xcode project that contains all the resources (such as images and nib files) that you’ll need for this first part. Download the starter code here and open Snap.xcodeproj in Xcode.

If you look at the source code, you’ll see that the project contains a single view controller, MainViewController. Run the app and it doesn’t look like much yet:

The starter version of the app

The main screen contains five UIImageViews for the logo cards (S, N, A, P and the joker) and three UIButtons. You can make this look a bit more lively by animating the image views, but first you’ll improve the look of those buttons.

The download also includes a file named Action_Man.ttf. This is a custom font that you’re going to use instead of the standard Helvetica, or any of the iPhone’s built-in fonts. If you double-click the Action_Man.ttf file on your Mac, it will open in Font Book:

The Action Man font in Font Book

That looks a bit more exciting than the standard fonts, if you ask me. Unfortunately, you can’t simply install this font on the Mac and then put it on your buttons in Interface Builder. You have to write some code to make this happen. First, however, you need to tell UIKit about this font, so your app can load it.

Open Snap-Info.plist in Xcode. Add a new row and give it the key “Fonts provided by application.” This is an array type. Give the first item the name of the font file, Action_Man.ttf:

Adding font to Info plist

You also need to add the actual TTF file to the project. Simply drag it into the Supporting Files section:

The TTF file added to the project

Note that you need to check the Snap target in the Add to Targets section of the dialog box that pops up. Otherwise, the font file will not be included in the application bundle:

Adding the new file to the target

Now you can simply set this font on your buttons and labels by doing:

UIFont *font = [UIFont fontWithName:@"Action Man" size:16.0f];
someLabel.font = font;

To avoid having to repeat this code over and over, make a category for it. From the File menu, select the New->File… option and choose the “Objective-C category” template. Fill in “SnapAdditions” for the category name and “UIFont” as the class that will take the category:

Creating a category on UIFont

This creates two new files, UIFont+SnapAdditions.h and UIFont+SnapAdditions.m. Just to keep things tidy, I placed these two files into a new group named Categories:

Placing the source files in the Categories group

Replace the contents of UIFont+SnapAdditions.h with:

@interface UIFont (SnapAdditions)

+ (id)rw_snapFontWithSize:(CGFloat)size;

@end

Then replace the contents of the .m file with:

#import "UIFont+SnapAdditions.h"

@implementation UIFont (SnapAdditions)

+ (id)rw_snapFontWithSize:(CGFloat)size
{
	return [UIFont fontWithName:@"Action Man" size:size];
}

@end

It’s a pretty simple category. You’ve just added a single class method, rw_snapFontWithSize:, which will create a new UIFont object with the Action Man font.

Notice that the file name of the font is Action_Man.ttf, with an underscore. But the name of the font has no underscore. You should always use the name of the font itself, not its filename. To find out the name of a font, double-click it from Finder to open it in Font Book. (Notice in the Font Book screenshot above that it does say Action Man and not Action_Man.)

The second thing to notice here is that I have prefixed the method name with “rw_”. It’s always a good idea to prefix methods that you add to categories on standard framework classes with your initials (or some other unique identifier), just to make sure the method name won’t clash with a built-in method, or a method that Apple may add in the future. It’s unlikely that Apple would actually add a method named “snapFontWithSize:”, but it’s better to be safe than sorry.

With these preparations in place, you can now set this new font on the buttons from your Main View Controller. At the top of MainViewController.m, add an import for your new category:

#import "UIFont+SnapAdditions.h"

Add the viewDidLoad method inside the @implementation block:

- (void)viewDidLoad
{
	[super viewDidLoad];

	self.hostGameButton.titleLabel.font = [UIFont rw_snapFontWithSize:20.0f];
	self.joinGameButton.titleLabel.font = [UIFont rw_snapFontWithSize:20.0f];
	self.singlePlayerGameButton.titleLabel.font = [UIFont rw_snapFontWithSize:20.0f];
}

That should do it. Run the app again. The button titles will show up in their new font:

The buttons with the new font

If you’re still seeing the old font, make sure the Action_Man.ttf file is truly being placed in the application bundle. Select the file and make sure the box in the Target Membership section is checked (in the File Inspector in the righthand pane of the Xcode window):

Target membership checkbox

Note: Be careful to read the font’s license agreement when you embed a custom TTF file into an app. Font files are protected by copyright and often require license fees if you want to distribute them as part of your app. Fortunately for you, the Action Man font is free to use and distribute.

The button titles look a lot better already, but a proper button has a border. To add borders, you’ll use a set of stretchable images. These images have already been added to the project and are named Button.png and ButtonPressed.png.

Because you have several different screens that all need to use similar-looking buttons, you’ll place the code for customizing the appearance of the buttons into a category as well.

Add a new category to the project. Again name it “SnapAdditions,” but this time, make it a category on UIButton. This creates two new files, UIButton+SnapAdditions.h and UIButton+SnapAdditions.m. Put these into the Categories group as well, and then replace the contents of the .h file with:

@interface UIButton (SnapAdditions)

- (void)rw_applySnapStyle;

@end

Replace the contents of the .m file with:

#import "UIButton+SnapAdditions.h"
#import "UIFont+SnapAdditions.h"

@implementation UIButton (SnapAdditions)

- (void)rw_applySnapStyle
{
	self.titleLabel.font = [UIFont rw_snapFontWithSize:20.0f];

	UIImage *buttonImage = [[UIImage imageNamed:@"Button"] stretchableImageWithLeftCapWidth:15 topCapHeight:0];
	[self setBackgroundImage:buttonImage forState:UIControlStateNormal];

	UIImage *pressedImage = [[UIImage imageNamed:@"ButtonPressed"] stretchableImageWithLeftCapWidth:15 topCapHeight:0];
	[self setBackgroundImage:pressedImage forState:UIControlStateHighlighted];
}

@end

Once again, there’s only one method (with the “rw_” prefix). When you call this method on a UIButton object, it will give the button a new background image, and it will also set the font.

Add an import for this new category in MainViewController.m:

#import "UIButton+SnapAdditions.h"

You can now replace viewDidLoad with the following:

- (void)viewDidLoad
{
	[super viewDidLoad];

	[self.hostGameButton rw_applySnapStyle];
	[self.joinGameButton rw_applySnapStyle];
	[self.singlePlayerGameButton rw_applySnapStyle];
}

Run the app again. Now the buttons look like real buttons:

The buttons now have borders

Animating the Intro

Now you’ll liven up the main screen a little. How about when the app starts, the logo cards fly into the screen?

To create this effect, add the following methods to MainViewController.m:

- (void)prepareForIntroAnimation
{
	self.sImageView.hidden = YES;
	self.nImageView.hidden = YES;
	self.aImageView.hidden = YES;
	self.pImageView.hidden = YES;
	self.jokerImageView.hidden = YES;
}

- (void)performIntroAnimation
{
	self.sImageView.hidden = NO;
	self.nImageView.hidden = NO;
	self.aImageView.hidden = NO;
	self.pImageView.hidden = NO;
	self.jokerImageView.hidden = NO;

	CGPoint point = CGPointMake(self.view.bounds.size.width / 2.0f, self.view.bounds.size.height * 2.0f);

	self.sImageView.center = point;
	self.nImageView.center = point;
	self.aImageView.center = point;
	self.pImageView.center = point;
	self.jokerImageView.center = point;

	[UIView animateWithDuration:0.65f
		delay:0.5f
		options:UIViewAnimationOptionCurveEaseOut
		animations:^
		{
			self.sImageView.center = CGPointMake(80.0f, 108.0f);
			self.sImageView.transform = CGAffineTransformMakeRotation(-0.22f);
			
			self.nImageView.center = CGPointMake(160.0f, 93.0f);
			self.nImageView.transform = CGAffineTransformMakeRotation(-0.1f);

			self.aImageView.center = CGPointMake(240.0f, 88.0f);

			self.pImageView.center = CGPointMake(320.0f, 93.0f);
			self.pImageView.transform = CGAffineTransformMakeRotation(0.1f);

			self.jokerImageView.center = CGPointMake(400.0f, 108.0f);
			self.jokerImageView.transform = CGAffineTransformMakeRotation(0.22f);
		}
		completion:nil];
}

The first method, prepareForIntroAnimation, simply hides the five UIImageViews that hold the logo cards. The actual animation happens in performIntroAnimation. First, you place the cards off-screen, horizontally centered but vertically below the bottom of the screen. Then you start a UIView animation block that places the UIImageViews at their final positions, to look like they’re fanned out from the center.

You’ll call these methods from viewWillAppear: and viewDidAppear:

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

	[self prepareForIntroAnimation];
}

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

	[self performIntroAnimation];
}

Now when you run the app, the cards fly up into the screen and fan out. Pretty cool.

Logo with fanned out cards

The animation is not perfect yet, though. It would be nicer if the buttons subtly faded into view while the cards were flying to their final positions. Add the following lines to the bottom of prepareForIntroAnimation:

- (void)prepareForIntroAnimation
{
	. . .
	
	self.hostGameButton.alpha = 0.0f;
	self.joinGameButton.alpha = 0.0f;
	self.singlePlayerGameButton.alpha = 0.0f;

	_buttonsEnabled = NO;
}

That makes the buttons fully transparent. Also add a second UIView-animation block to the end of performIntroAnimation:

- (void)performIntroAnimation
{
	. . .

	[UIView animateWithDuration:0.5f
		delay:1.0f
		options:UIViewAnimationOptionCurveEaseOut
		animations:^
		{
			self.hostGameButton.alpha = 1.0f;
			self.joinGameButton.alpha = 1.0f;
			self.singlePlayerGameButton.alpha = 1.0f;
		}
		completion:^(BOOL finished)
		{
			_buttonsEnabled = YES;
		}];
}

Here you simply animate the buttons back to fully opaque. But what is that _buttonsEnabled variable? You don’t want the user to tap the buttons while they’re still fading in. Only after the animations have completed should the buttons become available for tapping.

You’ll use the _buttonsEnabled instance variable to ignore taps on the buttons while the animation is still taking place. For now, just add this new instance variable to the @implementation section:

@implementation MainViewController
{
	BOOL _buttonsEnabled;
}

Run the app and check out the animation. Pretty smooth!

Game Kit and Multiplayer Games

Game Kit is a standard framework that comes with the iOS SDK. Its main features are for use in Game Center (which you won’t use in this tutorial) and voice chat, but it also has a peer-to-peer connectivity feature that connects devices over a Bluetooth connection. If all devices are on a local Wi-Fi network, Game Kit can also use that instead of Bluetooth. (There is also a provision for peer-to-peer matchmaking over the Internet, but you’ll have to write most of the code for that yourself – and these days it’s probably easier to use Game Center.)

Game Kit’s peer-to-peer feature is great for multiplayer games where the players are in the same room together and all using their own devices. Reportedly, the players cannot be more than about 10 meters (or 30 feet) away from each other when using Bluetooth.

So what does peer-to-peer connectivity mean? Each device that participates in a Game Kit networking session is named a “peer.” A device can act as a “server” that broadcasts a particular service, as a “client” that looks for servers that provide a particular service, or as both client and server at the same time. To do this, Game Kit uses Bonjour technology behind the scenes, but you don’t need to work directly with Bonjour in order to use Game Kit.

When using Bluetooth, devices don’t need to be paired, the way you’d need to pair a Bluetooth mouse or keyboard with your device. Game Kit simply lets clients discover servers, and once this connection is made, the devices can send messages to each other over the local network.

You don’t have the option to choose between Bluetooth or Wi-Fi; GameKit makes this decision for you. Bluetooth is also not supported in the Simulator, but Wi-Fi is.

While developing and testing for this tutorial, I found it easiest to use the Simulator and one or two physical devices, and play over the local Wi-Fi network. If you want to play over Bluetooth, you’ll need to have at least two physical devices that both have Bluetooth enabled.

Note: It’s possible to do network communications with Bonjour and Bluetooth without Game Kit, but if you’re building a multiplayer game, using Game Kit is a lot easier. It hides all the nasty networking stuff from you and gives you a single class to use, GKSession. That will be the only Game Kit class you’re going to be working with in this tutorial (and its delegate, GKSessionDelegate).

If you’re making a two-player multiplayer game that uses Bluetooth or Wi-Fi, then you can use Game Kit’s GKPeerPickerController to establish the connection between the two devices. It looks like this:

The GKPeerPickerController user interface

The GKPeerPickerController is pretty easy to use, but it’s limited to establishing a connection between two devices. Snap! can have up to four players, so this tutorial takes you through writing your own matchmaking code.

The “Host Game” Screen

In this section, you’ll add the Host Game screen to the app. This screen lets a player host a gaming session that other players can join. When you’re done, it will look like this:

The Host Game screen

The table view lists the players who have connected to this host, and the Start button begins the game. There is also a text field that allows you to name your player (by default it will use the name of your device).

Add a new UIViewController subclass to the project, named HostViewController. Disable the “With XIB for user interface” option. The starter code already comes with a fully-prepared nib file for the Host Game screen. You can find it in the “Snap/en.lproj” folder. Drag the HostViewController.xib file into the project.

The nib file looks like this:

The Host Game nib

Most of the UI elements are hooked up to properties and action methods, so you should add these to your HostViewController class. Otherwise, the app will crash when you try to load this nib.

In HostViewController.m, add the following lines to the class extension (at the top of the file):

@interface HostViewController ()
@property (nonatomic, weak) IBOutlet UILabel *headingLabel;
@property (nonatomic, weak) IBOutlet UILabel *nameLabel;
@property (nonatomic, weak) IBOutlet UITextField *nameTextField;
@property (nonatomic, weak) IBOutlet UILabel *statusLabel;
@property (nonatomic, weak) IBOutlet UITableView *tableView;
@property (nonatomic, weak) IBOutlet UIButton *startButton;
@end

Notice that you’re putting the IBOutlet properties inside the .m file, not in the .h file. This is one of the new features of the LLVM compiler that ships with the latest versions of Xcode (4.2 and up). It helps to keep your .h files really clean so that you only expose the properties and methods that other objects need to see.

Of course, you need to synthesize these properties, so add the following lines below @implementation:

@synthesize headingLabel = _headingLabel;
@synthesize nameLabel = _nameLabel;
@synthesize nameTextField = _nameTextField;
@synthesize statusLabel = _statusLabel;
@synthesize tableView = _tableView;
@synthesize startButton = _startButton;

Tip: It’s rumored that in the next version of Xcode (which you may already be using by the time you read this tutorial), you can simply leave out the @synthesize lines. But if you’re on Xcode 4.3, it’s still necessary to put them in.

Replace shouldAutorotateToInterfaceOrientation: so that it will only support the landscape orientation:

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
	return UIInterfaceOrientationIsLandscape(interfaceOrientation);
}

Also, add the following placeholder methods to the bottom of the file:

- (IBAction)startAction:(id)sender
{
}

- (IBAction)exitAction:(id)sender
{
}

#pragma mark - UITableViewDataSource

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
	return 0;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	return nil;
}

Finally, replace the @interface line in HostViewController.h with:

@interface HostViewController : UIViewController <UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate>

That’s enough to get a basic version of the Host Game screen working, but you still have to display it when the user taps the corresponding button on the main screen. Add an import statement to MainViewController.h:

#import "HostViewController.h"

And replace the hostGameAction: method in MainViewController.m with the following:

- (IBAction)hostGameAction:(id)sender
{
	if (_buttonsEnabled)
	{
		HostViewController *controller = [[HostViewController alloc] initWithNibName:@"HostViewController" bundle:nil];

		[self presentViewController:controller animated:NO completion:nil];
	}
}

If you run the app now, you’ll see that tapping the Host Game button instantly brings up the Host Game screen. It works, although it doesn’t look particularly pretty. You’re presenting this new view controller modally, but because you passed NO to the animated: parameter, there is no standard “slide up” animation for this new screen.

Such an animation wouldn’t look particularly good here – you’d see the felt background of the Host Game screen slide up over the felt from the main screen – so instead, create a new animation by adding the following method to MainViewController.m:

- (void)performExitAnimationWithCompletionBlock:(void (^)(BOOL))block
{
	_buttonsEnabled = NO;

	[UIView animateWithDuration:0.3f
		delay:0.0f
		options:UIViewAnimationOptionCurveEaseOut
		animations:^
		{
			self.sImageView.center = self.aImageView.center;
			self.sImageView.transform = self.aImageView.transform;

			self.nImageView.center = self.aImageView.center;
			self.nImageView.transform = self.aImageView.transform;

			self.pImageView.center = self.aImageView.center;
			self.pImageView.transform = self.aImageView.transform;

			self.jokerImageView.center = self.aImageView.center;
			self.jokerImageView.transform = self.aImageView.transform;
		}
		completion:^(BOOL finished)
		{
			CGPoint point = CGPointMake(self.aImageView.center.x, self.view.frame.size.height * -2.0f);

			[UIView animateWithDuration:1.0f
				delay:0.0f
				options:UIViewAnimationOptionCurveEaseOut
				animations:^
				{
					self.sImageView.center = point;
					self.nImageView.center = point;
					self.aImageView.center = point;
					self.pImageView.center = point;
					self.jokerImageView.center = point;
				}
				completion:block];

			[UIView animateWithDuration:0.3f
				delay:0.3f
				options:UIViewAnimationOptionCurveEaseOut
				animations:^
				{
					self.hostGameButton.alpha = 0.0f;
					self.joinGameButton.alpha = 0.0f;
					self.singlePlayerGameButton.alpha = 0.0f;
				}
				completion:nil];
		}];
}

Tip: It doesn’t really matter where you add this method (as long as it’s between the @implementation and @end directives). Previously you had to declare the method in the .h file, and put its signature in a class extension at the top of the .m file, or make sure that any method you call is higher up in the source file. That’s no longer necessary thanks to the LLVM compiler that ships with Xcode 4.3. The compiler is now smart enough to find the method, no matter where you put it in the source file, and even if you didn’t forward-declare it previously.

The animation in performExitAnimationWithCompletionBlock: slides the logo cards off the screen and at the same time fades out the buttons. When the animation is done, it executes the code from the block that you pass in as a parameter.

Now change hostGameAction: to:

- (IBAction)hostGameAction:(id)sender
{
	if (_buttonsEnabled)
	{
		[self performExitAnimationWithCompletionBlock:^(BOOL finished)
		{	
			HostViewController *controller = [[HostViewController alloc] initWithNibName:@"HostViewController" bundle:nil];

			[self presentViewController:controller animated:NO completion:nil];
		}];
	}
}

It’s almost the same as before, but now the logic that creates and presents the Host Game screen is wrapped in a block that gets performed when the exit animation completes. Run the app and see for yourself. You put the animation code in a separate method so that you can also use it when the user taps the other buttons.

As you can see in the nib, the Host Game screen also uses the default Helvetica font, and its Start button doesn’t have a border. This is easily fixed. Add these two imports to HostViewController.m:

#import "UIButton+SnapAdditions.h"
#import "UIFont+SnapAdditions.h"

And replace viewDidLoad with the following:

- (void)viewDidLoad
{
	[super viewDidLoad];

	self.headingLabel.font = [UIFont rw_snapFontWithSize:24.0f];;
	self.nameLabel.font = [UIFont rw_snapFontWithSize:16.0f];
	self.statusLabel.font = [UIFont rw_snapFontWithSize:16.0f];
	self.nameTextField.font = [UIFont rw_snapFontWithSize:20.0f];

	[self.startButton rw_applySnapStyle];
}

Because you created these convenient categories to add your font and style your buttons, it’s a snap (ha ha) to make the screen look good. Run the app to see for yourself.

There are a few more tweaks to make. The screen has a UITextField that allows the player to type in his name. When you tap in this text field, the on-screen keyboard slides up. The keyboard takes up about half of the screen space, and there is currently no way to dismiss it.

The on-screen keyboard

The first way to dismiss the keyboard is with the big blue Done button. Currently this does nothing when tapped, but adding the following method to HostViewController.m will solve that:

#pragma mark - UITextFieldDelegate

- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
	[textField resignFirstResponder];
	return NO;
}

The second way to dismiss the keyboard is to add the following code to the end of viewDidLoad:

- (void)viewDidLoad
{
	. . .

	UITapGestureRecognizer *gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self.nameTextField action:@selector(resignFirstResponder)];
	gestureRecognizer.cancelsTouchesInView = NO;
	[self.view addGestureRecognizer:gestureRecognizer];
}

You’re creating a gesture recognizer that responds to simple taps and adding it to the view controller’s main view. Now when the user taps outside of the text field, the gesture recognizer sends the “resignFirstResponder” message to the text field, which will make the keyboard disappear.

Note that you need to set the cancelsTouchesInView property to NO, otherwise it will no longer be possible to tap on anything else in the screen, such as the table view and the buttons.

Exiting the Host Game Screen

Speaking of the buttons, they don’t do anything yet. Leave the Start button alone for now, but it would be nice if you could return to the main screen by tapping the X in the bottom-left corner.

This button is hooked up to exitAction:, which is currently empty. It should close the screen, and you’ll implement this using a delegate protocol. There will be several view controllers in this app, and they will communicate with one another through delegates to keep the dependencies minimal and clean.

Add the following to HostViewController.h, above the @interface line:

@class HostViewController;

@protocol HostViewControllerDelegate <NSObject>

- (void)hostViewControllerDidCancel:(HostViewController *)controller;

@end

Inside the @interface section, add a new property:

@property (nonatomic, weak) id <HostViewControllerDelegate> delegate;

Properties need to be synthesized, so in HostViewController.m, do:

@synthesize delegate = _delegate;

Finally, replace exitAction: with:

- (IBAction)exitAction:(id)sender
{
	[self.delegate hostViewControllerDidCancel:self];
}

The idea should be clear: you’ve declared a delegate protocol for the HostViewController. When the exit button is tapped, the HostViewController tells its delegate that the Host Game screen has been cancelled. The delegate is then responsible for closing the screen.

In this case, the role of the delegate is played by the MainViewController, of course. Change the following line in MainViewController.h:

@interface MainViewController : UIViewController <HostViewControllerDelegate>

In MainViewController.m, the hostGameAction: method becomes:

- (IBAction)hostGameAction:(id)sender
{
	if (_buttonsEnabled)
	{
		[self performExitAnimationWithCompletionBlock:^(BOOL finished)
		{	
			HostViewController *controller = [[HostViewController alloc] initWithNibName:@"HostViewController" bundle:nil];
			controller.delegate = self;

			[self presentViewController:controller animated:NO completion:nil];
		}];
	}
}

Now you’re making the MainViewController the delegate of the HostViewController. Lastly, add the implementation of the delegate method to MainViewController.m:

#pragma mark - HostViewControllerDelegate

- (void)hostViewControllerDidCancel:(HostViewController *)controller
{
	[self dismissViewControllerAnimated:NO completion:nil];
}

This simply closes the HostViewController screen without an animation. Because MainViewController’s viewWillAppear will be called at this point, the flying cards animation will be performed again. Run the app and try it out.

Note: For debugging purposes, I like to make sure my view controllers (and any other objects) really do get deallocated when the screen gets dismissed, so I always add a dealloc method to my view controllers that logs a message to the Debug Output pane:

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

Even though this project uses ARC, it’s still possible that your apps leak memory. ARC greatly simplifies memory management, but it cannot magically make so-called “retain cycles” (or “ownership cycles”) disappear. If you have two objects that have strong pointers at each other, then they will stay in memory forever. That’s why I like to log when my objects get deallocated, just to keep an eye on things.

The Host Game screen is done for now. Before you can start the Game Kit session and broadcast your service, you have to build-in the Join Game screen. Otherwise, there’s no way for other devices to find that new Game Kit session!

The “Join Game” Screen

This screen looks very similar to the Host Game screen, but there are enough differences below the hood to warrant making this a totally separate class (rather than reusing or subclassing the HostViewController). But because it’s quite similar to what you did before, you can get through this quite quickly.

Add a new UIViewController subclass to the project and name it JoinViewController. Disable the XIB option, as I have already provided a nib for you in the starter code. Drag that nib file (from “Snap/en.lproj/”) into the project file.

Replace the contents of JoinViewController.h with:

@class JoinViewController;

@protocol JoinViewControllerDelegate <NSObject>

- (void)joinViewControllerDidCancel:(JoinViewController *)controller;

@end

@interface JoinViewController : UIViewController <UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate>

@property (nonatomic, weak) id <JoinViewControllerDelegate> delegate;

@end

This is very similar to what you did with the Host Game screen. Replace JoinViewController.m with:

#import "JoinViewController.h"
#import "UIFont+SnapAdditions.h"

@interface JoinViewController ()
@property (nonatomic, weak) IBOutlet UILabel *headingLabel;
@property (nonatomic, weak) IBOutlet UILabel *nameLabel;
@property (nonatomic, weak) IBOutlet UITextField *nameTextField;
@property (nonatomic, weak) IBOutlet UILabel *statusLabel;
@property (nonatomic, weak) IBOutlet UITableView *tableView;

@property (nonatomic, strong) IBOutlet UIView *waitView;
@property (nonatomic, weak) IBOutlet UILabel *waitLabel;
@end

@implementation JoinViewController

@synthesize delegate = _delegate;

@synthesize headingLabel = _headingLabel;
@synthesize nameLabel = _nameLabel;
@synthesize nameTextField = _nameTextField;
@synthesize statusLabel = _statusLabel;
@synthesize tableView = _tableView;

@synthesize waitView = _waitView;
@synthesize waitLabel = _waitLabel;

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

- (void)viewDidLoad
{
	[super viewDidLoad];

	self.headingLabel.font = [UIFont rw_snapFontWithSize:24.0f];
	self.nameLabel.font = [UIFont rw_snapFontWithSize:16.0f];
	self.statusLabel.font = [UIFont rw_snapFontWithSize:16.0f];
	self.waitLabel.font = [UIFont rw_snapFontWithSize:18.0f];
	self.nameTextField.font = [UIFont rw_snapFontWithSize:20.0f];

	UITapGestureRecognizer *gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self.nameTextField action:@selector(resignFirstResponder)];
	gestureRecognizer.cancelsTouchesInView = NO;
	[self.view addGestureRecognizer:gestureRecognizer];
}

- (void)viewDidUnload
{
	[super viewDidUnload];
	self.waitView = nil;
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
	return UIInterfaceOrientationIsLandscape(interfaceOrientation);
}

- (IBAction)exitAction:(id)sender
{
	[self.delegate joinViewControllerDidCancel:self];
}

#pragma mark - UITableViewDataSource

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
	return 0;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	return nil;
}

#pragma mark - UITextFieldDelegate

- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
	[textField resignFirstResponder];
	return NO;
}

@end

There’s not much new here, except for the waitView outlet. Notice that this property is declared “strong,” instead of “weak” like the other properties. That’s done because it’s actually a top-level view in the nib:

The Join Game nib

It’s important to mark this as strong so that something has a reference to it, to prevent it from becoming deallocated. You don’t have to do that for the first view because it is retained from the built-in self.view property on the view controller.

After the user has tapped on the name of a host in the table view, you’ll place that second view (the one that says “Connecting…”) on top of the main view. You could have used a new view controller for that, but this is just as easy.

Augment MainViewController.h to read:

#import "HostViewController.h"
#import "JoinViewController.h"

@interface MainViewController : UIViewController <HostViewControllerDelegate, JoinViewControllerDelegate>

@end

In MainViewController.m, replace joinGameAction: with:

- (IBAction)joinGameAction:(id)sender
{
	if (_buttonsEnabled)
	{
		[self performExitAnimationWithCompletionBlock:^(BOOL finished)
		{
			JoinViewController *controller = [[JoinViewController alloc] initWithNibName:@"JoinViewController" bundle:nil];
			controller.delegate = self;

			[self presentViewController:controller animated:NO completion:nil];
		}];
	}
}

And add an implementation for the delegate method:

#pragma mark - JoinViewControllerDelegate

- (void)joinViewControllerDidCancel:(JoinViewController *)controller
{
	[self dismissViewControllerAnimated:NO completion:nil];
}

Now you have a working Join Game screen. Time to finally add the matchmaking logic!

Note: When writing multiplayer games (or any networked software, really) you basically have a choice of two architectures: client-server and peer-to-peer. Even though you’re using the “peer-to-peer” API from Game Kit, this game actually uses the client-server model. The person who hosts the match is the server, and all the other players are the clients.

Client-server vs peer-to-peer

In a client-server setup, the server is in charge of everything and determines what is the “truth.” The clients send their updates to the server and the server updates all the clients, but clients don’t communicate between themselves. In a true peer-to-peer game, however, all participants are equal. All the peers do the same amount of work, but you need to take care to make sure each peer sees the same things, since there is no central authority.

Again, in this tutorial, we will be using a client-server model, where the peer who hosts the game will be the server.

Matchmaking

Now that you have the basic Host Game and Join Game screens working, you can add the matchmaking logic. When a player taps the Host Game button, her device should broadcast the availability of the Snap! service. When a player goes into the Join Game screen, his device should start looking for any servers that are making the Snap! service available.

Game Kit’s GKSession class takes care of the hard work for this, but you still need to do some work yourself. Rather than putting all this logic into the view controllers, you will be making two new classes, MatchmakingServer and MatchmakingClient. Source code can get a bit messy if the view controllers assume too many responsibilities, so that’s why you have these two new objects for setting up the connections between the devices.

Before you create these new classes, first add Game Kit to the project. In the Target Summary screen, under Linked Frameworks on Libraries, tap the + button and pick GameKit.framework from the list to add it to the project.

Linking with GameKit framework

Rather than putting an #import for the Game Kit headers in every source file that requires them, I prefer to import frameworks in the project’s Pre-Compiled Headers file. Open Snap-Prefix.pch (under Supporting Files) and add the following line inside the #ifdef __OBJC__ section:

	#import <GameKit/GameKit.h>

Now the Game Kit headers will be immediately available to all source files.

There’s one more thing you need to do, and that’s adding a special symbol in your Info.plist file to indicate that this app needs peer-to-peer functionality, because not all devices (notably first generation iPhone and iPod Touch) support peer-to-peer.

Open Snap-Info.plist and add a new row under the “Required device capabilities” entry. Give it the value “peer-peer”:

Adding peer-peer to required device capabilities

The MatchmakingServer

Add a new Objective-C class to the project, subclass of NSObject, and name it MatchmakingServer. I suggest putting it in a new group named “Networking.” Replace the contents of MatchmakingServer.h with the following:

@interface MatchmakingServer : NSObject <GKSessionDelegate>

@property (nonatomic, assign) int maxClients;
@property (nonatomic, strong, readonly) NSArray *connectedClients;
@property (nonatomic, strong, readonly) GKSession *session;

- (void)startAcceptingConnectionsForSessionID:(NSString *)sessionID;

@end

The server has a list of connected clients and a variable that limits how many clients may connect at a time. Snap! has a maximum of four players, so you don’t want to allow more than three clients to connect (the server itself counts as one of the players).

The server also has a GKSession object that will take care of the network communication between the devices, and it conforms to the GKSessionDelegate protocol because that’s how GKSession lets it know about important events.

Currently, MatchmakingServer has only one method, to start broadcasting the service and accepting connections from clients. Soon, you’ll be adding a lot more to it.

Replace MatchmakingServer.m with the following:

#import "MatchmakingServer.h"

@implementation MatchmakingServer
{
	NSMutableArray *_connectedClients;
}

@synthesize maxClients = _maxClients;
@synthesize session = _session;

- (void)startAcceptingConnectionsForSessionID:(NSString *)sessionID
{
	_connectedClients = [NSMutableArray arrayWithCapacity:self.maxClients];

	_session = [[GKSession alloc] initWithSessionID:sessionID displayName:nil sessionMode:GKSessionModeServer];
	_session.delegate = self;
	_session.available = YES;
}

- (NSArray *)connectedClients
{
	return _connectedClients;
}

#pragma mark - GKSessionDelegate

- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state
{
	#ifdef DEBUG
	NSLog(@"MatchmakingServer: peer %@ changed state %d", peerID, state);
	#endif
}

- (void)session:(GKSession *)session didReceiveConnectionRequestFromPeer:(NSString *)peerID
{
	#ifdef DEBUG
	NSLog(@"MatchmakingServer: connection request from peer %@", peerID);
	#endif
}

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

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

@end

This is mostly boilerplate stuff. The GKSessionDelegate methods don’t do anything yet, except log the results to the Xcode Debug Pane. The interesting stuff happens in startAcceptingConnectionsForSessionID:

	_session = [[GKSession alloc] initWithSessionID:sessionID displayName:nil sessionMode:GKSessionModeServer];
	_session.delegate = self;
	_session.available = YES;

Here is where you create the GKSession object and tell it to operate in server mode. This means it will only broadcast the availability of the service – named by the sessionID parameter – but it won’t look for any other devices that may be broadcasting the same service. Then you tell the session that MatchmakingServer is its delegate, and set the “available” property to YES, which starts the broadcasting. And that’s all you need to do to get a Game Kit session going.

Now you’ll put the MatchmakingServer into the Host Game screen. Add an import in HostViewController.h:

#import "MatchmakingServer.h"

Add the MatchmakingServer object as an instance variable to the Host View Controller in HostViewController.m:

@implementation HostViewController
{
	MatchmakingServer *_matchmakingServer;
}

Also add the following method to HostViewController.m:

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

	if (_matchmakingServer == nil)
	{
		_matchmakingServer = [[MatchmakingServer alloc] init];
		_matchmakingServer.maxClients = 3;
		[_matchmakingServer startAcceptingConnectionsForSessionID:SESSION_ID];

		self.nameTextField.placeholder = _matchmakingServer.session.displayName;
		[self.tableView reloadData];
	}
}

This creates the MatchmakingServer object as soon as the Host Game screen appears, and tells it to start accepting connections. It also places the name of the device (from session.displayName) as placeholder text into the “Your Name:” text field. If a player doesn’t enter her own name, she’s identified in the game using that placeholder text.

This new code won’t work until you define the SESSION_ID symbol somewhere. It doesn’t really matter what this ID is, as long as both the server and client agree on the same value. Behind the scenes, Game Kit will garble this into a unique Bonjour identifier. Because both MatchmakingServer and MatchmakingClient will need to use this symbol, you’ll simply add it to the prefix file. Open Snap-Prefix.pch and paste the following at the bottom:

// The name of the GameKit session.
#define SESSION_ID @"Snap!"

Run the app and tap the Host Game button. If you’re running on the Simulator, you may see something like this:

Host Game on Simulator

The displayName property from GKSession contains a string such as “com.hollance.Snap355561232…”, and so on. If you run the app on your device, it will say “Joe’s iPhone” or whatever you called your device when you first set it up.

You have an up-and-running Game Kit server that’s broadcasting the “Snap!” service, but no clients yet to connect to it. Now you’ll change that by building the MatchmakingClient class.

The MatchingmakingClient

Add a new Objective-C class to the project that extends NSObject, name it MatchmakingClient, and put it in the Networking group. Replace the contents of MatchmakingClient.h with:

@interface MatchmakingClient : NSObject <GKSessionDelegate>

@property (nonatomic, strong, readonly) NSArray *availableServers;
@property (nonatomic, strong, readonly) GKSession *session;

- (void)startSearchingForServersWithSessionID:(NSString *)sessionID;

@end

This somewhat mirrors what the MatchmakingServer did, but instead of connected clients, it has a list of available servers. Replace the contents of MatchmakingClient.m with:

#import "MatchmakingClient.h"

@implementation MatchmakingClient
{
	NSMutableArray *_availableServers;
}

@synthesize session = _session;

- (void)startSearchingForServersWithSessionID:(NSString *)sessionID
{
	_availableServers = [NSMutableArray arrayWithCapacity:10];

	_session = [[GKSession alloc] initWithSessionID:sessionID displayName:nil sessionMode:GKSessionModeClient];
	_session.delegate = self;
	_session.available = YES;
}

- (NSArray *)availableServers
{
	return _availableServers;
}

#pragma mark - GKSessionDelegate

- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state
{
	#ifdef DEBUG
	NSLog(@"MatchmakingClient: peer %@ changed state %d", peerID, state);
	#endif
}

- (void)session:(GKSession *)session didReceiveConnectionRequestFromPeer:(NSString *)peerID
{
	#ifdef DEBUG
	NSLog(@"MatchmakingClient: connection request from peer %@", peerID);
	#endif
}

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

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

@end

Again, this is a very barebones version of this class, and you’ll fill it out as you go along. Note that you now create the GKSession object in mode GKSessionModeClient, so that it will start looking for available servers (but not broadcast a service of its own).

Now integrate this new class into the JoinViewController, so you can start connecting servers with clients. First, add an import to JoinViewController.h:

#import "MatchmakingClient.h"

Then add an instance variable in JoinViewController.m:

@implementation JoinViewController
{
	MatchmakingClient *_matchmakingClient;
}

And get the whole thing started in viewDidAppear:

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

	if (_matchmakingClient == nil)
	{
		_matchmakingClient = [[MatchmakingClient alloc] init];
		[_matchmakingClient startSearchingForServersWithSessionID:SESSION_ID];

		self.nameTextField.placeholder = _matchmakingClient.session.displayName;
		[self.tableView reloadData];
	}
}

At this point, you can start testing. Make sure you either have two (or more!) devices that all have Bluetooth enabled, or put your device on the local Wi-Fi network and run the app on both the Simulator and your device. On one of the devices, tap Host Game, and on the other tap Join Game.

The app doesn’t show anything happening on the screen, but you should see a fair amount of debug output from Game Kit itself in Xcode’s Debug Pane.

The server will say things like:

Snap[3810:707] BTM: attaching to BTServer
Snap[3810:707] BTM: posting notification BluetoothAvailabilityChangedNotification
Snap[3810:707] BTM: received BT_LOCAL_DEVICE_CONNECTABILITY_CHANGED event
Snap[3810:707] BTM: posting notification BluetoothConnectabilityChangedNotification

These are messages from Game Kit itself. The client may also show messages from Game Kit, but it should say:

Snap[94530:1bb03] MatchmakingClient: peer 663723729 changed state 0

This message comes from the GKSessionDelegate method session:peer:didChangeState: in your MatchmakingClient class. It tells us that the peer with ID “663723729” has become available. In other words, the client has detected the presence of the server.

Note: The peer ID is an internal number used by Game Kit to identify the different devices that are partaking in the session. Every time you run the app, your peers will get different IDs. You’ll be using these peer IDs quite a bit later on.

If you have two or more devices, you can have more than one function as a server. For the purposes of this tutorial, a client can connect to only one of them, but it should detect them both. Try it out!

Where to Go From Here?

Here is a sample project with all of the code from the tutorial so far.

Congratulations – you now have a good-looking and smoothly animated main menu for the app, and the basic implementation of the host and join game screens. In addition, you have implemented broadcasting and detecting servers with GameKit and Bonjour!

This is great, but obviously you want to show the detected servers onscreen so the user can select which server to join. That is the topic of Part 2 of this epic series!

If you have any questions or comments about this part of the series, please join the forum discussion below!


This is a post by iOS Tutorial Team member Matthijs Hollemans, an experienced iOS developer and designer. You can find him on and Twitter.

Contributors

Over 300 content creators. Join our team.