Thursday, 24 May 2012

Sending Email Landscape style in Cocos2D

 

iPhone-Mail[Concrete/Interesting] You know when sometimes a piece of code seems really simple, but it just won’t go right? The code just won’t behave properly. Fix one problem and another one pops up...

Well, all I wanted was to use MFMailComposeViewController in my Cocos2d application. I’ve done it before so thought it would be a quick job. After many, many hours, I got it working, so to save you all the effort, here is what I did.

Landscape and MFMailComposeViewController

The problem was the game is in Landscape mode and it seems the mail view controller doesn’t like landscape. Works beautifully on the iPad, but the iPhone sees the world in Portrait.

The standard method to launch email is to call presentModelViewController. Here’s the sort of thing that works well in Portrait:

//Create the mail view controller
MFMailComposeViewController *picker = [[MFMailComposeViewController alloc] init];
picker.mailComposeDelegate = self;
[picker setSubject:@"The Title"];

// Email Body
NSMutableString *emailBody = [[[NSMutableString alloc] initWithString:@"<html><body>"] retain];
[emailBody appendString:@"<p>Some body text</p>"];
[emailBody appendString:@"</body></html>"];
[picker setMessageBody:emailBody isHTML:YES];

//An image
NSData *imageData = [NSData dataWithData:UIImagePNGRepresentation([self previewImage])];
[picker addAttachmentData:imageData mimeType:@"image/png" fileName:@"image.png"];

//Present the mail view controller
[[[CCDirector sharedDirector] view] addSubview:emailController.view];
[emailController presentModalViewController:picker animated:YES];
[picker release];
 
Ok, to get this working you need to ensure the class calling this code is marked as a MFMailComposeViewController delegate by inheriting from <MFMailComposeViewControllerDelegate>. You will also need a suitable UIViewController. This is emailController in the code above.
 
Notice also that I’m using cocos2d version 2. If you use an earlier version you will need to change the code below:
 
//Cocos2d version 2 uses view
//[[[CCDirector sharedDirector] view] addSubview:emailController.view];

//For earlier version use openGLView
[[[CCDirector sharedDirector] openGLView] addSubview:emailController.view];

Under Landscape it all goes wrong on the iPhone

But the code above doesn’t work in an application forced into Landscape orientation. What you get is part of the picker showing, clipped and rotated. Oddly enough, the onscreen keyboard is shown correctly, but this is like rubbing salt into an open wound. You will also find that touch handling is messed up and you can’t cancel the email process using the ‘Delete’ or ‘Save as Draft’. Just not working at all well.

There are a number of tips and tricks on the internet to get around this. One method is to handle the presentation of the modal view controller yourself. Tried this and it worked well, up to a point.

But everything went south after choosing an email address using the address book picker. Once again you end up with a clipped and ill orientated modal view.

Solution: Use the Root Controller

Digging deeper it looks like the mail picker and other pickers launched during the process are just not aware of the orientation of the device and application. They seem to be doing their best to ignore what is around them and are going for Portrait. Why?

Well it all comes down to the UIViewController used to present the pickers. In the world of MVC, the view controller used to present other views are responsible for describing the container’s properties, including the orientation. The problem is the UIViewController being used is not doing enough for the picker. In fact the solution is ever so simple, use the application root controller that is responsible for managing cocos2d and the views in this environment:

UIViewController *rootViewController = (UIViewController *)[[[CCDirector sharedDirector] view] nextResponder];
[rootViewController presentModalViewController:picker animated:YES];

The nextResponder to cocos2d’s main view is the root view controller. If you use this to present the modal mail picker, it all works like a charm. Only took me 8 hours to get to this solution.
 

Wrapping it all up

 
I like to wrap things up tight so I created a class called EmailScene that encapsulates all the code to present a mail picker:
 

//EmailScene.h

#import "cocos2d.h"
#import <MessageUI/MFMailComposeViewController.h>

 

@interface EmailScene : CCScene <MFMailComposeViewControllerDelegate>
{
NSString *emailTitle;
NSString *emailBody;
UIImage *emailImage;
MFMailComposeViewController *picker;
}

-(id)initWithTitle:(NSString *)title body:(NSString *)body image:(UIImage *)image;

@end


The implementation looks like this:
 
//EmailScene.m

#import "EmailScene.h"

@implementation EmailScene

- (id) init {
self = [super init];
if (self != nil) {
[self showMailPicker];
}
return self;
}

-(id)initWithTitle:(NSString *)title body:(NSString *)body image:(UIImage *)image
{
self = [super init];
if (self != nil) {
emailTitle = title;
emailBody = body;
emailImage = image;
[self showMailPicker];
}
return self;
}

-(void)showMailPicker
{
picker = [[MFMailComposeViewController alloc] init];
picker.mailComposeDelegate = self;
picker.modalPresentationStyle = UIModalPresentationFullScreen;
picker.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;

[picker setSubject:emailTitle];
[picker setMessageBody:emailBody isHTML:YES];

NSData *imageData = [NSData dataWithData:UIImagePNGRepresentation(emailImage)];
[picker addAttachmentData:imageData mimeType:@"image/png" fileName:[NSString stringWithFormat:@"%@.png", emailTitle]];

[[CCDirector sharedDirector] pause];
UIViewController *rootViewController = (UIViewController *)[[[CCDirector sharedDirector] view] nextResponder];
[rootViewController presentModalViewController:picker animated:YES];
[picker release];
}

- (void)mailComposeController:(MFMailComposeViewController*)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError*)error
{
[[CCDirector sharedDirector] resume];
[controller dismissModalViewControllerAnimated: YES];
}
@end

And to show the picker from any scene in any orientation on iPhone and iPad, simply call the appropriate initializer:
 
// Email Body
NSMutableString *emailBody = [[[NSMutableString alloc] initWithString:@"<html><body>"] retain];
[emailBody appendString:@"<p>Example Email Body</p>"];
[emailBody appendString:@"</body></html>"];

//Show the mail picker
[[EmailScene alloc] initWithTitle:@"Subject Line" body:emailBody image:takeScreenshot(rtx)];