I have a subclass of NSTextField that I made so that when a user is done editing the field, the text field will lose focus. I also have it set up so whenever the user clicks on the main view, this will act as losing focus on the textfield. And this all works great. Now I want to add some additional capabilities to the subclass.
I want the textfield to send a textDidEndEditing every time a user clicks anywhere outside of the box. This includes when a user clicks on another UI component. The behavior I'm seeing right now is that when a user clicks on another UI component (let's say a combo box) the action does not trigger. Is there a way to force this? Besides manually adding it as a part of the other components actions?
Any help would be appreciated!
Here's the code for my textDidEndEditing function
- (void)textDidEndEditing:(NSNotification *)notification
{
NSString *file = nil;
char c = ' ';
int index = 0;
[super textDidEndEditing:notification];
if ([self isEditable])
{
// is there a valid string to display?
file = [self stringValue];
if ([file length] > 0)
{
c = [file characterAtIndex:([file length] - 1)];
if (c == '\n') // check for white space at the end
{
// whitespace at the end... remove
NSMutableString *newfile = [[NSMutableString alloc] init];
c = [file characterAtIndex:index++];
do
{
[newfile appendFormat:#"%c", c];
c = [file characterAtIndex:index++];
}
while ((c != '\n') && (index < [file length]));
[self setStringValue:newfile];
file = newfile;
}
[[NSNotificationCenter defaultCenter]
postNotificationName:#"inputFileEntered" object:self];
}
}
// since we're leaving this box, show no text in this box as selected.
// and deselect this box as the first responder
[self setSelectedText:0];
[[NSNotificationCenter defaultCenter]
postNotificationName:#"setResponderToNil" object:self];
}
Where "setSelectedText" is a public function in the text field subclass:
- (void)setSelectedText:(int) length
{
int start = 0;
NSText *editor = [self.window fieldEditor:YES forObject:self];
NSRange range = {start, length};
[editor setSelectedRange:range];
}
And the "setResponderToNil" notification is a part of my NSView subclass:
- (void)setResponderToNil
{
AppDelegate *delegate = (AppDelegate *)[NSApp delegate];
[delegate.window makeFirstResponder:nil];
}
I think I found a way to do this. It may not be the most eloquent, but it seems to work with the type of behavior I want.
I added an mouse event listener to the app's main controller:
event_monitor_mousedown_ = [NSEvent addLocalMonitorForEventsMatchingMask:NSRightMouseDown
handler:^NSEvent *(NSEvent * event)
{
NSResponder *resp = [[[NSApplication sharedApplication] keyWindow] firstResponder];
if ([resp isKindOfClass:[NSTextView class]])
{
// set UI in proper state - remove focus from text field
// even when touching a new window for the first time
[[NSNotificationCenter defaultCenter]
postNotificationName:#"setResponderToNil" object:self];
[self setStopState];
}
return event;
}];
This event checks the current responder in the application on any mouseDown action. If it's a textView object (which is type of the object that would be the first responder when editing an NSTextField) it will send the notification to set the firstResponder to nil. This forces the textDidEndEditing notification. I want to play around with it some more to see if I'm getting the right expected behavior. I hope this helps someone out there!
Related
I am trying to do Error Checking for a Correct Time in a UItextField. I didn't want an error to pop up while the user is typing (To give them the chance to fix it first) so I thought I would do the Check during an EditingDidEnd Action. I setup ones for Hours, Minutes and Seconds. Here is the code I tried for inHours:
- (IBAction)inHourEditEnd:(id)sender
{
// Check to make sure value is between 0 & 23 Hours
if ([[[self inHour] text] intValue] >= 0 && [[[self inHour] text] intValue] < 24) {
[self updateDuration];
} else {
NSString *errorDescription = [NSString stringWithFormat:#"%# hours is not a valid start time. Please enter an hour between 0 and 23", [[self inHour] text]];
UIAlertView *errorMessage = [[UIAlertView alloc] initWithTitle:#"Ivalid Hour for Start Time"
message:errorDescription
delegate:self
cancelButtonTitle:#"OK"
otherButtonTitles:nil];
[errorMessage show];
[_inHour becomeFirstResponder];
}
}
The alert works fine but it won't go back to the textfield (inHour) after showing the alert.
It just stays on whatever textField I tapped to cause EditingDidEnd. Searching here I found a way to make the alertView send the user back to the right textbox using this code:
- (void)alertView:(UIAlertView *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
{
//Checks For Approval
if (buttonIndex == 0) {
[_inHour becomeFirstResponder];
}
}
But this will only work for the first box and I need to make it work with inHour, inMinute and inSeconds.
Any Suggestions how I can make this work?
Is either one of these paths in the right direction?
Thank you for any help.
If you really want to do your validation after the text field has already lost first responder status, you could add a property to your view controller to track the text field that needs to receive becomeFirstResponder:
// in your .m file, declare a class extension for "private" members
#interface MyViewController ()
#property (weak, nonatomic) UITextField *lastValidationFailureField;
#end
// add this to inHourEditEnd:
self.lastValidationFailureField = _inHour;
// use this property in alertView:clickedButtonAtIndex:
[self.lastValidationFailureField becomeFirstResponder];
self.lastValidationFailureField = nil;
Alternatively, you could prevent the user from leaving the text field. Have your view controller implement UITextFieldDelegate and set all of your text fields’ delegate outlets to your view controller. Then implement textFieldShouldEndEditing:
- (BOOL)textFieldShouldEndEditing:(UITextField *)textField
{
if (textField == [self inHour]) {
if ([[textField text] intValue] >= 0 && [[textField text] intValue] < 24) {
[self updateDuration];
} else {
NSString *errorDescription = ...
UIAlertView *errorMessage = ...
[errorMessage show];
// prevent the text field from even losing first responder status
return NO;
}
}
if (textField == [some someOtherTextFieldToValidate)] {
...
}
return YES;
}
I put all of the logic in a single method here for brevity, I would recommend at least some minimal refactoring of that code if you have more than a couple of text fields to validate.
You would not need your actions method such as inHourEditEnd: any more.
I'm trying to create a column of clickable URL-type text (NOT URLs like this, but essentially a borderless, title button or text field cell with a tracking area for a hover effect) within an NSTableView.
1.) When the user hovers over a particular cell the text in that cell should draw an underline below the text (hover/trackable area effect).
2.) When the user clicks the text it should perform an action.
I've subclassed NSCell and NSTableView and added a tracking area within the custom tableview to try and track the mouse location of the individual cell of the table to notify the cell when to redraw itself. I can get the current row and column of the mouse location, but can't seem to get the right cell in my custom tableview's mouseMoved: method
-(void)mouseMoved:(NSEvent *)theEvent {
[super mouseMoved:theEvent];
NSPoint p = [self convertPoint:[theEvent locationInWindow] fromView:nil];
long column = [self columnAtPoint:p];
long row = [self rowAtPoint:p];
id cell = [[self.tableColumns objectAtIndex:column] dataCellForRow:row];
}
It gets the cell for the column, but doesn't get the right cell for that particular row. Perhaps I'm not fully understanding the dataCellForRow: function for NSTableColumn?
I know you can't quite add a tracking area for cells, but instead you must create the hit test for mouse clicks and then begin tracking once the hit test is successful (meaning the mouse is already down) and then use startTracking:, continueTracking:, and stopTracking: to get the mouse's position. The idea though is that it has a hover effect before any mouseDown: action.
Also, I can't just use a view-based tableview (which would be incredible) because my app must be 10.6 compatible.
I'm not sure what's wrong with your method of getting the cell, but you don't really need to get that to do what you want. I tested a way to do this that entailed creating a table view subclass to do the tracking in the mouse moved method. Here is the code for that subclass:
-(void)awakeFromNib {
NSTrackingArea *tracker = [[NSTrackingArea alloc] initWithRect:self.bounds options:NSTrackingMouseEnteredAndExited|NSTrackingMouseMoved|NSTrackingActiveInActiveApp owner:self userInfo:nil];
[self addTrackingArea:tracker];
self.rowNum = -1;
}
-(void)mouseMoved:(NSEvent *)theEvent {
NSPoint p = theEvent.locationInWindow;
NSPoint tablePoint = [self convertPoint:p fromView:nil];
NSInteger newRowNum = [self rowAtPoint:tablePoint];
NSInteger newColNum = [self columnAtPoint:tablePoint];
if (newColNum != self.colNum || newRowNum != self.rowNum) {
self.rowNum = newRowNum;
self.colNum = newColNum;
[self reloadData];
}
}
-(void)mouseEntered:(NSEvent *)theEvent {
[self reloadData];
}
-(void)mouseExited:(NSEvent *)theEvent {
self.rowNum = -1;
[self reloadData];
}
I put the array and table delegate and data source code in the app delegate (probably not the best place, but ok for testing).
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
self.theData = #[#{#"name":#"Tom",#"age":#"47"},#{#"name":#"Dick",#"age":#"21"},#{#"name":#"Harry",#"age":#"27"}];
[self.table reloadData];
self.dict = [NSDictionary dictionaryWithObjectsAndKeys:#2,NSUnderlineStyleAttributeName,[NSColor redColor],NSForegroundColorAttributeName,nil];
}
- (NSInteger)numberOfRowsInTableView:(RDTableView *)aTableView {
return self.theData.count;
}
- (id)tableView:(RDTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex {
if (self.table.colNum == 0 && rowIndex == self.table.rowNum && [aTableColumn.identifier isEqualToString:#"Link"]) {
NSString *theName = [[self.theData objectAtIndex:rowIndex] valueForKey:#"name"];
return [[NSAttributedString alloc] initWithString:theName attributes:self.dict];
}else if ([aTableColumn.identifier isEqualToString:#"Link"]){
return [[self.theData objectAtIndex:rowIndex] valueForKey:#"name"];
}else{
return [[self.theData objectAtIndex:rowIndex] valueForKey:#"age"];
}
}
- (void)tableViewSelectionDidChange:(NSNotification *)aNotification {
if (self.table.colNum == 0)
NSLog(#"%ld",[aNotification.object selectedRow]);
}
I use the delegate method tableViewSelectionDidChange: to implement the action if you click on a cell in the first column (which has the identifier "Link" set in IB).
I have set up an NSCollectionView in a cocoa application. I have subclassed the collection view's NSCollectionViewItem to send me a custom NSNotification when one of its views it selected / deselected. I register to receive a notification within my controller object when this notification is posted. Within this method I tell the view that has just been selected that it is selected and tell it to redraw, which makes it shade itself grey.
The NSCollectionViewItem Subclass:
-(void)setSelected:(BOOL)flag {
[super setSelected:flag];
[[NSNotificationCenter defaultCenter] postNotificationName:#"ASCollectionViewItemSetSelected"
object:nil
userInfo:[NSDictionary dictionaryWithObjectsAndKeys:(ASListView *)self.view, #"view",
[NSNumber numberWithBool:flag], #"flag", nil]];}
The Controller Class (in the -(void)awakeFromNib Method):
//Register for selection changed notification
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(selectionChanged:)
name:#"ASCollectionViewItemSetSelected"
object:nil];
And the -(void)selectionChanged:(NSNotification *)notification method:
- (void)selectionChanged:(NSNotification *)notification {
// * * Must get the selected item and set its properties accordingly
//Get the flag
NSNumber *flagNumber = [notification.userInfo objectForKey:#"flag"];
BOOL flag = flagNumber.boolValue;
//Get the view
ASListView *listView = [notification.userInfo objectForKey:#"view"];
//Set the view's selected property
[listView setIsSelected:flag];
[listView setNeedsDisplay:YES];
//Log for testing
NSLog(#"SelectionChanged to: %d on view: %#", flag, listView);}
The application that contains this code requires there to be no empty selection within the collection view at any time. This is where i get my problem. I've tried checking when a view's selection is changed and reselecting it if there is no selection, and manually selecting the views using NSCollectionView's
-(void)setSelectionIndexes:(NSIndexSet *)indexes
But there is always a situation which occurs that causes there to be an empty selection in the collection view.
So I was wondering if there is an easier way to prevent an empty selection occurring in an NSCollectionView? I see no checkbox in interface builder.
Thanks in advance!
Ben
Update
I ended up just subclassing my NSCollectionView, and overriding the - (void)mouseDown:(NSEvent *)theEvent method. I only then sent the method [super mouseDown:theEvent]; if the click was in one of the subviews. Code:
- (void)mouseDown:(NSEvent *)theEvent {
NSPoint clickPoint = [self convertPoint:theEvent.locationInWindow fromView:nil];
int i = 0;
for (NSView *view in self.subviews) {
if (NSPointInRect(clickPoint, view.frame)) {
//Click is in rect
i = 1;
}
}
//The click wasnt in any of the rects
if (i != 0) {
[super mouseDown:theEvent];
}}
I ended up just subclassing my NSCollectionView, and overriding the - (void)mouseDown:(NSEvent *)theEvent method. I only then sent the method [super mouseDown:theEvent]; if the click was in one of the subviews. Code:
- (void)mouseDown:(NSEvent *)theEvent {
NSPoint clickPoint = [self convertPoint:theEvent.locationInWindow fromView:nil];
int i = 0;
for (NSView *view in self.subviews) {
if (NSPointInRect(clickPoint, view.frame)) {
//Click is in rect
i = 1;
}
}
//The click wasnt in any of the rects
if (i != 0) {
[super mouseDown:theEvent];
}}
I also wanted to avoid empty selection in my collection view.
The way I did it is also by subclassing, but I overrode -hitTest: instead of -mouseDown: to return nil in case the click wasn't on an item :
-(NSView *)hitTest:(NSPoint)aPoint {
// convert aPoint in self coordinate system
NSPoint localPoint = [self convertPoint:aPoint fromView:[self superview]];
// get the item count
NSUInteger itemCount = [[self content] count];
for(NSUInteger itemIndex = 0; itemIndex < itemCount; itemIndex += 1) {
// test the point in each item frame
NSRect itemFrame = [self frameForItemAtIndex:itemIndex];
if(NSPointInRect(localPoint, itemFrame)) {
return [[self itemAtIndex:itemIndex] view];
}
}
// not on an item
return nil;
}
Although I'm late to this thread, I thought I'd just chime in because I've had the same problem recently. I got around it using the following line of code:
[_collectionView setValue:#NO forKey:#"avoidsEmptySelection"];
There is one caveat: the avoidsEmptySelection property is not part of the official API although I think that it's pretty safe to assume that its the type of property that will stick around for a while.
I'm having trouble receiving keyboard events within a subclass of NSView.
I can handle mouse events fine, but my keyDown and keyUp methods are never called. It was my understanding per the documentation that both types of events follow the same hierarchy, however this is seemingly not the case.
Is this a first responder issue? Some field somewhere grabbing the focus? I've tried overriding that but no luck.
Any insights would be greatly appreciated.
If you'd like to see.. this is within a custom NSView class:
#pragma mark -
#pragma mark I/O Events
-(void)keyDown:(NSEvent *)theEvent {
NSLog(#"Sup brah!");
}
-(void)keyUp:(NSEvent *)theEvent {
NSLog(#"HERE");
}
// This function works great:
-(void)mouseDown:(NSEvent *)theEvent {
NSNumber *yeah = [[NSNumber alloc] initWithBool:YES];
NSNumber *nah = [[NSNumber alloc] initWithBool:NO];
NSString *asf = [[NSString alloc] initWithFormat:#"%#", [qcView valueForOutputKey:#"Food_Out"]];
if ([asf isEqualToString:#"1"]) {
[qcView setValue:nah forInputKey:#"Food_In"];
best_food_x_loc = convertToQCX([[qcView valueForOutputKey:#"Food_1_X"] floatValue]);
best_food_y_loc = convertToQCY([[qcView valueForOutputKey:#"Food_1_Y"] floatValue]);
NSLog(#"X:%f, Y:%f",best_food_x_loc, best_food_y_loc);
} else {
[qcView setValue:yeah forInputKey:#"Food_In"];
}
}
You have to set your NSView to be first responder
- (BOOL)acceptsFirstResponder {
return YES;
}
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;