How To Integrate Cocos2D and UIKit
Cocos2D is a great framework, but sometimes it’s handy to implement some of your game with UIKit. For example, it’s often useful to design your main menu, settings pages, and the like with UIKit and just use Cocos2D for your main game logic. You also might find it handy to use UIKit controls on top […] By Ray Wenderlich.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How To Integrate Cocos2D and UIKit
35 mins
Cocos2D is a great framework, but sometimes it’s handy to implement some of your game with UIKit.
For example, it’s often useful to design your main menu, settings pages, and the like with UIKit and just use Cocos2D for your main game logic.
You also might find it handy to use UIKit controls on top of a Cocos2D scene – such as text fields, buttons, tab bars, or ad views.
In this tutorial, you’ll learn how to do exactly like this! We’ll take a simple Cocos2D app and modify it to use UIKit for the main menu and an about page, and overlay some UIKit controls on top.
This tutorial assumes you have some basic familiarity with Cocos2D and UIKit development. If you are new to Cocos2D or UIKit, check out some of the other tutorials on this site first.
Getting Started
Start by downloading the starter project we’ll be using for this tutorial.
Go ahead and run the project and you should see the following:
This project is a simple Cocos2D app that displays a set of calendars made by my lovely wife Vicki.
If you’re curious how to make this app, we went through this step by step in the How To Mask a Sprite with Cocos2D 1.0 tutorial. However, I made a few slight modifications since that tutorial:
- Added some new wallpapers Vicki made
- Added some new art you’ll need for this tutorial
- Enabled retina display support
- Made the wallpapers advance in order rather than randomly
- Modified the scene transition
So take a look through the code, get your bearings, and make sure you understand how everything is working. Because we’re about to UIKit-it-up!
Adding a MainWindow.xib
By default, the Cocos2D template doesn’t use a MainWindow.xib. Instead, in Supporting Files\main.m, it passes the name of the AppDelegate class to UIApplicationMain:
int retVal = UIApplicationMain(argc, argv, nil, @"AppDelegate");
This tells UIKit to create an instance of the AppDelegate class on startup. The AppDelegate then creates the main window at the start of applicationDidFinishLaunching:
window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
This is a fine way of doing things, but if we continued down this path, if we wanted to start with a different view controller for the main menu on startup, we’d have to create it programatically.
Personally, I prefer using Interface Builder whenever possible since it’s faster and less lines of code means less maintenance, so let’s modify the project to start by loading a XIB first – called MainWindow.xib.
Start by opening Supporting Files\main.m, and replace the line that calls UIApplicationMain with the following:
int retVal = UIApplicationMain(argc, argv, nil, nil);
When you pass nil in as the last parameter, it instructs UIKit to load whatever XIB is specified in Info.plist under the NSMainNibFile key. So let’s add that next!
Open your Info.plist, right click in the empty area, and select Add Row from the popuo menu. You can select “Main nib file base name” from the dropdown (or type in the raw key NSMainNibFile).
Then double click in the Value column for this new key, and enter in MainWindow. Your screen should look like the following:
Next step is to create MainWindow.xib! Control-click on your MaskedCal group and select New File. Select iOS\User Interface\Empty, and click Next. Select iPhone for the Device Family, click Next, name the file MainWindow.xib, and click Save.
Click on the new MainWindow.xib file, and you’ll see it load into Interface Builder – mostly blank!
Let’s set this up. First, we need to set the File’s Owner to UIApplication. This is because we’ve told UIApplication to XIB for us on startup, and it passes us a reference to itself.
So click on File’s Owner, and in the Identity Inspector in the sidebar, set the Class to UIApplication.
Next, we need to configure the XIB to create an instance of our Application Delegate on startup. So in the Object library, drag an Object over to the Objects panel:
Then, select the new Object, go to the Identity Inspector, and set the Class to AppDelegate.
One last thing – we have to tell the UIApplication that this new AppDelegate class is its delegate! Do this by control-clicking File’s Owner, and control-dragging the dot next to the delegate onto the App Delegate Object:
w00t! We’ve just set up the following start-up sequence:
- In main, UIApplicationMain runs, with the last parameter set to nil.
- UIKit then loads the XIB we specified in Info.plist – MainWindow.xib.
- When the XIB is loaded, it creates an instance of each object we set up in the XIB. Right now, we have the AppDelegate class in there, so it creates a new AppDelegate class.
- Loading the XIB also wires the AppDelegate instance as the delegate of UIApplication.
- So now UIApplication will call methods such as applicationDidFinishLaunching on the AppDelegate class!
OK, now let’s finish setting up this XIB. Let’s create the main window in the XIB rather than creating it programatically. Open up AppDelegate.h and modify the UIWindow property to mark it as an IBOutlet:
@property (nonatomic, retain) IBOutlet UIWindow *window;
Also go to AppDelegate.m and comment out the line that was creating it programatically:
//window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
Go back to MainWindow.xib and drag a Window from the Object library into the Objects panel. Then control-drag on the App Delegate object, and drag a line from the window outlet to the new Window object:
Compile and run your project, and everything should work just as before, except now you’re using a MainWindow.xib, which will make our next step a bit easier.
Adding a Main Menu View Controller
Instead of starting out with our Cocos2D scene right away, let’s modify this project to start with a different View Controller instead – one we’ll set up as a main menu.
Let’s create the new view controller first. Control-click on the MaskedCal group and select New File. Select iOS\Cocoa Touch\UIViewController subclass, and click Next. Enter UIViewController for Subclass of, make sure Taregeted for iPad is NOT selected, With XIB for user interface IS selected, and click Next. Name the new class MainMenuViewController.m, and click Save.
Click MainMenuViewController.xib, and click on the View under Objects. In the Attributes Inspector, set the Orientation to Landscape.
Next drag an Image View into the view, and make it fill the entire area. Set the Image to Title_bg.png.
Next drag a Round Rect Button into the view. Select the Size Inspector and set X=173, Y=143, Width=135, and Height=62:
Select the Attributes Inspector, set the Type to Custom, and for the Default state set the Image to button_view_uns.png. Then change the State Config to Highlighted, and set the Image to button_view_sel.png.
Repeat this for a second button with the following info:
- X=173, Y=213, Width=135, Height=62
- Custom type
- Default state image: button_about_uns.png
- Highlighted state image: button_about_sel.png
One slight code mod we need to make. Open up MainMenuViewController.m and make the following changes:
// Replace this method
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return UIInterfaceOrientationIsLandscape(interfaceOrientation);
}
// Add new method
- (void) viewWillAppear:(BOOL)animated
{
[self.navigationController setNavigationBarHidden:YES animated:animated];
[super viewWillAppear:animated];
}
This will make sure our view controller only rotates to landscape orientation, and that it hides the navigation bar when it starts up.
This is good for now, we’ll finish hooking up the buttons later. Let’s just get this to show up on startup now.
Select MainWindow.xib, and drag a Navigation Controller into the Objects panel. Control-click on the Window, and drag a line from the rootViewControlelr to the new Navigation Controller:
This is setting the first view controller that shows up to be this navigation controller. We’re using a navigation controller to make managing the view controller stack a bit easier.
Next, click the arrow next to the Navigation Controller so it faces down and select the View Controller inside. In the Attributes Inspector, set the NIB Name to MainMenuViewController, and in the Identity Inspector set the Class to MainMenuViewController.
This makes the first item in the Navigation Controller the new Main Menu View Controller we just made!
Almost done – we just need to comment out some old code the Cocos2D template set up for us.
Open up AppDelegate.m and comment out the following sections:
/*
#import "HelloWorldLayer.h"
#import "RootViewController.h"
*/
/*
// Init the View Controller
viewController = [[RootViewController alloc] initWithNibName:nil bundle:nil];
viewController.wantsFullScreenLayout = YES;
//
// Create the EAGLView manually
// 1. Create a RGB565 format. Alternative: RGBA8
// 2. depth format of 0 bit. Use 16 or 24 bit for 3d effects, like CCPageTurnTransition
//
//
EAGLView *glView = [EAGLView viewWithFrame:[window bounds]
pixelFormat:kEAGLColorFormatRGB565 // kEAGLColorFormatRGBA8
depthFormat:0 // GL_DEPTH_COMPONENT16_OES
];
// attach the openglView to the director
[director setOpenGLView:glView];
*/
/*
// make the OpenGLView a child of the view controller
[viewController setView:glView];
// make the View Controller a child of the main window
[window addSubview: viewController.view];
*/
/*
[[CCDirector sharedDirector] runWithScene: [HelloWorldLayer sceneWithLastCalendar:0]];
*/
OK that’s it for now! Compile and run, and you’ll see the new main menu view controller on startup:
Next, let’s get that View button working!
Connecting the Cocos2D View
The first thing we need to do is to make a few modifications to RootViewController, to re-add the code to set up Cocos2D that we removed from the AppDelegate.
Open up RootViewController.m and make the following modifications:
// Add to top of file
#import "HelloWorldLayer.h"
// Add these new methods
- (void)setupCocos2D {
EAGLView *glView = [EAGLView viewWithFrame:self.view.bounds
pixelFormat:kEAGLColorFormatRGB565 // kEAGLColorFormatRGBA8
depthFormat:0 // GL_DEPTH_COMPONENT16_OES
];
glView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self.view insertSubview:glView atIndex:0];
[[CCDirector sharedDirector] setOpenGLView:glView];
CCScene *scene = [HelloWorldLayer sceneWithCalendar:1];
[[CCDirector sharedDirector] runWithScene:scene];
}
- (void) viewWillAppear:(BOOL)animated
{
[self.navigationController setNavigationBarHidden:YES animated:NO];
[super viewWillAppear:animated];
}
- (void)viewDidLoad {
[super viewDidLoad];
[self setupCocos2D];
}
// Add to end of viewDidUnload
[[CCDirector sharedDirector] end];
The code inside setupCocos2D is the code we removed from the AppDelegate. It creates an EAGLView, and adds it as a subview of the view controller’s view (at the bottom of the stack). It tells Cocos2D to use the new view, and tells it to run the HelloWorldLayer scene.
Note that since we’re calling this in viewDidLoad, we need to clean things up in viewDidUnload by calling [[CCDirector sharedDirector] end]. Remember views can get unloaded in low memory conditions when they are not visible.
The Cocos2D didn’t create a XIB for the RootViewController, but it is going to be convenient to have one, so let’s go ahead and get that set up too. Control-click on the MaskedCal group, click New File, select iOS\User Interface\Empty, and click Next. Select iPhone for Device Family, click Next, name the new file RootViewController.xib, and click Save.
This will be pretty simple to set up for now:
- Select File’s Owner, and in the Identity Inspector set the class to RootViewController.
- Then drag a View from the Object Library into the Objects list.
- In the Attributes Inspector, set the Orientation to Landscape.
- Finally, control-click on File’s Owner, and drag a line from the view outlet to the View you just added.
OK – our RootViewController is fully set up, now we just need to make tapping the button display it!
Open MainMenuViewController.xib, and bring up the Assistant Editor. Make sure it’s set to Automatic, and select the View so MainMenuViewController.h shows up in the Assistant Editor.
Then, control-drag from the View button down below the @interface. Set the Connection to Action, set the name to viewTapped, and click Connect:
Next we need to make an instance variable and property for the RootViewController. Open up MainMenuViewController.h and modify the file to look like the following:
#import <UIKit/UIKit.h>
#import "RootViewController.h"
@interface MainMenuViewController : UIViewController {
RootViewController *_rootViewController;
}
@property (retain) RootViewController *rootViewController;
- (IBAction)viewTapped:(id)sender;
@end
Next open MainMenuViewController.m and make the following changes:
// Add to top of file
@synthesize rootViewController = _rootViewController;
// Add new methods
- (void)viewWallpapers:(id)arg {
if (_rootViewController == nil) {
self.rootViewController = [[[RootViewController alloc] initWithNibName:nil bundle:nil] autorelease];
}
[self.navigationController pushViewController:_rootViewController animated:YES];
}
- (IBAction)viewTapped:(id)sender {
[self viewWallpapers:nil];
}
- (void)dealloc
{
[_rootViewController release];
_rootViewController = nil;
[super dealloc];
}
Pretty simple – to display the RootViewController with the Cocos2D subview, we just need to create the view controller and push it onto the navigation controller stack!
Note that when we call initWithNibName, we can pass in nil and it will look for a nib with the same name as the view controller (i.e. RootViewController.xib).
Compile and run, and now you can tap the View button to load the Cocos2D scene!
Overlaying UIKit Views
So far so good, but there’s no way to get back to the main menu!
Let’s overlay a UIKit button on top of the screen that you can tap to go back. You can use this same technique for any other sorts of controls you might need – from UITextFields to UISliders or more.
Open up RootViewController.xib, and drag to Round Rect buttons into your view. Set them up with the following settings:
- Button 1: X=0, Y=256, Width=64, Height=64, Custom, Image=Home.png
- Button 2: X=396, Y=256, Width=64, Height=64, Custom, Image=Message.png
Also, click the main view and set the background color to black so the buttons show up more clearly in Interface Builder. At this point your screen shoud look like this:
Next, control-drag from the Home button down inside your assistant editor’s RootViewController.h, right before the @end. Change the Connection to Action, the name to homeTapped, and click Connect.
Then switch to RootViewController.m and add the following inside the homeTapped method:
[self.navigationController popViewControllerAnimated:YES];
Compile and run, and you should be able to tap the home button to go back to the main menu!
Adding Gesture Recognizers
It is often useful to use gesture recognizers in your Cocos2D games, as they can often make things much easier than trying to detect the gestures yourself.
Let’s add the following gesture recognizers to this scene:
- A tap gesture recognizer, to go to the next page (like it does now).
- A swipe left gesture recognizer, to go to the next page.
- A swipe right gesture recognizer, to go to the previous page.
- A double tap gesture recognizer, which we’ll make hide the UIButton overlays later (but will just print a messge for now).
First, let’s remove the old touch handling code. Go to HelloWorldLayer.m, and delete the ccTouchBegan method, the registerWithTouchDispatcher method, and the line that says isTouchEnabled to YES.
Next switch to HelloWorldLayer.h and add some member variables and properties for the gesture recognizers:
// Add inside @interface
UITapGestureRecognizer * _tapRecognizer;
UITapGestureRecognizer * _doubleTapRecognizer;
UISwipeGestureRecognizer * _swipeLeftRecognizer;
UISwipeGestureRecognizer * _swipeRightRecognizer;
// Add after @interface
@property (retain) UITapGestureRecognizer * tapRecognizer;
@property (retain) UITapGestureRecognizer * doubleTapRecognizer;
@property (retain) UISwipeGestureRecognizer * swipeLeftRecognizer;
@property (retain) UISwipeGestureRecognizer * swipeRightRecognizer;
Next switch to HelloWorldLayer.m and make the following changes:
// Add after @implementation
@synthesize tapRecognizer = _tapRecognizer;
@synthesize doubleTapRecognizer = _doubleTapRecognizer;
@synthesize swipeLeftRecognizer = _swipeLeftRecognizer;
@synthesize swipeRightRecognizer = _swipeRightRecognizer;
// Then add these new methods
- (void)onEnter {
self.doubleTapRecognizer = [[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDoubleTap:)] autorelease];
_doubleTapRecognizer.numberOfTapsRequired = 2;
[[[CCDirector sharedDirector] openGLView] addGestureRecognizer:_doubleTapRecognizer];
self.tapRecognizer = [[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)] autorelease];
[_tapRecognizer requireGestureRecognizerToFail:_doubleTapRecognizer];
[[[CCDirector sharedDirector] openGLView] addGestureRecognizer:_tapRecognizer];
self.swipeLeftRecognizer = [[[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(handleLeftSwipe:)] autorelease];
_swipeLeftRecognizer.direction = UISwipeGestureRecognizerDirectionLeft;
[[[CCDirector sharedDirector] openGLView] addGestureRecognizer:_swipeLeftRecognizer];
self.swipeRightRecognizer = [[[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(handleRightSwipe:)] autorelease];
_swipeRightRecognizer.direction = UISwipeGestureRecognizerDirectionRight;
[[[CCDirector sharedDirector] openGLView] addGestureRecognizer:_swipeRightRecognizer];
}
- (void)onExit {
[[[CCDirector sharedDirector] openGLView] removeGestureRecognizer:_tapRecognizer];
[[[CCDirector sharedDirector] openGLView] removeGestureRecognizer:_doubleTapRecognizer];
[[[CCDirector sharedDirector] openGLView] removeGestureRecognizer:_swipeLeftRecognizer];
[[[CCDirector sharedDirector] openGLView] removeGestureRecognizer:_swipeRightRecognizer];
}
// Add to dealloc
[_tapRecognizer release];
_tapRecognizer = nil;
[_doubleTapRecognizer release];
_doubleTapRecognizer = nil;
[_swipeLeftRecognizer release];
_swipeLeftRecognizer = nil;
[_swipeRightRecognizer release];
_swipeRightRecognizer = nil;
onEnter is called when a scene is first displayed, and onExit is called when it disappears. In these we setup and tear down the gesture recognizers, respectively.
Notice that to create a gesture recognizer you use the following pattern:
- Create the gesture recognizer, specifying the target to receive the callback (this scene) and the callback method to be called (which we’ll write next).
- Specify any parameters you want on the gesture recognizers, like numberOfTapsRequired, or swipe direction.
- Add the gesture recognizer to the view – in this case the openGL view managed by CCDirector.
There’s also one interesting bit you might notice – when we set up the single tap gesture recognizer, we say that for it to count, the double tap gesture recognizer has to fail. If you didn’t have this, the single tap gesture recognizer would be called during the first tap of a double tap, which we don’t want for this app.
Next add the gesture recognizer callbacks, right before onEnter:
- (void)handleTap:(UITapGestureRecognizer *)tapRecognizer {
CCLOG(@"Tap!");
CCScene *scene = [HelloWorldLayer sceneWithCalendar:calendarNum+1];
[[CCDirector sharedDirector] replaceScene:[CCTransitionSlideInR transitionWithDuration:0.25 scene:scene]];
}
- (void)handleDoubleTap:(UITapGestureRecognizer *)doubletapRecognizer {
CCLOG(@"Double Tap!");
}
- (void)handleLeftSwipe:(UISwipeGestureRecognizer *)swipeRecognizer {
CCLOG(@"Swipe Left!");
CCScene *scene = [HelloWorldLayer sceneWithCalendar:calendarNum+1];
[[CCDirector sharedDirector] replaceScene:[CCTransitionSlideInR transitionWithDuration:0.25 scene:scene]];
}
- (void)handleRightSwipe:(UISwipeGestureRecognizer *)swipeRecognizer {
CCLOG(@"Swipe Right!");
CCScene *scene = [HelloWorldLayer sceneWithCalendar:calendarNum-1];
[[CCDirector sharedDirector] replaceScene:[CCTransitionSlideInL transitionWithDuration:0.25 scene:scene]];
}
As you can see, these are pretty simple methods, and just transition between the scenes. Compile and run, and you should now be able to advance with taps or swipes!
Also, try double tapping and look in the log to verify that it detected the double tap. We’ll implement this next, to make it hide the HUD for a full-screen view of the wallpaper!
Communicating with the RootViewController
You often need to have a way to communicate to the RootViewController from Cocos2D. For example, you might want to display a UI element upon some event. In our case, we want to show or hide the HUD panel on a double tap.
There are multiple ways to do this to avoid dependencies, but the easiest way is to just pass a parameter to the RootViewController to the layer directly.
Open up HelloWorldLayer.h and make the following changes:
// Add to top of file
#import "RootViewController.h"
// Add inside @interface
RootViewController * _rootViewController;
// Replace sceneWithCalendar and initWithCalendar with these
+ (CCScene *) sceneWithCalendar:(int)lastCalendar rootViewController:(RootViewController *)rootViewController;
- (id)initWithCalendar:(int)lastCalendar rootViewController:(RootViewController *)rootViewController;
Then switch to HelloWorldLayer.m and make the following changes:
// Replace sceneWithCalendar method with the following
+(CCScene *) sceneWithCalendar:(int)calendar rootViewController:(RootViewController *)rootViewController
{
CCScene *scene = [CCScene node];
HelloWorldLayer *layer = [[[HelloWorldLayer alloc]
initWithCalendar:calendar rootViewController:rootViewController] autorelease]; // new
[scene addChild: layer];
return scene;
}
// Replace beginning of initWithCalendar with the following
-(id) initWithCalendar:(int)calendar rootViewController:(RootViewController *)rootViewController
{
if( (self=[super init])) {
_rootViewController = rootViewController;
// Rest of method...
// Replace touch handler methods with the following
- (void)handleTap:(UITapGestureRecognizer *)tapRecognizer {
CCLOG(@"Tap!");
CCScene *scene = [HelloWorldLayer sceneWithCalendar:calendarNum+1 rootViewController:_rootViewController];
[[CCDirector sharedDirector] replaceScene:[CCTransitionSlideInR transitionWithDuration:0.25 scene:scene]];
}
- (void)handleDoubleTap:(UITapGestureRecognizer *)doubletapRecognizer {
CCLOG(@"Double Tap!");
[_rootViewController toggleUI];
}
- (void)handleLeftSwipe:(UISwipeGestureRecognizer *)swipeRecognizer {
CCLOG(@"Swipe Left!");
CCScene *scene = [HelloWorldLayer sceneWithCalendar:calendarNum+1 rootViewController:_rootViewController];
[[CCDirector sharedDirector] replaceScene:[CCTransitionSlideInR transitionWithDuration:0.25 scene:scene]];
}
- (void)handleRightSwipe:(UISwipeGestureRecognizer *)swipeRecognizer {
CCLOG(@"Swipe Right!");
CCScene *scene = [HelloWorldLayer sceneWithCalendar:calendarNum-1 rootViewController:_rootViewController];
[[CCDirector sharedDirector] replaceScene:[CCTransitionSlideInL transitionWithDuration:0.25 scene:scene]];
}
So basicaly we just pass a reference to the RootViewController through – pretty simple stuff. And on double tap, we call a new method called toggleUI that we’ll write now!
First, we need to add some outlets for the buttons so we can show/hide them. Open up RootViewController.xib, and control-drag from the home button into the assistant editor’s RootViewController.h, right after the @interface. Set the connection to Outlet, set the name to homeButton, and click connect.
Repeat for the mail button, naming the outlet mailButton.
Next, open up RootViewController.h and make the following changes:
// Add inside @interface
BOOL _showingUI;
CGPoint _mailCenterOrig;
CGPoint _homeCenterOrig;
// Add after @interface
- (void)toggleUI;
Then open RootViewController.m and make the following changes:
// Add inside viewDidLoad, BEFORE call to setupCocos2D
_showingUI = YES;
_mailCenterOrig = _mailButton.center;
_homeCenterOrig = _homeButton.center;
// Modify call to sceneWithCalendar inside setupCocos2D with the following
CCScene *scene = [HelloWorldLayer sceneWithCalendar:1 rootViewController:self];
// Add after viewDidLoad
- (void)toggleUI {
_showingUI = !_showingUI;
[UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseInOut animations:^(void) {
if (_showingUI) {
_mailButton.center = _mailCenterOrig;
_homeButton.center = _homeCenterOrig;
} else {
_mailButton.center = CGPointMake(_mailCenterOrig.x+_mailButton.bounds.size.width, _mailCenterOrig.y+_mailButton.bounds.size.height);
_homeButton.center = CGPointMake(_homeCenterOrig.x-_homeButton.bounds.size.width, _homeCenterOrig.y+_homeButton.bounds.size.height);
}
} completion:^(BOOL finished) {
}];
}
This uses UIView animation to make a nice animation to make the buttons slide offscreen/onscreen to show and hide them. If you’re new to UIView animation, you might want to check out the How To Use UIView Animation Tutorial.
That’s it – compile and run, and you should be able to double tap the screen to reveal the wallpapers in their full glory!
Communicating with the Layer
Just like you sometimes need to communicate to the RootViewController from the layer, you also often need to communicate with the layer from the RootViewController.
We’re going to implement tapping the mail button to send an email with the current wallpaper. The RootViewController will contain the code to present the mail composer, but it will need to ask the layer for an UIImage of the currently displayed wallpaper.
Again, there are multiple ways to handle this, but we’re going to take the simple way – just ask the CCDirector for the current running scene. We only have one scene/layer, so we can just get the first layer in the scene and cast it to a HelloWorldLayer, then call whatever methods we want on it.
Let’s first prepare the HelloWorldLayer to have a method to give us the current wallpaper image. Open HelloWorldLayer.h and make the following changes:
// Add inside @interface
UIImage * _curImage;
// Add after @interface
@property (retain) UIImage * curImage;
Then switch to HelloWorldLayer.m and make the following changes:
// Add after @implementation
@synthesize curImage = _curImage;
// Inside maskedSpriteWithBuffer, comment out these lines:
//[textureSprite setBlendFunc:(ccBlendFunc){GL_DST_ALPHA, GL_ZERO}];
//[maskSprite visit];
// Add inside maskedSpriteWithBuffer, right before return retval
self.curImage = [rt getUIImageFromBuffer];
// Add to dealloc
[_curImage release];
_curImage = nil;
Here we use a handy method on CCRenderTexture to convert the current buffer to a UIImage. We also remove the call to visiting the mask sprite, since we don’t really need to demonstrate this anymore, so we’ll just look at the full-screen wallpapers.
Next, open RootViewController.xib, and control-drag from the mail button below the @interface in the assistant editor’s RootViewController.h. Set the connection type to Action, the name to mailTapped, and click Connect.
To display the mail composer, we need to add the MessageUI framework to the project. So select the MaskedCal project in the groups & files tree, select the MaskedCal target, select the Build Phases Tab, expand the Link binary with Libraries section, and click the + button.
Select MessageUI.framework from the dropdown and click Add. Then open RootViewController.h and make the following changes:
// Add to top of file
#import <MessageUI/MessageUI.h>
#import <MessageUI/MFMailComposeViewController.h>
// Mark class as implementing MFMailComposeViewControllerDelegate
@interface RootViewController : UIViewController <MFMailComposeViewControllerDelegate> {
And finally, switch to RootViewController.m and replace the mailTapped method with the following (plus two new methods):
- (void)mailData:(NSData *)data {
if (![MFMailComposeViewController canSendMail]) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Oops!", @"")
message:NSLocalizedString(@"Your device cannot send mail.", @"")
delegate:self
cancelButtonTitle:NSLocalizedString(@"OK", @"")
otherButtonTitles:nil];
[alert show];
[alert release];
return;
}
// Start up mail picker
MFMailComposeViewController *picker = [[MFMailComposeViewController alloc] init];
UINavigationBar *bar = picker.navigationBar;
picker.mailComposeDelegate = self;
[picker setSubject:@"Check out this cute wallpaper!"];
[picker addAttachmentData:data mimeType:@"image/jpg" fileName:@"wallpaper.jpg"];
// Set up the recipients.
NSArray *toRecipients = [NSArray arrayWithObjects:nil];
[picker setToRecipients:toRecipients];
// Fill out the email body text.
NSString *actualBody = @"Check out this cute wallpaper! You can download the fullscreen version for free from: http://www.vickiwenderlich.com";
[picker setMessageBody:actualBody isHTML:NO];
// Present the mail composition interface.
[self presentModalViewController:picker animated:YES];
bar.topItem.title = @"Email Wallpaper";
[picker release]; // Can safely release the controller now.
}
- (IBAction)mailTapped:(id)sender {
CCScene * scene = [[CCDirector sharedDirector] runningScene];
HelloWorldLayer *layer = [scene.children objectAtIndex:0];
UIImage *curImage = layer.curImage;
NSData *data = UIImageJPEGRepresentation(curImage, 0.8);
[self mailData:data];
}
#pragma mark MFMailComposeViewControllerDelegate
- (void)mailComposeController:(MFMailComposeViewController *)controller
didFinishWithResult:(MFMailComposeResult)result
error:(NSError *)error {
[self dismissModalViewControllerAnimated:YES];
}
Notice how the mailTapped method gets the running scene, pulls out the first child, and knows that it is a HelloWorldLayer. Then it can get the curImage from the layer.
Compile and run, and now you can send a wallpaper you like to a friend! :]
Gratuitous Progress HUDs
If you’ve been running this on your actual device, you might notice that there are certain parts with an annoying delay – setting up Cocos2D for the first time, and compressing image to JPG format for email sending.
When you have long-running operations that the user hast to wait for like this, it’s nice to display an indiator that something is going on to make the app feel more responsive.
One simple way to do this is through a utility class I like called MBProgressHUD. Go ahead and download it, then drag MBProgressHUD.h and MBProgressHUD.m from the unzipped folder into your project. Make sure “Copy items into destination group’s folder (if needed)” is selected, and click Finish.
Then go to MainViewController.m and make the following changes:
// Add to top of file
#import "MBProgressHUD.h"
// Replace viewTapped with the following
- (IBAction)viewTapped:(id)sender {
if (_rootViewController == nil) {
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
hud.labelText = @"Loading...";
[self performSelector:@selector(viewWallpapers:) withObject:nil afterDelay:0.1];
} else {
[self viewWallpapers:nil];
}
}
// Add to the beginning of viewWillAppear
[MBProgressHUD hideHUDForView:self.view animated:NO];
This just displays a HUD as the viewWallpapers method we wrote earlier (which starts up the RootViewController) gets called. We schedule the viewWallpapers method to run after a slight delay so that the HUD has time to appear before we start running the code to initialize the RootViewController.
Next move to RootViewController.m and make the following changes:
// Add to top of file
#import "MBProgressHUD.h"
// Replace mailTapped method with the following
- (IBAction)mailTapped:(id)sender {
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
hud.labelText = @"Preparing wallpaper...";
CCScene * scene = [[CCDirector sharedDirector] runningScene];
HelloWorldLayer *layer = [scene.children objectAtIndex:0];
UIImage *curImage = layer.curImage;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
NSData *data = UIImageJPEGRepresentation(curImage, 0.8);
dispatch_async(dispatch_get_main_queue(), ^(void) {
[MBProgressHUD hideHUDForView:self.view animated:YES];
[self mailData:data];
});
});
}
This runs the call to UIImageJPEGRepresentation on a background thread, with the HUD animating as this occurs.
Compile and run, and the app should be more responsive!
Gratuitous About Screen
The app is almost complete, except we need an About page! Feel free to skip this if you’re already pretty familiar with how to add new view controllers, but if you need more practice follow along!
Control-click on the MaskedCal group, and select New File. Choose iOS\Cocoa Touch\UIViewController subclass, and click Next. Enter UIViewController for Subclass of, make sure With XIB for user interface is checked, click Next, name the class AboutViewController.m, and click Save.
Click on AboutViewController.xib and make the following changes:
- Select the View, and in the Attributes Inspector set the Orientation to Landscape.
- Drag a UIImageView to fill the View and set the image to About_page_bg.jpg
- Drag a UILabel into the View, set X=49, Y=132, Width=382, Height=104, Text Color to White, and Font to Heiti SC Light 16.0, Txt Alignment to Center, # of Lines to 0 (means unlimited).
- Change the text to: “Vicki works with her husband to create iPhone and iPad apps. Find more of her wallpapers on her website, vickiwenderlich.com, as well as free art for app developers and tutorials for artists.”
At this point your screen should look like this:
Then switch to AboutViewController.m and replace the shouldAutorotateToInterfaceOrienation with the following plus a new method:
- (void) viewWillAppear:(BOOL)animated
{
[self.navigationController setNavigationBarHidden:NO animated:animated];
[super viewWillAppear:animated];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return UIInterfaceOrientationIsLandscape(interfaceOrientation);
}
Nice, now we just need to set up our main menu to display this. Open up MainMenuViewController.h and make the following changes:
// Add to top of file
#import "AboutViewController.h"
// Add inside @interface
AboutViewController *_aboutViewController;
// Add after @interface
@property (retain) AboutViewController *aboutViewController;
Then select MainMenuViewController.xib and control-drag from the About utton into the assistant editor’s MainMenuViewController.h. Set the Connection type to action, the name to aboutTapped, and click Connect.
Then switch to MainMenuViewController.m and make the following changes:
// Add after @implementation
@synthesize aboutViewController = _aboutViewController;
// Add inside dealloc
[_aboutViewController release];
_aboutViewController = nil;
// Add inside aboutTapped
if (_aboutViewController == nil) {
self.aboutViewController = [[[AboutViewController alloc] initWithNibName:nil bundle:nil] autorelease];
}
[self.navigationController pushViewController:_aboutViewController animated:YES];
Almost done – one more minor tweak for style. Open up MainWindow.xib and select the Navitation Controller\Navigation Bar. Select the Tint color, and set R=168, G=215, B=224 for a nice teal color to match the rest of the app:
Also select the Navigation Controller \ Main Menu View Controller\Navigation Item and set the Title to Home.
That’s it! Compile and run, and now the app is complete – with an About page and all!
Where To Go From Here?
Here is the complete example project with all of the code from the above tutorial series.
At this point, you should be familiar with the basics of how to integrate Cocos2D with UIKit. If you have any questions, comments, or suggestions, please join the forum discussion below!