Modal view controllers in cocos2d

Today’s post is going to be a tutorial-style one. One of the (few) annoying things of cocos2d is the fact that it is very unrelated to UIKit and the Model View Controller paradigm of Apple’s views a view controllers. This is a problem when you need to show, for example, a view controller modally to send an e-mail or show Game Center leaderboards and achievements.

In this tutorial I am going to describe the technique I use to connect with “Apple’s layer” from cocos2d in an easy, modular, reusable way. You will find the project source code used on this tutorial at the end of this post.

Cocos2d Mug

So, to put an example of the problem we are facing, imagine that you want to launch the typical e-mail composer interface from Apple SDK to send an e-mail from cocos2d. We will need to “present a modal view controller”. However, you won’t be allowed to do that from any of the cocos2d classes. That’s because all cocos2d classes derive from CCNode and CCNode is not a UIViewController. Only UIViewController objects can launch other view controller modally.

The quick solution

So, one quick solution could be to query our App Delegate to tell its RootViewController to do the job. Something like this:

-(void) sendContactMail {
    UIViewController *rootViewController = (UIViewController*)[(NewSokobanAppDelegate*)[[UIApplication sharedApplication] delegate] viewController];

	MFMailComposeViewController *picker = [[MFMailComposeViewController alloc] init];
	picker.mailComposeDelegate = rootViewController;

	// Recipient.
    NSString *recipient = [NSString stringWithString:@"your_mail@gmail.com"];
	NSArray *recipientsArray = [NSArray arrayWithObject:recipient];
	[picker setToRecipients:recipientsArray];

	// Subject.
	[picker setSubject:NSLocalizedString(@"Feedback", "")];

	// Body.
	NSString *emailBody = @"";
	[picker setMessageBody:emailBody isHTML:NO];

	[rootViewController presentModalViewController:picker animated:YES];

	[picker release];
}

This code takes advantage of the fact that in recent versions of cocos2d, the project template is build upon an UIViewController object called “RootViewController”. What we basically do on this piece of code is to retrieve the RootViewController from our AppDelegate and present the mail modal view controller through it. It works but has a few problems. First, it triggers an annoying warning on Xcode. Moreover, this way we are not encapsulating the functionality of sending a mail. And last but not least, the code is ugly :p

The singleton based method

So what we are going to do is to create a singleton class that encapsulates all the necessary calls to our project’s RootViewController. Let’s go and write a bit of code.

First, start a new cocos2d project

Select cocos2d project template

Select cocos2d project template

Name it, for example, cocos2dViewController. This is the base project we are going to work on.

Now, we are going to create the singleton class. Create a new NSObject file called, for example, “RootViewControllerInterface”.

Make the needed modifications to make it singleton and add a UIViewController member variable called, for example, rootViewController. Copy-paste this code to the .h file:

#import

@interface RootViewControllerInterface : NSObject {
    UIViewController *rootViewController;
}

@property (nonatomic, retain) UIViewController *rootViewController;

#pragma mark -
#pragma mark Singleton Methods
+ (RootViewControllerInterface *) rootViewControllerInterfaceSharedInstance;

@end

And paste this code to the .m file:

#import "RootViewControllerInterface.h"

@implementation RootViewControllerInterface

@synthesize rootViewController;

#pragma mark -
#pragma mark Singleton Variables
static RootViewControllerInterface *rootViewControllerInterfaceSingletonDelegate = nil;

#pragma mark -
#pragma mark Singleton Methods
+ (RootViewControllerInterface *) rootViewControllerInterfaceSharedInstance {
	@synchronized(self) {
		if (rootViewControllerInterfaceSingletonDelegate == nil) {
			[[self alloc] init]; // assignment not done here
		}
	}
	return rootViewControllerInterfaceSingletonDelegate;
}

+ (id)allocWithZone:(NSZone *)zone {
	@synchronized(self) {
		if (rootViewControllerInterfaceSingletonDelegate == nil) {
			rootViewControllerInterfaceSingletonDelegate = [super allocWithZone:zone];
			// assignment and return on first allocation
			return rootViewControllerInterfaceSingletonDelegate;
		}
	}
	// on subsequent allocation attempts return nil
	return nil;
}

- (id)copyWithZone:(NSZone *)zone {
	return self;
}

- (id)retain {
	return self;
}

- (unsigned)retainCount {
	return UINT_MAX;  // denotes an object that cannot be released
}

- (void)release {
	//do nothing
}

- (id)autorelease {
	return self;
}

Nice piece of code, isn’t it? ;) Don’t worry about the details of this code. This is just the way a singleton class is created in objective-c. You only need to know that now our RootViewControllerInterface class is a singleton class, so we can query its shared instance from anywhere in the project without the need to allocate/release new memory in every call.

Let’s add to it some useful functionality. First, we need the basic method to show a generic view controller modally. Add this method declaration to the .h file:

-(void) presentModalViewController:(UIViewController*)controller animated:(BOOL)animated;

And the corresponding implementation to the .m file:

-(void) presentModalViewController:(UIViewController*)controller animated:(BOOL)animated {
    [rootViewController presentModalViewController:controller animated:animated];
}

As you can see, we are only forwarding the call to our member variable rootViewController, which is a UIViewController an is allowed to launch other UIViewController modally.

Before start using our new singleton class, let’s add to it another method. A higher level one, for example, the method to send an e-mail. Add this code to the RootViewControllerInterface.h file:

-(void) sendContactMail;

And the corresponding implementation to the .m file:

-(void) sendContactMail {
    MFMailComposeViewController *picker = [[MFMailComposeViewController alloc] init];
    picker.mailComposeDelegate = rootViewController;

    // Recipient.
    NSString *recipient = [NSString stringWithString:@"your_mail@gmail.com"];
    NSArray *recipientsArray = [NSArray arrayWithObject:recipient];
    [picker setToRecipients:recipientsArray];

    // Subject.
    [picker setSubject:NSLocalizedString(@"Feedback", "")];

    // Body.
    NSString *emailBody = @"";
    [picker setMessageBody:emailBody isHTML:NO];

    [rootViewController presentModalViewController:picker animated:YES];

    [picker release];

}

As you can see in line 17, we are using our member variable rootViewController to present the mail composer view controller modally.

Few things to take into account. In RootViewControllerInterface.h and RootViewController.h you will need to import two files:  <MessageUI/MessageUI.h> and <MessageUI/MFMailComposeViewController.h>. RootViewController also needs to conform to the <MFMailComposeViewControllerDelegate> protocol. Finally, we will need to add this delegate method to RootViewController.h:

-(void) mailComposeController:(MFMailComposeViewController*)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError*)error {
	[self dismissModalViewControllerAnimated:YES];
}

Ok, our RootViewControllerInterface is ready to send mails :) We are going to create a CCMenu with a button on the HelloWorldLayer to trigger the mail composer.

Replace the “init” method in HelloWorldLayer.m by the following one:

-(id) init {
	if( (self=[super init])) {

        // ask director the the window size
		CGSize size = [[CCDirector sharedDirector] winSize];

        // The button.
        CCMenuItemFont *button = [CCMenuItemFont itemFromString:@&quot;Present modal view&quot; target:self selector:@selector(menuCallback:)];
        button.position = ccp( size.width /2 , size.height/2 );

        // the menu.
        CCMenu *menu = [CCMenu menuWithItems:button, nil];
        menu.position = CGPointZero;
        [self addChild:menu];
	}
	return self;
}

This code only creates a CCMenu with a button in it. When the button is touched the method “menuCallBack:” is going to be called. Let’s implement this method. Add it in the HelloWorldLayer.m file:

-(void) menuCallback:(id)sender {
    [[RootViewControllerInterface rootViewControllerInterfaceSharedInstance] sendContactMail];
}

Now you need to add our AppDelegate viewController to the RootViewControllerInterface. At this code at the end of the method “applicationDidFinishLaunching:” in the AppDelegate class:

// Add the viewController to the RootViewControllerInterface.
    [[RootViewControllerInterface rootViewControllerInterfaceSharedInstance] setRootViewController:viewController];

And that’s it! As you can see, we have needed a little of preparation, but when our RootViewController and RootViewControllerInterface are ready, their usage is very straight forward and easily reusable.

Similarly as the method to send a contact mail, you can add other high level methods that need to present a modal view controller for sending generic mails, showing Game Center leaderboards and achievements, the build-in camera modal view controller… Or even custom view controllers you need for your projects.

Conclusion

So, this technique needs a little preparation of the RootViewControllerInterface singleton class. However, when it is ready, its usage is very straight forward and the most important thing is that it is easily reusable.

You can find the project source code used on this tutorial here.

Any suggestion is welcome. Hope you find it useful :)

14 thoughts on “Modal view controllers in cocos2d

  1. Yes, I also used to have this kind of AppDelegate :) The technique explained in this post has revealed to be quite good. I have re-used the class in several projects with minimum pain :)

  2. I am confused — In a recent Cocos2d project, I subclassed a UIViewController and loaded my RootViewController, as well as my iAd adBannerViews into it. This way I could display the iAds across all scenes.

    Would it have been possible to do this all through the RootViewController using this singleton method? I feel like my extra “wrapper” UIViewController may be a waste.

  3. it doesn’t work for me. i tried to modify it a little bit.
    i want the world with my mash started normally, but at application startup, it should push a modal view before it (something like a login window).

    it doesn’t throw an exception or an error, but it also don’t show my modal view… :/

  4. yes i did.
    in the meantime, i found another way to push my modal view.
    for all the others, here’s my snippet:
    UIViewController *viewController = [[[UIViewController alloc] init] autorelease];
    [[[CCDirector sharedDirector] openGLView] addSubview:viewController.view];
    [viewController presentModalViewController:modalViewContoller animated:YES];
    [modalViewContoller release];

  5. Nice article. One change I made I added the MFMailComposeViewControllerDelegate to the singleton and removed it from the rootviewcontroller. This makes it fully encapsulated.

    ie. in the singleton .m
    -(void) mailComposeController:(MFMailComposeViewController*)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError*)error
    {
    [rootViewController dismissModalViewControllerAnimated:YES];
    }

  6. Thanks so much Toni, after a bit of head scratching I managed to make it work with a UIView
    [self.rootViewController presentMoviePlayerViewControllerAnimated:moviePlayer];
    (in order to load MPMoviePlayerViewController, finally it is working, after initially having problems with loading the movie (as unknowen to me initially MPMoviePlayerViewController creates its own layer to load the movie into…and hiding somewhere is a feature to stop hello world from animating as it slides out of view… that or ill just have to et used to the idea of rey hairs :P (managed to at least stop the movie sliding into view)

    But then nothing can dampen my day! the movie works and in a modal view :O)
    with full accessibility to all the features that i was unable to get in CCVideoPlayer!!

    Just a couple of things (be gentle im a newbie to coding :P)
    I know i need to release the movie but am unsure how as if i release the UI view hello world will have no view to load in :P – any clues/suggestions on what to do would be wonderful!

    I was wondering also after playing the movie for cocos scene2 node to be loaded instead of hello world (the thought being to use modal view for showing cut scenes for an interactive book) and have started looking into a way of doing this but was wondering if you had any suggestions

    (I was looking at on of ray Wenderliches tutorials on using uikit and loading nibs into the root view controller, but there might be a more efficient way.

    pps sorry if none of this makes any sense (as i said im still very new to cocos, coding and xcode, but you tutorial has definitely been an amazing eyeopener and also give me the opportunity to use uikit now as well as cocos in a much more efficient way, so I am quite excited and very thankful already.

  7. Thanks for great post.

    I have used the code in my project.
    It works well, but the width of mail modal view is not fill out scree size.
    The screen size is portrait size eventhour landscape.

    I hope you help me.
    Regards,
    Yin

    • I don’t understand exactly what is your problem. Maybe you could send me an e-mail with more details of your problem and a demo project that reproduces the issue.

      Thanks.

Leave a Reply