Score system in Cocos2d project - objective-c

I have a game where the user collects different types of objects and a label that has a value of 0 at the start. Every time the user collects an object (by touching it) it should make the score = current score + 1; I have tried with the following code but it crashes when I click on the object.
This is the code for my score label which puts a 0 on the screen:
score = 0;
scoreLabel1 = [CCLabelTTF labelWithString:#"0" fontName:#"Times New Roman" fontSize:33];
scoreLabel1.position = ccp(240, 160);
[self addChild:scoreLabel1 z:1];
And this is the void function which I call every time I touch an object:
- (void) addScore
{
score = score + 1;
[scoreLabel1 setString:[NSString stringWithFormat:#"%#", score]];
}
And this is the actual part where I put the code for touching the object:
-(void) ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self ccTouchesMoved:touches withEvent:event];
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:[touch view]];
location = [[CCDirector sharedDirector] convertToGL:location];
for (Apple in self.appleArray)
{
if (CGRectContainsPoint(Apple.boundingBox, location))
{
[self addScore];
Apple.visible = NO;
}
}
Everything else works except for the score. Also is there a way to make the apple disappear instead of just making it invisible by apple.visible = false? because this way the apple is still there but not visible, I want to get rid of it.
Hope some one can help!
If you have any questions let me know.
Thanks.
This is where I draw the apples:
-(id) init
{
// always call "super" init
// Apple recommends to re-assign "self" with the "super's" return value
if( (self=[super init]) ) {
isTouchEnabled_ = YES;
self.appleArray = [CCArray arrayWithCapacity:20];
for (int i = 0; i < 5; i++) {
Apple = [CCSprite spriteWithFile:#"Apple4.png"];
[self addChild:Apple];
[appleArray addObject:Apple];
}
[Apple removeFromParentAndCleanup:true];
[self scheduleUpdate];
}
return self;
}
And this is where the screen gets updated:
-(void) update: (ccTime) dt
{
for (int i = 0; i < 5; i++) {
Apple = ((CCSprite *)[appleArray objectAtIndex:i]);
if (Apple.position.y > -250) {
Apple.position = ccp(Apple.position.x, Apple.position.y - (Apple.tag*dt));
}
}
}

a couple of things here. In setScore, your format is broken and will cause a crash (%# requires an NSObject*). Try:
[scoreLabel1 setString:[NSString stringWithFormat:#"%i", score]];
also, the syntax of your for loop is odd. Try
for (Apple *anyApple in self.appleArray)
{
if (CGRectContainsPoint(anyApple.boundingBox, location))
{
if (anyApple.visible) {
[self addScore];
anyApple.visible = NO;
}
}
}

score = score + 1;
[scoreLabel1 setString:[NSString stringWithFormat:#"%#", score]];
Please read this: String Format Specifiers
%# - Objective-C object, printed as the string returned by descriptionWithLocale: if available, or description otherwise. Also works with CFTypeRef objects, returning the result of the CFCopyDescription function.
If your score is an object, you can't "increment" its value that way. If it's an int or a float, you are using wrong format specifier.

Related

Communication Between Unrelated Objects

Background
I'm creating a rhythm app for MIDI drumming using SpriteKit on Mac OS. Note objects are the ones in green and blue below. The notes move from right to left on the screen. As the notes get closer to the "play bar" in red, I want to create an outline of the notes next to the play bar to indicate to the user that the notes are getting close to the play bar and that it's time to play soon.
The Problem
I need each Note object to notify another class called Sequencer when they get a certain distance from the play head so that it can start the note outline animation for the closest note. The notes know how far they are from the play head, so that's not a problem. Would delegation be a candidate for this situation? I have tried to understand the purpose of delegation, but I can never figure out how to implement it into my specific game-related situation.
EDIT: Code added
GameScene.m
#implementation GameScene
- (void)didMoveToView:(SKView *)view {
...
[self initSequence];
[self initNoteProcessor];
[self initPlayBar];
...
}
...
//keeps track of a collection of notes and vertical line measure markers and
//responsible for drawing them in scene and moving them left
-(void) initSequence {
_sequence = [[Sequencer alloc] initWithWidth:1150];
[_sequence setPosition:CGPointMake(130, 0)];
[self addChild:_sequence];
}
...
//responsible for handling incoming MIDI input from a keyboard and checking to see
//if notes are played on time, early or late
-(void) initNoteProcessor {
_noteProcessor = [[NoteProcessor alloc] initWithSequence:_sequence];
}
...
//creates a vertical line that indicates when notes need to be played
-(void) initPlayBar {
_playBar = (SKSpriteNode*)[self childNodeWithName:#"playBar"];
_playBar.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:_playBar.frame.size];
_playBar.physicsBody.affectedByGravity = NO;
[_playBar.physicsBody setDynamic:YES];
[_playBar.physicsBody setAllowsRotation:NO];
[_playBar.physicsBody setCategoryBitMask:kPlayBarCategory];
[_playBar.physicsBody setCollisionBitMask:kNoCategory];
[_playBar.physicsBody setContactTestBitMask:kNoteCategory | kLineCategory];
[[NSUserDefaults standardUserDefaults] setDouble:_playBar.position.x forKey:#"playBarXPosition"];
}
NoteProcessor.m
#implementation NoteProcessor
-(instancetype) initWithSequence:(Sequencer*)sequence {
if (self = [super init]) {
_sequence = sequence;
_noteActiveIndex = 0;
[self initActiveNoteIndices];
[self initNoteIndicator];
//when user plays key on keyboard, notify this class
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(midiPlayed:) name:#"MidiPlayedNotificationKey" object:nil];
}
return self;
}
//keeps track of the next note of each pitch that is incoming
-(void) initActiveNoteIndices {
_activeNoteIndicesByPitch = [NSMutableArray array];
for (int i = 0; i < kTotalKeys; i++) {
[_activeNoteIndicesByPitch addObject:[NSNumber numberWithInteger:0]];
}
}
//checks position of note and decides whether they are on time, early or late
-(void) updateCurrentNoteStatus {
...
}
//handles notifications from MIDI keyboard when user plays a key
-(void) midiPlayed:(NSNotification*)notification {
//stuff happens here
}
#end
Sequencer.m
#implementation Sequencer
-(instancetype) initWithWidth:(double)width {
if (self = [super init]) {
...
[self drawNotes];
}
return self;
}
//draws all of the notes on screen
-(void) drawNotes {
NSMutableArray *parsedNotes = [_midiParser getNoteData];
float distancePerBeat = [self getGridArea] / [self getMeasuresDisplayed] / 4;
float xPos = 0;
for (int i = 0; i < [parsedNotes count]; i++) {
NSArray *currNote = [parsedNotes objectAtIndex:i];
double location = [[currNote objectAtIndex:0] doubleValue];
double duration = [[currNote objectAtIndex:1] doubleValue];
int midiValue = [[currNote objectAtIndex:2] intValue];
Note *note = [[Note alloc] initWithLocation:location Duration:duration MidiValue:midiValue];
xPos = distancePerBeat * (location - 1);
float yPos = kNoteHeight[note.midiValue - 36];
[note setPosition:CGPointMake(xPos, yPos)];
[note setZPosition:kNoteZPosition];
NSMutableArray *noteList = [_notes objectAtIndex:midiValue-36];
[noteList addObject:note];
[self addChild:note];
}
}
//this method is called 60 times per second
//each call moves 1/60th of the distance it would move in 1/60th of a second
-(void) moveLeftAfterElapsedTime:(double)elapsedTime {
double timePerBeat = 60 / [self getBpm];
double timePerMeasure = timePerBeat * 4;
double lengthPerMeasure = [self getGridArea] / [self getMeasuresDisplayed];
double distancePerFrame = lengthPerMeasure / (timePerMeasure * 60);
double x = elapsedTime * distancePerFrame * 60;
[self moveAllNotesLeftByDistance:x];
}
//actually does all of the moving of the notes
-(void) moveAllNotesLeftByDistance:(double)distance {
for (int i = 0; i < [_notes count]; i++) {
NSMutableArray *noteList = [_notes objectAtIndex:i];
for (int j = 0; j < [noteList count]; j++) {
Note *note = [noteList objectAtIndex:j];
double xPos = note.position.x - distance;
double y = note.position.y;
[note setPosition:CGPointMake(xPos, y)];
}
}
}

Getting memory warnings after starting a new Cocos2d-iPhone project in ARC. Beginner

Have recently been teaching myself objective-c in order to learn Cocos2d.
Started a new project to teach myself the basics with a classic falling gems tetris-like game. The first step I took was to enable the project for ARC using edit > refactor, and selecting the last 4 files in the template.
So far have been able to add a single gem with a dynamically colored sprite through subclassing CCSprite, and move it along the XCoordinates while having it fall from the top. The gems are given a gemPos location – eg 5,3 – in order for me to later implement the matching methods.
Am getting an EXC_BAD_ACCESS warning when a few of the blocks stack up on top of each other after testing out the project for a while. Am confused as to why this is happening if ARC is enabled.
My clumsy code is as follows (have left out .h files):
Default helloworld layer:
-(id) init
{
if( (self=[super init]) ) {
oldGems = [[NSMutableArray alloc]init];
[self setIsTouchEnabled:YES];
[self newGem];
[self scheduleUpdate];
}
return self;
}
- (void)registerWithTouchDispatcher
{
[[[CCDirector sharedDirector] touchDispatcher] addTargetedDelegate:self priority:0 swallowsTouches:YES];
}
- (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event
{
CGPoint touchLocation = [self convertTouchToNodeSpace:touch];
oldPos = touchLocation;
return TRUE;
}
- (void)ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event
{
CGPoint touchLocation = [self convertTouchToNodeSpace:touch];
[self updateXCoord:(CGPoint)touchLocation];
}
- (void)updateXCoord:(CGPoint)touchLocation
{
CGPoint distance = ccpSub(touchLocation, oldPos);
float xDistance = distance.x;
if (abs(xDistance) >= 36) {
if (touchLocation.x > oldPos.x) {
[_thisGem setPosition:ccp([_thisGem position].x + 36, [_thisGem position].y)];
[_thisGem setGemPos:ccp([_thisGem gemPos].x+1,[_thisGem gemPos].y)];
NSLog(#"The gem position is %#", NSStringFromCGPoint([_thisGem gemPos]));
oldPos = touchLocation;
} else {
[_thisGem setPosition:ccp([_thisGem position].x - 36, [_thisGem position].y)];
[_thisGem setGemPos:ccp([_thisGem gemPos].x-1,[_thisGem gemPos].y)];
NSLog(#"The gem position is %#", NSStringFromCGPoint([_thisGem gemPos]));
oldPos = touchLocation;
}
}
}
- (void)newGem
{
if (_thisGem) {
PCGem *oldGem = _thisGem;
NSLog(#"Old gem position at %#", NSStringFromCGPoint([oldGem gemPos]));
[oldGems addObject:oldGem];
}
_thisGem = [[PCGem alloc] initWithGemColor];
[_thisGem setPosition:ccp(160, 450)];
[self addChild:_thisGem];
[_thisGem setGemPos:ccp(4,10)];
NSLog(#"Gem added with %# color", [_thisGem gemColorName]);
}
- (void)update:(ccTime)dt
{
if ([_thisGem gemPos].y > 0) {
[self spaceBelowOccupied];
[_thisGem setPosition:ccp([_thisGem position].x, [_thisGem position].y-1)];
[_thisGem setGemPos:ccp([_thisGem gemPos].x, floor([_thisGem position].y / 36))];
} else {
[self newGem];
}
}
- (void)spaceBelowOccupied
{
for (PCGem *occupiedTile in oldGems) {
if (CGRectIntersectsRect(occupiedTile.boundingBox, _thisGem.boundingBox)) {
NSLog(#"Collision detected!");
[self newGem];
}
}
}
And my clumsy PCGem class:
- (id)initWithGemColor
{
if ((self = [super initWithFile:#"gemGS.png"])) {
NSArray *gemColorList = [NSArray arrayWithObjects:(NSString *)#"blue", (NSString *)#"red", (NSString *)#"green", nil];
NSInteger randColor = arc4random_uniform(3);
switch (randColor) {
case 0:
{
[self setGemColorName:[gemColorList objectAtIndex:0]];
ccColor3B gemColorRGB = {0,0,255};
[self setColor:gemColorRGB];
break;
}
case 1:
{
[self setGemColorName:[gemColorList objectAtIndex:1]];
ccColor3B gemColorRGB = {255,0,0};
[self setColor:gemColorRGB];
break;
}
case 2:
{
[self setGemColorName:[gemColorList objectAtIndex:2]];
ccColor3B gemColorRGB = {0,255,0};
[self setColor:gemColorRGB];
break;
}
}
}
return self;
}
Which leads me to another question, which I can't seem to find the answer to. In examples of ARC enabled Cocos2d projects, I've seen the node convenience method used, which of course is alloc]init]autorelease]. But I thought you can't use Autorelease with ARC and this would cause a crash? How does this work?
Any ideas to my woes? Would greatly appreciate clarification :)
Cheers,
Adrian
I think it might be caused by editing an array while enumerating through it. In your update function you call spaceBelowOccupied:
- (void)spaceBelowOccupied
{
for (PCGem *occupiedTile in oldGems) {
if (CGRectIntersectsRect(occupiedTile.boundingBox, _thisGem.boundingBox)) {
NSLog(#"Collision detected!");
[self newGem];
}
}
}
then if newGem gets called while in your for loop and this loop succeeds:
if (_thisGem) {
PCGem *oldGem = _thisGem;
NSLog(#"Old gem position at %#", NSStringFromCGPoint([oldGem gemPos]));
[oldGems addObject:oldGem];
}
then you are adding an object to an array while enumerating through it. So change your spaceBelowOccupied to this and see if it works:
- (void)spaceBelowOccupied
{
for (PCGem *occupiedTile in [oldGems copy]) {
if (CGRectIntersectsRect(occupiedTile.boundingBox, _thisGem.boundingBox)) {
NSLog(#"Collision detected!");
[self newGem];
}
}
}
that way you make a copy of oldGems to enumerate through that will get autoreleased once you are done going through it.

Having Draggable NSTabViewItem

is it possible to provide draggable NSTabViewItem,
Basically what i want, if i down L Button on the label of NSTabViewITem and Move, i should allow to drag the TabView item,
I want to do it for Moving of NSTabView Item and have one more feature, if user drag a Label of NSTabView Item and move it to a perticular region, then i should allow to remove that NSTabView Item,
I could able to find only one way of having PSMTab bar, but i have other features also on NSTabView Item that i will be missing if i go with that approach.
Thanks for looking into it,
Somehow i could able to do it.... posting some important piece of code...
1 -- Have to have Custom TabView class for Handling mouse events.
// Interface posted below,
#import <Cocoa/Cocoa.h>
typedef enum __itemDragState{
itemNotDragging = 0,
itemDragStatNormal = 0,
itemDragging = 1,
itemDropped = 2
} ItemDragStat;
#protocol CustomTabViewDelegate <NSObject>
#required
-(bool)allowDrag;
-(bool)allowDrop;
-(void)dragEnter;
-(void)acceptDrop;
-(void)draggingCancelled;
-(void)itemDropped:(id)draggedTabViewItem;
-(void)itemDroppedCompleted:(id)droppedTabViewItem;
#end
#interface CustomTab : NSTabView{
ItemDragStat eItemDragStat;
id draggedItem;
}
#property(assign)id draggedItem;
#end
Now some of the important implementation
#import "CustomTab.h"
#include "Log.h"
#implementation CustomTab
#synthesize draggedItem;
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code here.
}
return self;
}
# if 0
// don't delete it, might need later on
- (void)drawRect:(NSRect)dirtyRect
{
// Drawing code here.
}
# endif
- (void)mouseUp:(NSEvent *)theEvent{
log(" Mouse up ");
NSPoint location = [self convertPoint: [theEvent locationInWindow]
fromView: nil];
NSTabViewItem *anItem = [self tabViewItemAtPoint: location];
if ( anItem == nil ) {
// if its mouse up else where, reject dragging regardless
eItemDragStat = itemDragStatNormal;
log("Item will not be dropped");
return;
}
if ( ![anItem isEqual:[self selectedTabViewItem]]){
log("Mouse up is in nonselected item");
if ( eItemDragStat == itemDragging){
log("Item will be dropped into this ");
id droppedTabViewItem = anItem;
if ( droppedTabViewItem && [droppedTabViewItem respondsToSelector:#selector(itemDropped:)]){
id selectedTabViewItem = [self selectedTabViewItem];
[droppedTabViewItem performSelector:#selector(itemDropped:) withObject:selectedTabViewItem];
}
}
}
eItemDragStat = itemDragStatNormal;
// return;
// [super mouseUp:theEvent];
}
- (void)mouseDown:(NSEvent *)theEvent{
NSPoint location = [self convertPoint: [theEvent locationInWindow]
fromView: nil];
draggedItem = [self tabViewItemAtPoint:location];
NSTabViewItem *anItem = [self tabViewItemAtPoint: location];
if (anItem != nil && ![anItem isEqual: [self selectedTabViewItem]])
{
[self selectTabViewItem: anItem];
}
}
- (void)mouseDragged:(NSEvent *)theEvent{
NSPoint location = [self convertPoint: [theEvent locationInWindow]
fromView: nil];
id tabViewItemId = [self tabViewItemAtPoint:location];
NSTabViewItem *anItem = [self tabViewItemAtPoint: location];
if (anItem){
if (![anItem isEqual:draggedItem]){
if (tabViewItemId && [tabViewItemId respondsToSelector:#selector(allowDrag)]){
eItemDragStat = itemDragging;
}else{
// drag will be cancelled now.
// tell client item to stop dragging
if (eItemDragStat == itemDragging){
if ( draggedItem && [ draggedItem respondsToSelector:#selector(draggingCancelled)]){
[draggedItem performSelector:#selector(draggingCancelled)];
draggedItem = nil;
}
}
eItemDragStat = itemNotDragging;
// if we have +cursor then it should be reset
}
}else{
log(" Mouse dragged");
}
}else{
// dragging went elsewhere, lets close this dragging operation
if ( draggedItem && [ draggedItem respondsToSelector:#selector(draggingCancelled)]){
[draggedItem performSelector:#selector(draggingCancelled)];
draggedItem = nil;
}
// here reset the mouse pointer
eItemDragStat = itemNotDragging;
}
}
#end
It needs some more fine tuning and its going on....

How to add a CCsprite to layer from array

Hi guys I really need some help, I have been stuck in this part of my game for over a week now and I can't seem to get past this issue, So have look at my code below,
#import "HelloWorldLayer.h"
#import "AppDelegate.h"
#implementation HelloWorldLayer
+(CCScene *) scene
{
// 'scene' is an autorelease object.
CCScene *scene = [CCScene node];
// 'layer' is an autorelease object.
HelloWorldLayer *layer = [HelloWorldLayer node];
// add layer as a child to scene
[scene addChild: layer];
// return the scene
return scene;
}
-(id) init
{
// always call "super" init
// Apple recommends to re-assign "self" with the "super's" return value
if( (self=[super init]) ) {
moles = [[NSMutableArray alloc] init];
winSize = [[CCDirector sharedDirector]winSize];
CCSprite *mole1 = [CCSprite spriteWithFile:#"lightsabericonblue.png"];
[self starCreateCurrentLevel:mole1];
}
return self;
}
-(void)starCreateCurrentLevel:(CCSprite *)mole1{
starCountCurrentLevel = 10;
for (int i = 0; i < starCountCurrentLevel;) {
[moles addObject:mole1];
starCountCurrentLevel--;
}
[self schedule:#selector(tryPopMoles:) interval:1];
}
- (void)tryPopMoles:(ccTime)dt {
if(moles.count != 0){
for (CCSprite *mole in moles) {
if (arc4random() % moles.count == 0) {
if (mole.numberOfRunningActions == 0) {
[self popMole:mole];
}
}
}
}else if(moles.count == 0){
NSLog(#"No More Moles To Spawn");
[self unschedule:#selector(tryPopMoles:)];
}
}
- (void) popMole:(CCSprite *)mole {
mole.position = ccp(150, 150);
[self addChild:mole];
}
- (void) dealloc
{
[moles release];
moles = nil;
[super dealloc];
}
#end
When I run this code i get the following error, * Assertion failure in -[HelloWorldLayer addChild:z:tag:], /Users/....../libs/cocos2d/CCNode.m:335.
I when i add the child in the init method its fine but I don't want to do that, I want to be able to call the try pop mole which will then call the pop mole based on if there is anymore sprites left in the array, I have a feeling I am missing something or doing something wrong.
UPDATE -
#import "HelloWorldLayer.h"
#import "AppDelegate.h"
#pragma mark - HelloWorldLayer
CGSize winSize;
int enemyX;
int enemyY;
int randomAngleY;
int randomAngleX;
int winSizeX;
int winSizeY;
int minDuration = 1;
int maxDuration = 4.0;
int rangeDuration;
int actualDuration;
int starCountCurrentLevel;
int test = 0;
#implementation HelloWorldLayer
+(CCScene *) scene
{
CCScene *scene = [CCScene node];
HelloWorldLayer *layer = [HelloWorldLayer node];
[scene addChild: layer];
return scene;
}
-(id) init
{
if( (self=[super init]) ) {
moles = [[NSMutableArray alloc] init];
winSize = [[CCDirector sharedDirector]winSize];
[self starCreateCurrentLevel];
}
return self;
}
-(void)starCreateCurrentLevel{
starCountCurrentLevel = 10;
for (int i = 0; i < starCountCurrentLevel;) {
[moles addObject:[CCSprite spriteWithFile:#"lightsabericonblue.png"]];
starCountCurrentLevel--;
}
[self schedule:#selector(tryPopMoles:) interval:3];
}
- (void)tryPopMoles:(ccTime)dt {
NSMutableArray *tempArray = [NSMutableArray arrayWithArray:moles];
for (int randomIndex = tempArray.count -1; randomIndex < tempArray.count; randomIndex--) {
CCSprite * sprite = (CCSprite *)[tempArray objectAtIndex:randomIndex];
//CCSprite *sprite = [tempArray objectAtIndex:randomIndex];
[self popMole:sprite];
NSLog(#"Enemy Added");
[tempArray removeObject:sprite];
}
if([tempArray count] == 0){
NSLog(#"No More Moles To Spawn");
[self unschedule:#selector(tryPopMoles:)];
}
}
- (void) popMole:(CCSprite *)sprite {
winSizeX = winSize.width - sprite.contentSize.width + 25;
winSizeY = winSize.height - 25 - sprite.contentSize.height + 17;
randomAngleX = arc4random() % winSizeX;
randomAngleY = arc4random() % winSizeY;
enemyX = arc4random() % winSizeX ;
enemyY = arc4random() % winSizeY;
rangeDuration = maxDuration - minDuration;
actualDuration = (arc4random() % rangeDuration) + minDuration;
sprite.position = ccp(enemyX, enemyY);
[self addChild:sprite];
}
- (void) dealloc
{
[moles release];
moles = nil;
[super dealloc];
}
#end
You're only creating the sprite once, but trying to add it several times. (Remember, these are pointers to objects, so you're always referring to exactly the same sprite.) If you want 10 versions of the same sprite (at different positions, scales, speeds, whatever), you'll need to create the sprite (via the creator method) 10 times. You can do a copy on the original one, or just create it on the fly:
-(void)starCreateCurrentLevel:(CCSprite *)mole1{
starCountCurrentLevel = 10;
for (int i = 0; i < starCountCurrentLevel;) {
//[moles addObject:mole1];
// ^^ You're adding the same sprite (meaning same pointer reference) again and again. Try this instead:
[moles addObject:[CCSprite spriteWithFile:#"lightsabericonblue.png"]];
starCountCurrentLevel--;
}
Of course, now you don't need to pass mole1 into your method. You may (depending on your need) consider passing in a spriteFrameName or something.

Observe touches on multiple views in one motion

I have a parent view with 3 separate child views. The child views are spread out within the parent with no overlap (and with some space in between). As a user moves her finger around the screen (without lifting it), I'd like to track touches as they enter and exit each of the child views.
Example: If the user begins touching somewhere on the screen outside of the child views, then swipes her finger over child 1, off of child 1, over child 2, and then lets go, I would expect these events to be triggered:
Touch began
Touch entered child 1
Touch exited child 1
Touch entered child 2
Touch ended
It seems as if touchesBegan:withEvent: and touchesEnded:withEvent: methods would be helpful in this case, but when I define them on the child view controllers, they don't do exactly what I want -- if the user begins touching outside the child view, then swipes over the child view, no touch events are triggered on the child itself.
Current Solution: I'm currently using a solution that feels really hacky to accomplish this. I'm observing touchesBegan:withEvent:, touchesEnded:withEvent:, and touchesMoved:withEvent: on the parent, grabbing the coordinates of each event, and determining if they lie within the bounds of a child. If they do, I trigger the appropriate events as described above.
This method mostly works, but feels very inefficient. It feels like the framework should handle this work for me. My state management code also sometimes misses an "enter" or "exit" trigger and I suspect it's because touch events were either dropped or came to me in an unexpected order. Am I missing a better method here?
The simplest solution would be something like:
- (void)viewDidLoad
{
[super viewDidLoad];
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(pan:)];
[self.view addGestureRecognizer:pan];
// Do any additional setup after loading the view.
}
- (void)pan:(UIPanGestureRecognizer *)sender
{
static NSInteger startViewIndex;
static NSInteger endViewIndex;
CGPoint location = [sender locationInView:self.view];
if (sender.state == UIGestureRecognizerStateBegan)
{
if (CGRectContainsPoint(self.view0.frame, location))
startViewIndex = 0;
else if (CGRectContainsPoint(self.view1.frame, location))
startViewIndex = 1;
else if (CGRectContainsPoint(self.view2.frame, location))
startViewIndex = 2;
else
startViewIndex = -1;
}
else if (sender.state == UIGestureRecognizerStateEnded)
{
if (CGRectContainsPoint(self.view0.frame, location))
endViewIndex = 0;
else if (CGRectContainsPoint(self.view1.frame, location))
endViewIndex = 1;
else if (CGRectContainsPoint(self.view2.frame, location))
endViewIndex = 2;
else
endViewIndex = -1;
if (startViewIndex != -1 && endViewIndex != -1 && startViewIndex != endViewIndex)
{
// successfully moved between subviews!
NSLog(#"Moved from %1d to %1d", startViewIndex, endViewIndex);
}
}
}
Perhaps a little more elegant would be to define your own custom gesture recognizer (that way if you aren't dragging from one of your subviews, it will fail which will allow other gesture recognizers you might have going on elsewhwere to work ... probably not an issue unless you're use multiple gesture recognizers; it also isolates the gory details of the gesture logic from the rest of your view controller):
#interface PanBetweenSubviewsGestureRecognizer : UIPanGestureRecognizer
{
NSMutableArray *_arrayOfFrames;
}
#property NSInteger startingIndex;
#property NSInteger endingIndex;
#end
#implementation PanBetweenSubviewsGestureRecognizer
#synthesize startingIndex = _startingIndex;
#synthesize endingIndex = _endingIndex;
- (void)dealloc
{
_arrayOfFrames = nil;
}
- (id)initWithTarget:(id)target action:(SEL)action
{
self = [super initWithTarget:target action:action];
if (self)
{
_arrayOfFrames = [[NSMutableArray alloc] init];
}
return self;
}
- (void)addSubviewToArrayOfFrames:(UIView *)view
{
[_arrayOfFrames addObject:[NSValue valueWithCGRect:view.frame]];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesBegan:touches withEvent:event];
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:self.view];
for (NSInteger i = 0; i < [_arrayOfFrames count]; i++)
{
if (CGRectContainsPoint([[_arrayOfFrames objectAtIndex:i] CGRectValue], location))
{
self.startingIndex = i;
return;
}
}
self.startingIndex = -1;
self.endingIndex = -1;
self.state = UIGestureRecognizerStateCancelled;
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesEnded:touches withEvent:event];
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView:self.view];
for (NSInteger i = 0; i < [_arrayOfFrames count]; i++)
{
if (CGRectContainsPoint([[_arrayOfFrames objectAtIndex:i] CGRectValue], location))
{
self.endingIndex = i;
return;
}
}
self.endingIndex = -1;
self.state = UIGestureRecognizerStateCancelled;
}
#end
Which you could then use as follows:
- (void)viewDidLoad
{
[super viewDidLoad];
PanBetweenSubviewsGestureRecognizer *pan = [[PanBetweenSubviewsGestureRecognizer alloc] initWithTarget:self action:#selector(pan:)];
[pan addSubviewToArrayOfFrames:self.view0];
[pan addSubviewToArrayOfFrames:self.view1];
[pan addSubviewToArrayOfFrames:self.view2];
[self.view addGestureRecognizer:pan];
// Do any additional setup after loading the view.
}
- (void)pan:(PanBetweenSubviewsGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateEnded && sender.startingIndex >= 0 && sender.endingIndex >= 0 && sender.startingIndex != sender.endingIndex)
{
// successfully moved between subviews!
NSLog(#"Moved from %1d to %1d", sender.startingIndex, sender.endingIndex);
}
}