Objective - C: `self` not responding - objective-c

I have a cocos2d layer named ChoosePlayer and in init method I am adding a few sprite using [self addChild:]. Its plain and works right. But when I try to do the same in another method as given below, its not working:
-(void) avatarchanged {
[self addChild:[CCSprite spriteWithFile:#"av1.png"]];
[self runAction:[CCMoveBy actionWithDuration:1.0 position:ccp(100, 100)]];
NSLog(#"added new avatar");
}
The [self runAction:] is also not responding. So I guess its not the problem with sprite, but with the self itself.
In between the init and avatarchanged, what I am doing is showing a UIView on top of openGL View, perform some actions there and returning back as follows:
-(void) selectAvatar {
CGSize winSize = [CCDirector sharedDirector].winSize;
flowCoverView = [[[FlowCoverView alloc] initWithFrame: CGRectMake(0, 0, 480, 320)] autorelease];
flowCoverView.center = ccp(-80 + winSize.width / 2, 80 + winSize.height / 2);
flowCoverView.delegate = self;
flowCoverView.transform = CGAffineTransformMakeRotation(90*(3.14/180));
[[CCDirector sharedDirector].openGLView.window addSubview:flowCoverView];
}
When the necessary actions are performed, flowCoverView is removed as follows:
- (void)flowCover:(FlowCoverView *)view didSelect:(int)cover {
selectedavat = cover;
[flowCoverView removeFromSuperview];
[[NSNotificationCenter defaultCenter] postNotificationName:#"avatarchanged" object:nil];
}
The notification posted above invoked my avatarchanged method, where the self is not responding.
Edit: here is my init method:
-(id) init {
if( (self=[super init])) {
self.isTouchEnabled = YES;
BG = [CCSprite spriteWithFile:#"opponent.jpg"];
BG.scale *= CC_CONTENT_SCALE_FACTOR() * 1;
BG.position = ccp(240,160);
[self addChild:BG];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(avatarchanged) name:#"avatarchanged" object:nil];
}
return self;
}
Note: In my project there is a bunch of global variables declared using extern, they might something to do with my problem, but am not sure.
Could someone please help me with this?
Edit 2:
changed avatarchanged as follows:
-(void) avatarchanged {
if (self == nil) {
NSLog(#"self is nil!!!!!!!!");
} else {
NSLog(#"pheww.. its not nil");
}
if (self.isRunning) {
NSLog(#"running");
} else {
NSLog(#"not running");
}
[BG runAction:[CCRotateBy actionWithDuration:1.0 angle:100.0]];
[self addChild:[CCSprite spriteWithFile:#"av1.png"]];
NSLog(#"added new avatar");
[self runAction:[CCMoveBy actionWithDuration:1.0 position:ccp(100, 100)]];
}
log shows as
2012-03-26 11:16:21.213 Funsip[1550:207] pheww.. its not nil
2012-03-26 11:16:21.214 Funsip[1550:207] running
2012-03-26 11:16:21.224 Funsip[1550:207] added new avatar
the BG's runAction is also not getting applied, but doing the same in init method works perfectly right.
Edit 3:
The FlowCoverView that I add is implemented with OpenGL calls internally. May be it could be causing conflicts with OpenGL states setup in cocos2d. But I dont know OpenGL to look for these sort of issues.
Here is the link to the page where I took the flowcoverview from http://www.chaosinmotion.com/flowcover.html

Is 'self' in running mode (self.isRunning) ? if not nothing much will happen from a cocos2d point of view. The isRunning mode is achieved when you add the ChoosePlayer instance to a running CCNode descendant. If you forgot to add it to a running node, it will be ignored in draws, actions, etc ...

This might help: http://www.cocos2d-iphone.org/forum/topic/28056

Related

Xcode os x : Why drawRect could not access instance data

I am trying to draw something in my custom view, but not sure why drawRect could not access its instance data. Here is the steps I tried.
Create a Mac OS X app, with using storyboard checked.
In the storyboard, delete the view, then add a new custom view under the view at the same place. (I tried if the view is not deleted, same).
Assign EEGView class to the newly added custom view.
then run. From the log information, you will notice that the drawRect could not access the instance data although the instance variables get initialized and updated.
In viewCtroller.m
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
myView = [[EEGView alloc] init];
//[self.view addSubview:myView];
//Start Timer in 3 seconds to show the result.
NSTimer* _timerAppStart = [NSTimer scheduledTimerWithTimeInterval:2
target:self
selector:#selector(UpdateEEGData)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_timerAppStart forMode:NSDefaultRunLoopMode];
}
- (void)UpdateEEGData
{
// NSLog(#"UpdateEEGData.....1");
// myView.aaa = 200;
// myView.nnn = [NSNumber numberWithInteger:myView.aaa];
// make sure this runs on the main thread
if (![NSThread isMainThread]) {
// NSLog(#"NOT in Main thread!");
[self performSelectorOnMainThread:#selector(updateDisplay) withObject:nil waitUntilDone:TRUE];
}else
{
[self.view setNeedsDisplay:YES];
}
NSLog(#"UpdateEEGData.....2");
[myView setAaa:400];
myView.nnn = [NSNumber numberWithInteger:myView.aaa];
// make sure this runs on the main thread
if (![NSThread isMainThread]) {
// NSLog(#"NOT in Main thread!");
[self performSelectorOnMainThread:#selector(updateDisplay) withObject:nil waitUntilDone:TRUE];
}else
{
[self.view setNeedsDisplay:YES];
}
}
-(void)updateDisplay
{
[self.view setNeedsDisplay:YES];
}
In my custom view class EEGView.m
#implementation EEGView
#synthesize aaa;
#synthesize nnn;
-(id)init{
self = [super init];
if (self) {
aaa = 10;
nnn = [NSNumber numberWithInteger:aaa];
NSLog(#"init aaa: %i", aaa);
NSLog(#"init nnn: %i", [nnn intValue]);
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
// Drawing code here.
NSLog(#"drawRect is here");
NSLog(#"drawRect aaa: %i", aaa);
NSLog(#"drawRect nnn: %i", [nnn intValue]);
}
#end
Did I miss anything? Tested in Xcode 7.2 & 7.2. But if I leave the 'using storyboard' unchecked, it works.
Or is it a Xcode bug?
Any advice appreciated.
Thanks in advance.
If you've added EEGView view on storyboard, you shouldn't be also instantiating one in viewDidLoad. You've alloc/init'ed a new one, but it bears no relationship to the one that the storyboard created for you. So, the one created by the storyboard has drawRect called, but you're setting the properties in the separate instance that you created in viewDidLoad which was never added to the view hierarchy (and thus never will have its drawRect called).
When the storyboard instantiates the view controller's view, it will instantiate your EEGView for you. All you need to do is to hook up an IBOutlet for this view in order to get a reference to it from your view controller. (For example, you can control drag from the EEGView in the storyboard scene to the #interface for the view controller that you've pulled up in the assistant editor.)

How to draw custom window controls (close, minimize, and zoom buttons)

I've made an attempt to draw custom NSButtons, but it seems I'm reinventing the wheel here. Is there a way to just replace the default images used for the close, minimize and zoom buttons?
Several apps already do it:
OSX 10.8's Reminders app (they appear dark grey when the window is not key, vs most appear light grey)
Tweetbot (All buttons look totally custom)
More info:
I can generate the system defaults as such standardWindowButton:NSWindowCloseButton. But from there the setImage setter doesn't change the appearance of the buttons.
Edit: Since I wrote this, INAppStore has implemented a pretty nice way to do this with INWindowButton. If you're looking for a drag and drop solution check there, but the code below will still help you implement your own.
So I couldn't find a way to alter the standardWindowButtons. Here is a walkthrough of how I created my own buttons.
Note: There are 4 states the buttons can be in
Window inactive
Window active - normal
Window active - hover
Window active - press
On to the walkthrough!
Step 1: Hide the pre-existing buttons
NSButton *windowButton = [self standardWindowButton:NSWindowCloseButton];
[windowButton setHidden:YES];
windowButton = [self standardWindowButton:NSWindowMiniaturizeButton];
[windowButton setHidden:YES];
windowButton = [self standardWindowButton:NSWindowZoomButton];
[windowButton setHidden:YES];
Step 2: Setup the view in Interface Builder
You'll notice on hover the buttons all change to their hover state, so we need a container view to pick up the hover.
Create a container view to be 54px wide x 16px tall.
Create 3 Square style NSButtons, each 14px wide x 16px tall inside the container view.
Space out the buttons so there is are 6px gaps in-between.
Setup the buttons
In the attributes inspector, set the Image property for each button to the window-active-normal image.
Set the Alternate image property to the window-active-press image.
Turn Bordered off.
Set the Type to Momentary Change.
For each button set the identifier to close,minimize or zoom (Below you'll see how you can use this to make the NSButton subclass simpler)
Step 3: Subclass the container view & buttons
Container:
Create a new file, subclass NSView. Here we are going to use Notification Center to tell the buttons when they should switch to their hover state.
HMTrafficLightButtonsContainer.m
// Tells the view to pick up the hover event
- (void)viewDidMoveToWindow {
[self addTrackingRect:[self bounds]
owner:self
userData:nil
assumeInside:NO];
}
// When the mouse enters/exits we send out these notifications
- (void)mouseEntered:(NSEvent *)theEvent {
[[NSNotificationCenter defaultCenter] postNotificationName:#"HMTrafficButtonMouseEnter" object:self];
}
- (void)mouseExited:(NSEvent *)theEvent {
[[NSNotificationCenter defaultCenter] postNotificationName:#"HMTrafficButtonMouseExit" object:self];
}
Buttons:
Create a new file, this time subclass NSButton. This one's a bit more to explain so I'll just post all the code.
HMTrafficLightButton.m
#implementation HMTrafficLightButton {
NSImage *inactive;
NSImage *active;
NSImage *hover;
NSImage *press;
BOOL activeState;
BOOL hoverState;
BOOL pressedState;
}
-(id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self setup];
}
return self;
}
- (id)initWithFrame:(NSRect)frameRect {
self = [super initWithFrame:frameRect];
if (self) {
[self setup];
}
return self;
}
- (void)setup {
// Setup images, we use the identifier to chose which image to load
active = [NSImage imageNamed:[NSString stringWithFormat:#"window-button-%#-active",self.identifier]];
hover = [NSImage imageNamed:[NSString stringWithFormat:#"window-button-%#-hover",self.identifier]];
press = [NSImage imageNamed:[NSString stringWithFormat:#"window-button-%#-press",self.identifier]];
inactive = [NSImage imageNamed:#"window-button-all-inactive"];
// Checks to see if window is active or inactive when the `init` is called
if ([self.window isMainWindow] && [[NSApplication sharedApplication] isActive]) {
[self setActiveState];
} else {
[self setInactiveState];
}
// Watch for hover notifications from the container view
// Also watches for notifications for when the window
// becomes/resigns main
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(setActiveState)
name:NSWindowDidBecomeMainNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(setInactiveState)
name:NSWindowDidResignMainNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(hoverIn)
name:#"HMTrafficButtonMouseEnter"
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(hoverOut)
name:#"HMTrafficButtonMouseExit"
object:nil];
}
- (void)mouseDown:(NSEvent *)theEvent {
pressedState = YES;
hoverState = NO;
[super mouseDown:theEvent];
}
- (void)mouseUp:(NSEvent *)theEvent {
pressedState = NO;
hoverState = YES;
[super mouseUp:theEvent];
}
- (void)setActiveState {
activeState = YES;
if (hoverState) {
[self setImage:hover];
} else {
[self setImage:active];
}
}
- (void)setInactiveState {
activeState = NO;
[self setImage:inactive];
}
- (void)hoverIn {
hoverState = YES;
[self setImage:hover];
}
- (void)hoverOut {
hoverState = NO;
if (activeState) {
[self setImage:active];
} else {
[self setImage:inactive];
}
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#end
In IB set the Custom Class of the container view and all 3 buttons to their respective classes that we just created.
Step 4: Set the button actions
These methods, called from the view controller, are the same as the standardWindowButtons'. Link them to the buttons in IB.
- (IBAction)clickCloseButton:(id)sender {
[self.view.window close];
}
- (IBAction)clickMinimizeButton:(id)sender {
[self.view.window miniaturize:sender];
}
- (IBAction)clickZoomButton:(id)sender {
[self.view.window zoom:sender];
}
Step 5: Add the view to the window
I have a separate xib and view controller setup specifically for the window controls. The view controller is called HMWindowControlsController
(HMWindowControlsController*) windowControlsController = [[HMWindowControlsController alloc] initWithNibName:#"WindowControls" bundle:nil];
NSView *windowControlsView = windowControlsController.view;
// Set the position of the window controls, the x is 7 px, the y will
// depend on your titlebar height.
windowControlsView.frame = NSMakeRect(7.0, 10.0, 54.0, 16.0);
// Add to target view
[targetView addSubview:windowControlsView];
Hope this helps. This is a pretty lengthy post, if you think I've made a mistake or left something out please let me know.

Reading touch events in a QLPreviewController

I've got a QuickLook view that I view some of my app's documents in. It works fine, but I'm having my share of trouble closing the view again. How do I create a touch event / gesture recognizer for which I can detect when the user wants to close the view?
I tried the following, but no events seem to trigger when I test it.
/------------------------ [ TouchPreviewController.h ]---------------------------
#import <Quicklook/Quicklook.h>
#interface TouchPreviewController : QLPreviewController
#end
//------------------------ [ TouchPreviewController.m ]---------------------------
#import "TouchPreviewController.h"
#implementation TouchPreviewController
- (id)init:(CGRect)aRect {
if (self = [super init]) {
// We set it here directly for convenience
// As by default for a UIImageView it is set to NO
UITapGestureRecognizer *singleFingerDTap = [[UITapGestureRecognizer alloc]
initWithTarget:self action:#selector(handleSingleDoubleTap:)];
singleFingerDTap.numberOfTapsRequired = 2;
[self.view addGestureRecognizer:singleFingerDTap];
[self.view setUserInteractionEnabled:YES];
[self.view setMultipleTouchEnabled:YES];
//[singleFingerDTap release];
}
return self;
}
- (IBAction)handleSingleDoubleTap:(UIGestureRecognizer *) sender {
CGPoint tapPoint = [sender locationInView:sender.view.superview];
[UIView beginAnimations:nil context:NULL];
sender.view.center = tapPoint;
[UIView commitAnimations];
NSLog(#"TouchPreviewController tap!" ) ;
}
// I also tried adding this
- (BOOL)gestureRecognizer:(UIGestureRecognizer *) gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer*) otherGestureRecognizer {
return YES;
}
#end
Edit: For clarification, this is how I instantiate the controller:
documents = [[NSArray alloc] initWithObjects: filename , nil ] ;
preview = [[TouchPreviewController alloc] init];
preview.dataSource = self;
preview.delegate = self;
//set the frame from the parent view
CGFloat w= backgroundViewHolder.frame.size.width;
CGFloat h= backgroundViewHolder.frame.size.height;
preview.view.frame = CGRectMake(0, 0,w, h);
//refresh the preview controller
[preview reloadData];
[[preview view] setNeedsLayout];
[[preview view] setNeedsDisplay];
[preview refreshCurrentPreviewItem];
//add it
[quickLookView addSubview:preview.view];
Also, I've defined the callback methods as this:
- (NSInteger) numberOfPreviewItemsInPreviewController: (QLPreviewController *) controller
{
return [documents count];
}
- (id <QLPreviewItem>) previewController: (QLPreviewController *) controller previewItemAtIndex: (NSInteger) index
{
return [NSURL fileURLWithPath:[documents objectAtIndex:index]];
}
Edit2: One thing i noticed. If I try making swiping gestures, I get the following message. This could shed some light on what is wrong/missing?
Ignoring call to [UIPanGestureRecognizer setTranslation:inView:] since
gesture recognizer is not active.
I think your example code is incomplete. It isn't clear how you are instantiating the TouchPreviewController (storyboard, nib file or loadView.)
I have never used the class so I could be way out in left field.
If you've already instantiated a UITapGestureRecognizer in the parent viewController, it is absorbing the tap events and they aren't passed on to your TouchPreviewController.
I would implement the view hierarchy differently by attaching the UITapGestureRecognizer to the parent viewController and handle presentation and unloading of the QLPreviewController there.
I think you might not have to subclass QLPreviewController by instantiating the viewController from a nib file.
When your parent viewController's UITapGestureRecognizer got an event you would either push the QLPreviewController on the navigation stack or pop it off the navigation stack when done.
Hope this is of some help.

UIImage not updating when called from another object

I have an UIImageView with a method to change the image it displays. It works fine, if the method is triggered from a touch event received by the UIImageView itself, but the same method fails to update the UIImageView, if called from another object, which triggered the touch event. I have an NSLog call in the method in question and thus can see, that the method is called in both cases. But only in one case I can see the actual change of the image in the other case the view is not updated.
When it works, I do not need to setNeedsDisplay or setNeedsLayout, but that is what I tried to fix the problem. Setting needsDisplay and needsLayout in the UIImageView as well as its superview. To no avail. I can see, that the image is actually changed, if I rotate the device, which causes a refresh and I see, that the UIImageView indeed changed.
The superview calls the method eventAtLocation: on an OutletCollection using makeObjectsPerformSelector:withObject:
It sure looks like an embarrassing mistake on my part, but I can't figure it out since hours. I am running out of ideas what to try :-(
Here's the code in question:
- (void)eventAtLocation:(NSValue *)location
{
CGPoint loc = [self.superview convertPoint:[location CGPointValue] toView:self];
if ([self pointInside:loc withEvent:nil]) {
if (!self.isAnimating) {
[self autoSwapImage];
}
}
else{
if (self.isAnimating) {
[self cancelAnimation];
}
}
}
- (void)cancelAnimation
{
self.isAnimating = NO;
[NSObject cancelPreviousPerformRequestsWithTarget:self];
}
- (void) swapImage
{
currentImage++;
if (currentImage > numberOfImages)
currentImage = 1;
NSLog(#"set image to %i", currentImage);
self.image = [UIImage imageNamed:[NSString stringWithFormat:#"img_%i.jpg", currentImage]];
[self setNeedsDisplay];
}
- (void) autoSwapImage
{
self.isAnimating = YES;
[self swapImage];
[self performSelector:#selector(autoSwapImage) withObject:self afterDelay:0.1];
}
// The following works, but if eventAtLocation: is called form the superview swapImage gets called (-> NSLog output appears), but the view is not updated
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//[self autoSwapImage];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
//[NSObject cancelPreviousPerformRequestsWithTarget:self];
}
#end
It looks like you aren't actually setting the UIImageView's image, you are just setting self.image. You need to do something like
self.myImageView.image = [UIImage imageNamed:[NSString stringWithFormat:#"img_%i.jpg", currentImage]];
Also you'll want to put a check in that [UIImage imageNamed:] actually is returning an image, and that its not nil.
UIImage* image = [UIImage imageNamed:#"someImage"];
if(!image){
NSLog(#"%#",#"Image Not Found");
}

Forcing Consecutive Animations with CABasicAnimation

I have a notification that fires in my model when certain properties change. As a result, a selector in a particular view object catches the notifications to change the position of the view accordingly.
The notifications causes a view on the window to move in a particular direction (always vertically or horizontally and always by a standard step size on the window). It's possible for the user-actions to cause several notifications to fire one after the other. For example, 3 notifications could be sent to move the view down three steps, then two more notifications could be sent to move the view to the right two steps.
The problem is that as I execute the animations, they don't happen consecutively. So, in the previous example, though I want the view to move slowly down three spaces and then move over two spaces as a result of the notifications, instead it ends up moving diagonally to the new position.
Here is the code for my two selectors (note that placePlayer sets the position of the view according to current information in the model):
- (void)moveEventHandler: (NSNotification *) notification
{
[self placePlayer];
CABasicAnimation* moveAnimation = [CABasicAnimation animationWithKeyPath:#"position"];
moveAnimation.duration = 3;
moveAnimation.fillMode = kCAFillModeForwards; // probably not necessary
moveAnimation.removedOnCompletion = NO; // probably not necessary
[[self layer] addAnimation:moveAnimation forKey:#"animatePosition"];
}
Any suggestions on how to make multiple calls to this methods force animation to execute step-by-step rather than all at once? Thanks!!
I think what you might want to do here is set up a queue of animations that need to happen consecutively and set the animations delegate so that you will receive the animationDidStop:finished: message. This way when one animation is completed you can set off the next one in the queue.
The solution I implemented does indeed use a queue. Here's a pretty complete description:
This is all done in a view class called PlayerView. In the header I include the following:
#import "NSMutableArray+QueueAdditions.h"
#interface PlayerView : UIImageView {
Player* representedPlayer; // The model object represented by the view
NSMutableArray* actionQueue; // An array used as a queue for the actions
bool animatingPlayer; // Notes if the player is in the middle of an animation
bool stoppingAnimation; // Notes if all animations should be stopped (e.g., for re-setting the game)
CGFloat actionDuration; // A convenient way for me to change the duration of all animations
// ... Removed other variables in the class (sound effects, etc) not needed for this example
}
// Notifications
+ (NSString*) AnimationsDidStopNotification;
#property (nonatomic, retain) Player* representedPlayer;
#property (nonatomic, retain, readonly) NSMutableArray* actionQueue;
#property (nonatomic, assign) CGFloat actionDuration;
#property (nonatomic, assign) bool animatingPlayer;
#property (nonatomic, assign) bool stoppingAnimation;
// ... Removed other properties in the class not need for this example
- (void)placePlayer; // puts view where needed (according to the model) without animation
- (void)moveEventHandler:(NSNotification *) notification; // handles events when the player moves
- (void)rotateEventHandler:(NSNotification *) notification; // handles events when the player rotates
// ... Removed other action-related event handles not needed for this example
// These methods actually perform the proper animations
- (void) doMoveAnimation:(CGRect) nextFrame;
- (void) doRotateAnimation:(CGRect)nextFrame inDirection:(enum RotateDirection)rotateDirection;
// ... Removed other action-related methods not needed for this example
// Handles things when each animation stops
- (void) animationDidStop:(NSString*)animationID
finished:(BOOL)finished
context:(void*)context;
// Forces all animations to stop
- (void) stopAnimation;
#end
As an aside, the QueueAdditions category in NSMutableArray+QueueAdditions.h/m looks like this:
#interface NSMutableArray (QueueAdditions)
- (id)popObject;
- (void)pushObject:(id)obj;
#end
#implementation NSMutableArray (QueueAdditions)
- (id)popObject
{
// nil if [self count] == 0
id headObject = [self objectAtIndex:0];
if (headObject != nil) {
[[headObject retain] autorelease]; // so it isn't dealloc'ed on remove
[self removeObjectAtIndex:0];
}
return headObject;
}
- (void)pushObject:(id)obj
{
[self addObject: obj];
}
#end
Next, in the implementation of the PlayerView, I have the following:
#import "PlayerView.h"
#import <QuartzCore/QuartzCore.h>
#implementation PlayerView
#synthesize actionQueue;
#synthesize actionDuration;
#synthesize animatingPlayer;
#synthesize stoppingAnimation;
// ... Removed code not needed for this example (init to set up the view's image, sound effects, actionDuration, etc)
// Name the notification to send when animations stop
+ (NSString*) AnimationsDidStopNotification
{
return #"PlayerViewAnimationsDidStop";
}
// Getter for the representedPlayer property
- (Player*) representedPlayer
{
return representedPlayer;
}
// Setter for the representedPlayer property
- (void)setRepresentedPlayer:(Player *)repPlayer
{
if (representedPlayer != nil)
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
[representedPlayer release];
}
if (repPlayer == nil)
{
representedPlayer = nil;
// ... Removed other code not needed in this example
}
else
{
representedPlayer = [repPlayer retain];
if (self.actionQueue == nil)
{
actionQueue = [[NSMutableArray alloc] init];
}
[actionQueue removeAllObjects];
animatingPlayer = NO;
stoppingAnimation = NO;
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(moveEventHandler:)
name:[Player DidMoveNotification]
object:repPlayer ];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(rotateEventHandler:)
name:[Player DidRotateNotification]
object:repPlayer ];
// ... Removed other addObserver actions and code not needed in this example
}
}
// ... Removed code not needed for this example
- (void) placePlayer
{
// Example not helped by specific code... just places the player where the model says it should go without animation
}
// Handle the event noting that the player moved
- (void) moveEventHandler: (NSNotification *) notification
{
// Did not provide the getRectForPlayer:onMazeView code--not needed for the example. But this
// determines where the player should be in the model when this notification is captured
CGRect nextFrame = [PlayerView getRectForPlayer:self.representedPlayer onMazeView:self.mazeView];
// If we are in the middle of an animation, put information for the next animation in a dictionary
// and add that dictionary to the action queue.
// If we're not in the middle of an animation, just do the animation
if (animatingPlayer)
{
NSDictionary* actionInfo = [NSDictionary dictionaryWithObjectsAndKeys:
[NSValue valueWithCGRect:nextFrame], #"nextFrame",
#"move", #"actionType",
#"player", #"actionTarget",
nil];
[actionQueue pushObject:actionInfo];
}
else
{
animatingPlayer = YES; // note that we are now doing an animation
[self doMoveAnimation:nextFrame];
}
}
// Handle the event noting that the player rotated
- (void) rotateEventHandler: (NSNotification *) notification
{
// User info in the notification notes the direction of the rotation in a RotateDirection enum
NSDictionary* userInfo = [notification userInfo];
NSNumber* rotateNumber = [userInfo valueForKey:#"rotateDirection"];
// Did not provide the getRectForPlayer:onMazeView code--not needed for the example. But this
// determines where the player should be in the model when this notification is captured
CGRect nextFrame = [PlayerView getRectForPlayer:self.representedPlayer onMazeView:self.mazeView];
if (animatingPlayer)
{
NSDictionary* actionInfo = [NSDictionary dictionaryWithObjectsAndKeys:
[NSValue valueWithCGRect:nextFrame], #"nextFrame",
#"rotate", #"actionType",
rotateNumber, #"rotateDirectionNumber",
#"player", #"actionTarget",
nil];
[actionQueue pushObject:actionInfo];
}
else
{
enum RotateDirection direction = (enum RotateDirection) [rotateNumber intValue];
animatingPlayer = YES;
[self doRotateAnimation:nextFrame inDirection:direction];
}
}
// ... Removed other action event handlers not needed for this example
// Perform the actual animation for the move action
- (void) doMoveAnimation:(CGRect) nextFrame
{
[UIView beginAnimations:#"Move" context:NULL];
[UIView setAnimationDuration:actionDuration];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:#selector(animationDidStop:finished:context:)];
self.frame = nextFrame;
[UIView commitAnimations];
}
// Perform the actual animation for the rotate action
- (void) doRotateAnimation:(CGRect)nextFrame inDirection:(enum RotateDirection)rotateDirection
{
int iRot = +1;
if (rotateDirection == CounterClockwise)
{
iRot = -1;
}
[UIView beginAnimations:#"Rotate" context:NULL];
[UIView setAnimationDuration:(3*actionDuration)];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:#selector(animationDidStop:finished:context:)];
CGAffineTransform oldTransform = self.transform;
CGAffineTransform transform = CGAffineTransformRotate(oldTransform,(iRot*M_PI/2.0));
self.transform = transform;
self.frame = nextFrame;
[UIView commitAnimations];
}
- (void) animationDidStop:(NSString*)animationID
finished:(BOOL)finished
context:(void *)context
{
// If we're stopping animations, clear the queue, put the player where it needs to go
// and reset stoppingAnimations to NO and note that the player is not animating
if (self.stoppingAnimation)
{
[actionQueue removeAllObjects];
[self placePlayer];
self.stoppingAnimation = NO;
self.animatingPlayer = NO;
}
else if ([actionQueue count] > 0) // there is an action in the queue, execute it
{
NSDictionary* actionInfo = (NSDictionary*)[actionQueue popObject];
NSString* actionTarget = (NSString*)[actionInfo valueForKey:#"actionTarget"];
NSString* actionType = (NSString*)[actionInfo valueForKey:#"actionType"];
// For actions to the player...
if ([actionTarget isEqualToString:#"player"])
{
NSValue* rectValue = (NSValue*)[actionInfo valueForKey:#"nextFrame"];
CGRect nextFrame = [rectValue CGRectValue];
if ([actionType isEqualToString:#"move"])
{
[self doMoveAnimation:nextFrame];
}
else if ([actionType isEqualToString:#"rotate"])
{
NSNumber* rotateNumber = (NSNumber*)[actionInfo valueForKey:#"rotateDirectionNumber"];
enum RotateDirection direction = (enum RotateDirection) [rotateNumber intValue];
[self doRotateAnimation:nextFrame inDirection:direction];
}
// ... Removed code not needed for this example
}
else if ([actionTarget isEqualToString:#"cell"])
{
// ... Removed code not needed for this example
}
}
else // no more actions in the queue, mark the animation as done
{
animatingPlayer = NO;
[[NSNotificationCenter defaultCenter]
postNotificationName:[PlayerView AnimationsDidStopNotification]
object:self
userInfo:[NSDictionary dictionaryWithObjectsAndKeys: nil]];
}
}
// Make animations stop after current animation by setting stopAnimation = YES
- (void) stopAnimation
{
if (self.animatingPlayer)
{
self.stoppingAnimation = YES;
}
}
- (void)dealloc {
if (representedPlayer != nil)
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
[representedPlayer release];
[actionQueue release];
// …Removed other code not needed for example
[super dealloc];
}
#end
Explanation:
The view subscribes to appropriate notifications from the model object (player). When it captures a notification, it checks to see if it is already doing an animation (using the animatingPlayer property). If so, it takes the information from the notification (noting how the player is supposed to be animated), puts that information into a dictionary, and adds that dictionary to the animation queue. If there is no animation currently going, the method will set animatingPlayer to true and call an appropriate do[Whatever]Animation routine.
Each do[Whatever]Animation routine performs the proper animations, setting a setAnimationDidStopSelector to animationDidStop:finished:context:. When each animation finishes, the animationDidStop:finished:context: method (after checking whether all animations should be stopped immediately) will perform the next animation in the queue by pulling the next dictionary out of the queue and interpreting its data in order to call the appropriate do[Whatever]Animation method. If there are no animations in the queue, that routine sets animatingPlayer to NO and posts a notification so other objects can know when the player has appropriately stopped its current run of animations.
That's about it. There may be a simpler method (?) but this worked pretty well for me. Check out my Mazin app in the App Store if you're interested in seeing the actual results.
Thanks.
You should think about providing multiple points along the animation path in an array as shown below.
The example below specifies multiple points along the y-axis but you can also specify a bezier path that you want your animation to follow.
The main difference between basic animation and key-frame animation is that Key-frame allows you to specify multiple points along the path.
CAKeyframeAnimation *downMoveAnimation;
downMoveAnimation = [CAKeyframeAnimation animationWithKeyPath:#"transform.translation.y"];
downMoveAnimation.duration = 12;
downMoveAnimation.repeatCount = 1;
downMoveAnimation.values = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:20],
[NSNumber numberWithFloat:220],
[NSNumber numberWithFloat:290], nil];
downMoveAnimation.keyTimes = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0],
[NSNumber numberWithFloat:0.5],
[NSNumber numberWithFloat:1.0], nil];
downMoveAnimation.timingFunctions = [NSArray arrayWithObjects:
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn],
// from keyframe 1 to keyframe 2
[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut], nil];
// from keyframe 2 to keyframe 3
downMoveAnimation.removedOnCompletion = NO;
downMoveAnimation.fillMode = kCAFillModeForwards;