May 06

In this tutorial I’m going to show you how to build a Universal iPhone / iPad app that displays a self-contained Web site in a UIWebView component. Why would you want to do that? Well because you could then design and build entire apps with nothing more than HTML5, CSS and JavaScript.

There are plenty of books on building apps that work in the UIWebView – which uses the same technology behind the iPhone / iPad Safari Web browser. I would suggest searching for online tutorials and books with the keyword “WebKit.” You could narrow down your search by also searching on “Safari”.

A good place to start is here:

http://developer.apple.com/safari/


Bundled Web Site


This tutorial is not going to cover WebKit. It’s just going to show you how to build a wrapper in Objective-C for the browser component. The design of the Web app is up to you. Though first, I would like to offer a few tips on writing your HTML.

Bundled HTML Issues

When bundling HTML to be run locally in a UIWebView, you need to consider a few issues:

  • There is no server. So server-side scripts like PHP will not work. Though if your user is connected to the Internet you could always provide links to a Web site running PHP. But the focus here is on self-contained apps.
  • Because there is no server there is nothing to interpret links like “test/” to mean “test/index.html.” So when linking to sub-folders you need to append “/index.htm,” “index.html,” etc.
  • When copying HTML folders into your Xcode project be sure to check “Create Folder References for any added folders.” Otherwise Xcode will bundle your files into groups and relative HTML links to sub-folders won’t work.
  • JavaScript calls can not take too long or your script may be stopped.
  • When using the new HTML5 video tag, the iPad needs the controls attribute to be set to true (or anything which is interpreted as true).
  • The iPad will use the first frame of a video as a poster frame. The iPhone will not and you should provide it.
  • The iPhone and iPad support SVG (Scalable Vector Graphics) – a technology that I started writing about a few years ago – but didn’t have a lot of traction. Nice to see it making a comeback. :) You should look for updated tutorials.
  • If you plan to also write a wrapper for Android and use your HTML there too, keep it simple. Their HTML5 implementation needs work. They currently do not support SVG and the video tag didn’t work for me in their latest emulator.

Now on to the tutorial:

Create a Project in Xcode

1. Select: File / New Project …
2. Select: [ iPhone OS / Applications ] / Window-based Application
3. Select: Product: Universal
4. Click: Choose
5. Name your project (I named this one TestWebApp01)
6. Click: Save

Create a simple Web Site

It is important that you create your initial Web site outside of Xcode.

1. Create a folder somewhere called “www”
2. Create an index.html file with this content.

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" 
	"http://www.w3.org/TR/html4/strict.dtd">
<html>
  <head>
	<!-- Change this if you want to allow scaling -->
    <meta name="viewport" 
		content="width=default-width; user-scalable=no" />
 
    <meta http-equiv="Content-type" 
		content="text/html; charset=utf-8">
 
    <title>Web App Demo</title>
 
  </head>
  <body>
	<h1>Test</h1>
	<p>This is a test.</p>
  </body>
</html>

3. Back in Xcode expand the Shared folder
4. Right-click and select: Add / Existing Files …
5. Navigate to the “www” folder and click Add.
6. Check: Copy items into destination group’s folder (if needed)
7. IMPORTANT: Select: Create Folder References for any added folders

Create the App

1. Modify iPad / AppDelegate_Pad.h so that it looks like this:

#import 
 
@interface AppDelegate_Pad : NSObject  {
    UIWindow *window;
    UIWebView *webView;	// Add UIWebView and outlet below
}
 
@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet UIWebView *webView;
@end

2. Modify iPhone / AppDelegate_Phone.h so that it looks like this:

#import 
 
@interface AppDelegate_Phone : NSObject  {
    UIWindow *window;
    UIWebView *webView;	// Add UIWebView and outlet below
}
 
@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet UIWebView *webView;
 
@end

3. Make the following changes to AppDelegate_Pad.m

#import "AppDelegate_Pad.h"
 
@implementation AppDelegate_Pad
 
@synthesize window;
 
@synthesize webView;		// Synthesize webView
 
#define WWW_ROOT	@"www"	// Define the root of the Web folder
 
- (BOOL)application:(UIApplication *)application
	didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    
 
	// Add code to open bundled Web site
 
	NSString *path = [[NSBundle mainBundle]
					  pathForResource:@"index"
					  ofType:@"html"
					  inDirectory:WWW_ROOT ]; 
 
	NSURL *url = [NSURL fileURLWithPath:path];
 
	NSURLRequest *request = [NSURLRequest requestWithURL:url];
 
	[webView loadRequest:request];
 
    // Override point for customization after application launch
 
    [window makeKeyAndVisible];
 
	return YES;
}

4. Make the following changed to AppDelegate_Phone.m

#import "AppDelegate_Phone.h"
 
@implementation AppDelegate_Phone
 
@synthesize window;
 
@synthesize webView;		// Synthesize webView
 
#define WWW_ROOT	@"www"	// Define the root of the Web folder
 
- (BOOL)application:(UIApplication *)application
	didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    
 
	// Add code to open bundled Web site
 
	NSString *path = [[NSBundle mainBundle]
					  pathForResource:@"index"
					  ofType:@"html"
					  inDirectory:WWW_ROOT ];
 
	NSURL *url = [NSURL fileURLWithPath:path];
 
	NSURLRequest *request = [NSURLRequest requestWithURL:url];
 
	[webView loadRequest:request];
 
    // Override point for customization after application launch
 
    [window makeKeyAndVisible];
 
	return YES;
}

Interface Builder

1. In Xcode expand the iPad folder
2. Double click: MainWindow_Pad.xib to launch Interface Builder
3. Double click Window to bring up the iPad layout
4. Drag a Tool Bar to the bottom of the window
5. Click on the Tool Bar’s Item button and press Command-D three times so you end up with four buttons.
6. Change the first two buttons to be labeled “Back” and “Forward”
7. Click on the third button and press Command-1 to bring up the Attributes Inspector panel
8. Change the Identifier of the button to “Refresh”
9. Do the same for the fourth button, make its Identifier “Stop”
10. Drag a Flexible Space Bar Button Item between the two middle buttons
11. Drag a UIWebView from the Library above the toolbar and let if fill the rest of the window

Wire it Up

12. Click on App Delegate Pad
13. Press: Command-2 to bring up the Connections Inspector panel
14. Under Outlets drag from the circle next to webView to the UIWebView in the Window
15. Click on the UIWebView
16. Press: Command-2 to bring up the Connections Inspector panel
17. Under Received Actions drag from the circles next to the actions to the matching buttons (goBack to Back button, etc).
18. Select: File / Save
19. Repeat all these steps for the iPhone folder

Test It

If you set things for Simulator 3.2 the iPad simulator should come up.
If you set things for Simulator 3.1 the iPhone simulator should come up.


Source Code

If you have git installed, you can obtain a copy of the project in this tutorial by executing the following command:

git clone git@github.com:mitchallen/TestWebApp01.git

Tags: , , , , ,

Apr 30

Since the UITableView sample has been generating a lot of comments (and some frustration), I’ve posted an update compiled with the latest 3.x SDK:

http://mitchallen.com/iphone/archives/184

The update includes a link to a new sample that you can clone from github.

Tags: ,

Oct 30

I’ve added code to my Cocos2D test harness to show you how to handle coordinating touch events among multiple sprites. The class I created to manage the sprites was based on code that I found in this article:

http://juanmunozar.blogspot.com/2009/02/cocos2d-iphone-dynamically-touch.html

If you have git installed you can retrieve the latest copy of the project via the instructions at the end of this article.

Getting Started

In order to complete this tutorial you first you need to go through the steps of my posts:

If you are using git you may want to save and tag the original project and try expanding on it here in a new branch.

Graphic Files

For this tutorial you will need to copy some additional graphic files from the cocos2d-iphone sample folders to your projects Resources folder:

  • Resources/Images/blocks.png

Classes / TestScene.m

First, you need to add a string identifying the new test layer to the transitions array in TestScene.m:

static NSString *transitions[] = {
	@"TestLayer01",
	@"TestLayer02",
	@"TestLayer03",
};

Classes / mcaTouchSprite.h

Here I introduce a new class based on code in the referenced article. As you can see I’ve trimmed it down quite a lot to demo just the basics.

Something that you may not have seen before is the “SEL” property type. Think of it as a function pointer. It lets you dynamically assign a function to call when a sprite is touched. You will see how this is done later in the code.

//
//  mcaTouchSprite.h
//
//  Created by Mitchell Allen on 10/6/09.
//  Copyright 2009 __MyCompanyName__. All rights reserved.
//
// References:
// http://juanmunozar.blogspot.com/2009/02/cocos2d-iphone-dynamically-touch.html
//
// See these links for additional info and credit:
// http://mitchallen.com/iphone/archives/424
//
 
#import <UIKIt/UIKit.h>
#import "cocos2d.h"
#import "chipmunk.h"
 
 
@interface mcaTouchSprite : Sprite {
 
	@private
 
	SEL		__onTouchBegan;
}
 
#pragma mark properties
 
@property (nonatomic, assign) SEL onTouchBegan;
 
#pragma mark class methods
 
+(NSMutableArray *)allMySprites;
+(void) track: (mcaTouchSprite *)aSprite;
+(void) untrack: (mcaTouchSprite *)aSprite;
 
#pragma mark instance methods
 
- (CGRect) rect;
 
@end

Classes / mcaTouchSprite.m

//
//  mcaTouchSprite.m
//
//  Created by Mitchell Allen on 10/6/09.
//  Copyright 2009 __MyCompanyName__. All rights reserved.
//
// See these links for additional info and credit:
// http://mitchallen.com/iphone/archives/424
//
 
#import "mcaTouchSprite.h"
 
@implementation mcaTouchSprite
 
@synthesize onTouchBegan = __onTouchBegan;
 
static NSMutableArray *allMySprites = nil;
 
+(NSMutableArray *)allMySprites {
    @synchronized(allMySprites) {
        if (allMySprites == nil)
            allMySprites = [[NSMutableArray alloc] init];
        return allMySprites;
    }
	return nil;
}
 
- (CGRect) rect {
	float w = self.contentSize.width;
	float h = self.contentSize.height;
	float x = self.position.x - w/2;
	float y = self.position.y - h/2;
	return CGRectMake(x,y,w,h);
}
 
+(void)track: (mcaTouchSprite *)aSprite {
    @synchronized(allMySprites) {
		NSUInteger i, count = [allMySprites count];
		for(i = 0; i < count ; i++){
			mcaTouchSprite * obj 
				= (mcaTouchSprite *)[allMySprites objectAtIndex:i];
			if(obj == aSprite){
				return;
			}
		}
 
        [[mcaTouchSprite allMySprites] addObject:aSprite];
    }
}
 
+(void)untrack: (mcaTouchSprite *)aSprite {
    @synchronized(allMySprites) {
        [[mcaTouchSprite allMySprites] removeObject:aSprite];
    }
}
 
-(void)dealloc {
    [mcaTouchSprite untrack:self];
    [super dealloc];
}
 
@end

Classes / mcaBoxSprite.h

This is my first pass at creating a sprite that has some built in functionality. In this case a turn method that lets you rotate the sprite on the screen through code. To demo that touch events are actually being processed, the sprite will turn n degrees every time it is touched. The method does that by adding the amount of degrees desired to self.rotation which it inherits from the base class.

//
//  mcaCarSprite.h
//  Potholes_001_001
//
//  Created by Mitchell Allen on 10/7/09.
//  Copyright 2009 __MyCompanyName__. All rights reserved.
//
// See these links for additional info and credit:
// http://mitchallen.com/iphone/archives/424
//
 
#import "mcaTouchSprite.h"
 
@interface mcaBoxSprite : mcaTouchSprite { }
 
-(void) turn:(float) n;
 
@end

Classes / mcaBoxSprite.m

//
//  mcaBoxSprite.m
//
//  Created by Mitchell Allen on 10/7/09.
//  Copyright 2009 __MyCompanyName__. All rights reserved.
//
// See these links for additional info and credit:
// http://mitchallen.com/iphone/archives/424
//
 
#import "mcaBoxSprite.h"
 
#import "math.h"
 
@implementation mcaBoxSprite
 
-(void) turn:(float) n { 
 
	self.rotation += n;
}
 
@end

Classes / MyTests.h

Now you need to include references to the new header files and add the interface declaration for our new test layer to MyTests.h:

//
//  MyTests.h
//  TestGame
//
//  Created by Mitchell Allen on 9/20/09.
//  Copyright 2009 __MyCompanyName__. All rights reserved.
//
 
// See these links for additional info and credit:
// http://mitchallen.com/iphone/archives/371
// http://mitchallen.com/iphone/archives/304
// http://mitchallen.com/iphone/archives/424
 
#import "TestScene.h"
 
#import "mcaTouchSprite.h"
#import "mcaBoxSprite.h"
 
///////////////////////////////////////////////////////
//
// Adding Test Layers to Test Game
//
// When a  dervied TestLayer is added, you must add it's name to 
// the NSString transitions array in TestScene.m for it to appear 
// in the test game.
 
#pragma mark -
 
@interface TestLayer01 : TestLayer
{}
@end
 
#pragma mark -
 
@interface TestLayer02 : TestLayer
{}
@end
 
#pragma mark -
 
#define LAYER_03_BOXES	4
 
@interface TestLayer03 : TestLayer
{
	mcaBoxSprite *box[ LAYER_03_BOXES ];
}
@end

Classes / MyTests.m

Finally, append the code for TestLayer03 to MyTests.m.

Where as the previous layers did their setup in the onEnter method, here we do setup in the init method.

You will notice that we loop through a collection of box sprites, assigning the function pointer (onTouchBegan) to the method touchBox and flag the box sprite for touch tracking. We also need to make sure isTouchEnabled is set to YES. When touchBox is called, the turn method on the touched box is called – rotating the sprite.

Most of the main processing happens in the ccTouchesBegan method. This method attempts to figure out which if any box sprite was called, then calls whatever method onTouchBegan is pointing to, passing in the object.

#pragma mark -
 
@implementation TestLayer03
 
-(id) init {
 
	self = [super init];
 
	if(self != nil) {
 
		//////////////////////////////
		// Add Boxes
 
		for( int i = 0; i < LAYER_03_BOXES; i++ ) {
 
			// Copy blocks.png from Cocos2D resources.
 
			box[i] = 
			  [[mcaBoxSprite spriteWithFile:@"blocks.png"] retain];
 
			box[i].onTouchBegan = @selector(touchBox:);
 
			[mcaBoxSprite track:box[i]];
 
			float bx = 50;
			float by = 50 * (1 + i);
 
			box[i].position = cpv( bx, by );
 
			box[i].scaleY = 0.5f;
			box[i].scaleX = 0.5f;
 
			box[i].rotation = 0.0f;
 
			[self addChild: box[i]];
		}
 
		//////////////////////////
		// Enable interaction.
 
		isTouchEnabled = YES;	// To enable touching
	}
 
	return self;
}
 
-(void) onEnter
{	
	[super onEnter];
}
 
-(void) onExit
{		
	[super onExit];
 
	for( int i = 0; i < LAYER_03_BOXES; i++ ) {
 
		[mcaBoxSprite untrack:box[i]];
	}
 
}
 
-(void) touchBox: (id) theBox {
 
	[theBox turn: 10];
 
}
 
- (BOOL)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
 
    UITouch *touch = [touches anyObject];
 
	CGPoint location = [[Director sharedDirector] 
						convertCoordinate: 
						[touch locationInView:touch.view]];
 
    NSArray * mySprites = [mcaTouchSprite allMySprites];
    NSUInteger i, count = [mySprites count];
 
	// NSLog( @"COUNT = %d", count );
 
    for (i = 0; i < count; i++) {
 
        mcaTouchSprite * obj 
			= (mcaTouchSprite *)[mySprites objectAtIndex:i];
 
        if (CGRectContainsPoint([obj rect], location) ) {
 
            // code here is only executed if obj has been touched
			// NSLog( @"Sprite touched." );
 
			if( [self respondsToSelector: obj.onTouchBegan ] ) {
 
				[self 
					performSelector: obj.onTouchBegan 
					withObject: obj];
			}
 
			return kEventHandled;
        }
    }
 
	return false;
}
 
- (BOOL)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
 
	return false;
}
 
-(void) dealloc
{
	for( int i = 0; i < LAYER_03_BOXES; i++ ) {
 
		[box[i] release];
	}
 
	[super dealloc];
}
 
-(NSString *) title
{
	// Override the base classes title 
	// that appears on the screen.
 
	return @"Test Layer 03 - Touch";
}
@end

References

http://juanmunozar.blogspot.com/2009/02/cocos2d-iphone-dynamically-touch.html

Git Clone

If you have git installed, you can clone a copy of this project with the following command:

git clone git://github.com/mitchallen/TestGame.git

I may add more code to the repository. So to get back to the original state of this tutorial, you will need to execute the following at the command line:

git checkout v300

See previous articles for previous states.

Tags:

Sep 20

In this tutorial I’m going to expand on my previous example TestGame featured in my post Getting Started with the Cocos2d Game Engine.  This follow up borrows heavily from the cocos2d sample file SpritesTest.  If you haven’t seen the SpritesTest demo, see my article on Running the Cocos2d examples. The objective is to reverse engineer that demo and use it to show you how to build your own test app.

Why Build a Test App ?

My background is in testing software libraries.  I have a lot of experience breaking down functionality into manageable test harnesses.  The test harnesses usually let you select one isolated feature and test it.  It’s a great way for learning how a library works.  It’s also a great way to try something.  By building a test harness you don’t have to build a whole project from scratch just to test one feature.  This is especially handy when you run into features that you can only test on the actual device.

Getting Started

In order to complete this tutorial you first you need to go through the steps of my post Getting Started with the Cocos2d Game Engine.

If you are using git you may want to save and tag the original project and try expanding on it here in a new branch.

Graphic Files

For this tutorial you will need to copy some additional graphic files from the cocos2d-iphone sample folders to your projects Resources folder:

  • Resources/Images/grossini images/grossini.png (male cartoon character)
  • Resources/Images/grossini images/grossini_sister1.png (female cartoon character)
  • Resources/Images/menu related images/b1.png (back arrow)
  • Resources/Images/menu related images/b2.png (back arrow highlighted)
  • Resources/Images/menu related images/f1.png (next arrow)
  • Resources/Images/menu related images/f2.png (next arrow highlighted)
  • Resources/Images/menu related images/r1.png (reset button)
  • Resources/Images/menu related images/r2.png (reset button highlighted)

Classes / TestGameAppDelegate.m

For this tutorial I’ve borrowed from the SpritesTest code and added some new functionality to TestGameAppDelegate.m:

//
//  TestGameAppDelegate.m
//  TestGame
//
//  Created by Mitchell Allen on 9/16/09.
//  Copyright __MyCompanyName__ 2009. All rights reserved.
//
// Some code and images borrowed from cocos2d SpritesTest sample code.
//
// See these links for additional info and credit:
// http://mitchallen.com/iphone/archives/371
// http://mitchallen.com/iphone/archives/304
 
#import "TestGameAppDelegate.h"
 
@implementation TestGameAppDelegate
 
- (void)applicationDidFinishLaunching:(UIApplication *)application { 
 
	UIWindow *window = [[UIWindow alloc] initWithFrame: [[UIScreen mainScreen] bounds]];
 
	[window setUserInteractionEnabled:YES];
	[window setMultipleTouchEnabled:YES];
 
	// DEPRECATED: [[Director sharedDirector] setLandscape: YES];
	[[Director sharedDirector] setDeviceOrientation: CCDeviceOrientationLandscapeLeft ];
	[[Director sharedDirector] attachInWindow: window];
 
	// display FPS (useful when debugging)
	[[Director sharedDirector] setDisplayFPS:YES];
 
	// frames per second
	[[Director sharedDirector] setAnimationInterval:1.0/60];
 
	[window makeKeyAndVisible];
 
	// Default texture format for PNG/BMP/TIFF/JPEG/GIF images
	// It can be RGBA8888, RGBA4444, RGB5_A1, RGB565
	// You can change anytime.
	[Texture2D setDefaultAlphaPixelFormat:kTexture2DPixelFormat_RGBA8888];
 
	TestScene *game = [TestScene node];
	[[Director sharedDirector] runWithScene: game];
}
 
// getting a call, pause the game
-(void) applicationWillResignActive:(UIApplication *)application
{
	[[Director sharedDirector] pause];
}
 
// call got rejected
-(void) applicationDidBecomeActive:(UIApplication *)application
{
	[[Director sharedDirector] resume];
}
 
// purge memroy
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
	[[TextureMgr sharedTextureMgr] removeAllTextures];
}
 
- (void) dealloc
{
		[super dealloc];
}
 
// next delta time will be zero
-(void) applicationSignificantTimeChange:(UIApplication *)application
{
	[[Director sharedDirector] setNextDeltaTimeZero:YES];
}
 
@end

Test Scene

There are no changes to TestScene from the previous project.  As a reminder:

  • A simple TestScene class is defined based on cocos2d Scene class
  • The TestScene loads a TestLayer based on a cocos2d Layer class

Test Layer

There have been several changes made to TestLayer.

  • Now TestLayer serves as a base class for additional test layers that you can customize based on what you want to test
  • It manages the loading of two sprites from the cocos2d samples: grossini and tamara
  • It loads a graphical menu for navigating between layers using arrows and restarting a layer
  • It has several functions to work with the menu to switch between layers
  • It has a centerSprites method for centering the two test sprites
  • It has a title method used by the UI that can be overridden by derived classes

Classes/TestScene.h

Both TestScene and TestLayer are defined in Classes/TestScene.*

//
//  TestScene.h
//  TestGame
//
//  Created by Mitchell Allen on 9/16/09.
//  Copyright 2009 __MyCompanyName__. All rights reserved.
//
// See these links for additional info and credit:
// http://mitchallen.com/iphone/archives/371
// http://mitchallen.com/iphone/archives/304
 
#import
#import "cocos2d.h"
#import "chipmunk.h"
 
#pragma mark -
 
@interface TestScene : Scene { }
@end
 
#pragma mark -
 
@interface TestLayer : Layer {
	Sprite * grossini;
	Sprite *tamara;
}
-(void) centerSprites;
-(NSString*) title;
 
-(void) setLayer: (Class) classLayer;
 
-(void) backCallback:(id) sender;
-(void) nextCallback:(id) sender;
-(void) restartCallback:(id) sender;
@end

Classes/TestScene.m

One important thing to note here is the global static transitions string array.  When you add a new class based on TestLayer (see MyTests.* below), you need to add a string containing the name to the array. The navigation code uses NSClassFromString to convert the currently indexed string to a layer object to load.

//
//  TestScene.m
//  TestGame
//
//  Created by Mitchell Allen on 9/16/09.
//  Copyright 2009 __MyCompanyName__. All rights reserved.
//
// See these links for additional info and credit:
// http://mitchallen.com/iphone/archives/371
// http://mitchallen.com/iphone/archives/304
//
 
#import "TestScene.h"
 
#pragma mark -
 
@implementation TestScene
 
-(id) init {
	self = [super init];
	if( self != nil ) {
		[self addChild: [TestLayer node] z:1];
	}
	return self;
}
 
@end
 
#pragma mark -
 
static int sceneIdx=-1;
 
///////////////////////////////////////////////////
// In order for a test layer to appear you must
// add a string for its class name to this array.
// Test layers are defined in MyTests.*
 
static NSString *transitions[] = {
	@"TestLayer01",
	@"TestLayer02" };
 
Class nextAction()
{
	// Retrieve the name of the next layer class
	// and return a class based on that string.
 
	sceneIdx++;
	sceneIdx = sceneIdx % ( sizeof(transitions) / sizeof(transitions[0]) );
	NSString *r = transitions[sceneIdx];
	Class c = NSClassFromString(r);
	return c;
}
 
Class backAction()
{
	// Retrieve the name of the previous layer class
	// and return a class based on that string.
 
	sceneIdx--;
	if( sceneIdx < 0 )
		sceneIdx = sizeof(transitions) / sizeof(transitions[0]) -1;
	NSString *r = transitions[sceneIdx];
	Class c = NSClassFromString(r);
	return c;
}
 
Class restartAction()
{
	// Retrieve the name of the current layer class
	// and return a class based on that string.
 
	if( sceneIdx < 0 || sceneIdx >= sizeof(transitions) ) return nil;
 
	NSString *r = transitions[sceneIdx];
	Class c = NSClassFromString(r);
	return c;
}
 
#pragma mark -
 
@implementation TestLayer
 
-(id) init {
	self = [super init];
	if(self != nil) {		
 
		// Initialize and add test images so they can be used by derived test layers.
 
		// Note that because we 'retain' the images,
		// we will need to release them in dealloc.
 
		grossini = [[Sprite spriteWithFile:@"grossini.png"] retain];
		tamara = [[Sprite spriteWithFile:@"grossinis_sister1.png"] retain];
 
		[self addChild: grossini z:1];
		[self addChild: tamara z:2];
 
		[self centerSprites];
 
		CGSize s = [[Director sharedDirector] winSize];
 
		// Display the title property for the current layer.
 
		Label* label = [Label labelWithString:[self title] fontName:@"Arial" fontSize:32];
		[self addChild: label];
		[label setPosition: ccp(s.width/2, s.height-50)];
 
		// Create a graphics-based layer navigation menu to be used by all derived test layers.
 
		MenuItemImage *item1 = [MenuItemImage itemFromNormalImage:@"b1.png" selectedImage:@"b2.png" target:self selector:@selector(backCallback:)];
		MenuItemImage *item2 = [MenuItemImage itemFromNormalImage:@"r1.png" selectedImage:@"r2.png" target:self selector:@selector(restartCallback:)];
		MenuItemImage *item3 = [MenuItemImage itemFromNormalImage:@"f1.png" selectedImage:@"f2.png" target:self selector:@selector(nextCallback:)];
 
		Menu *menu = [Menu menuWithItems:item1, item2, item3, nil];
		menu.position = CGPointZero;
		item1.position = ccp(480/2-100,30);
		item2.position = ccp(480/2, 30);
		item3.position = ccp(480/2+100,30);
		[self addChild: menu z:1];
	}
	return self;
}
 
-(void) centerSprites
{
	// Take our two test sprites and center them on the screen.
 
	CGSize s = [[Director sharedDirector] winSize];
 
	[grossini setPosition: ccp(s.width/3, s.height/2)];
	[tamara setPosition: ccp(2*s.width/3, s.height/2)];
}
 
-(NSString*) title
{
	// Create a default title for the opening layer.
 
	// Test layers should override this call.
 
	return @"Press Arrows to Continue";
}
 
-(void) dealloc
{
	// Since our test sprites were retained,
	// we must now release them.
 
	[grossini release];
	[tamara release];
	[super dealloc];
}
 
-(void) setLayer: (Class) classLayer {
 
	// Replace the current layer with one based another class object.
 
	if( classLayer == nil ) return;
 
	Scene *s = [Scene node];
	[s addChild: [classLayer node]];
	[[Director sharedDirector] replaceScene: s];
}
 
-(void) restartCallback: (id) sender
{
	// When the user clicks the restart button
	// (the green circle in the current UI)
	// reload the current layer,
	// triggering a restart of the action
 
	[self setLayer: restartAction()];
}
 
-(void) nextCallback: (id) sender
{
	// When the user clicks the next arrow
	// (the green right arrow in the current UI)
	// load the next layer in the list.
 
	[self setLayer: nextAction()];
}
 
-(void) backCallback: (id) sender
{
	// When the user clicks the back arrow
	// (the green left arrow in the current UI)
	// load the previous layer in the list.
 
	[self setLayer: backAction()];
}
 
@end

Classes/MyTests.h

When adding a new derived TestLayer you generally don’t have to do a lot in the header file (unless you want to add more functions and things like retained sprites to track).  For simple layers you just need to define interfaces based on TestLayer.

//
//  MyTests.h
//  TestGame
//
//  Created by Mitchell Allen on 9/20/09.
//  Copyright 2009 __MyCompanyName__. All rights reserved.
//
 
// See these links for additional info and credit:
// http://mitchallen.com/iphone/archives/371
// http://mitchallen.com/iphone/archives/304
 
#import "TestScene.h"
 
///////////////////////////////////////////////////////
//
// Adding Test Layers to Test Game
//
// When a  dervied TestLayer is added, you must add it's name to
// the NSString transitions array in TestScene.m for it to appear
// in the test game.
 
@interface TestLayer01 : TestLayer
{}
@end
 
@interface TestLayer02 : TestLayer
{}
@end

Classes/MyTests.m

For simple layers you just need to define two things:

  • An onEnter method for setting up the actions in your layer
  • A title method that overrides the base TestLayer title method and returns a pointer to an NSString used by the UI

You also need to remember this important step:

  • When you add a new layer you need to add the name as a string to the transistions array in TestScene.m

For this tutorial I just borrowed the SpritesTest demo code from the SpriteMove and SpriteRotate classes.

You can use this file to add as many test layers as you would like.

//
//  MyTests.m
//  TestGame
//
//  Created by Mitchell Allen on 9/20/09.
//  Copyright 2009 __MyCompanyName__. All rights reserved.
//
// See these links for additional info and credit:
// http://mitchallen.com/iphone/archives/371
// http://mitchallen.com/iphone/archives/304
 
#import "MyTests.h"
 
#pragma mark -
 
@implementation TestLayer01
 
-(void) onEnter
{
	// Code borrowed from cocos2d sample:
	// SpritesTest.m - SpriteMove class
 
	[super onEnter];
 
	CGSize s = [[Director sharedDirector] winSize];
 
	id actionTo = [MoveTo actionWithDuration: 2 position:ccp(s.width-40, s.height-40)];
 
	id actionBy = [MoveBy actionWithDuration:2  position: ccp(80,80)];
	id actionByBack = [actionBy reverse];
 
	[tamara runAction: actionTo];
	[grossini runAction: [Sequence actions:actionBy, actionByBack, nil]];
 
}
 
-(NSString *) title
{
	// Override the base classes title
	// that appears on the screen.
 
	return @"Test Layer 01 - Move";
}
@end
 
#pragma mark -
 
@implementation TestLayer02
 
-(void) onEnter
{
	// Code borrowed from cocos2d sample:
	// SpritesTest.m - SpriteRotate class
 
	[super onEnter];
 
	[self centerSprites];
 
	id actionTo = [RotateTo actionWithDuration: 2 angle:45];
	id actionBy = [RotateBy actionWithDuration:2  angle: 360];
	id actionByBack = [actionBy reverse];
 
	[tamara runAction: actionTo];
	[grossini runAction: [Sequence actions:actionBy, actionByBack, nil]];
}
 
-(NSString *) title
{
	// Override the base classes title
	// that appears on the screen.
 
	return @"Test Layer 02 - Rotate";
}
@end

Compile and Run

  • Click: Build and Go

Git Clone

If you have git installed, you can clone a copy of this project with the following command:

git clone git://github.com/mitchallen/TestGame.git

For my next tutorial I will be adding code to the repository. So to get back to the original state of this tutorial, you will need to execute the following at the command line:

git checkout v200

To get back to the state used in the previous tutorial you can use:

git checkout v100

Both v100 and v200 are tags that I’ve set on the master branch.

Tags:

Sep 17

If you scroll to the bottom of my previous post you will notice that I’ve updated it with a new section:

Git Clone

If you have git installed, you can clone a copy of this project with the following command:

git clone git://github.com/mitchallen/TestGame.git

Going forward, I will attempt to post all sample code using git. Usually this means that it will come wrapped in a full Xcode project.

I have several reasons for using git:

  • I don’t have to wrestle with creating a zip file and uploading it to some obscure server somewhere
  • You don’t have to wrestle with downloading a zip file and expanding it somewhere
  • You don’t need an account to copy my public repositories
  • All you have to do is enter one line in a terminal window
  • You instantly end up with a local copy of the repository that you can copy and branch any which way you like
  • It forces you to think about source control :)
  • If something is missing, I can just checkin new stuff and do a refresh
  • If I hack an existing project, I can just add a branch instead of forcing you and I to deal with multiple projects

So if you still haven’t installed git on your Mac yet, I suggest you go back and read my post on the subject again.  Also if you have, you may want to read it again.  I’ve added a few more links and info on filtering out certain files to keep your repository from getting cluttered.

Tags: ,