Animating an object in Cocos2d error - animation

I am trying to animate an object in cocos2d and then remove it from the layer once the animation is completed but due to being a complete novice the below is not working for me.
I have two states kStateDead and kStateScore that once called will perform an animation and then be removed from the scene but this is not currently happening
I have checked the animations and they all work so I added a debug label to the object and it does change state to either kStateDead or kStateScore. I also added a CCLOG to the states and have found that in the debug area it keeps repeating the CCLOG that that is in either kStateDead or kStateScore (but does not play the animation) which makes no sense as I have asked it to remove and clean up the object after the animation has played.
Please see below for the output from the console and also the code. Any help to push me in the right direction (don't expect you to solve it for me!) would be greatly appreciated.
2014-02-20 14:29:14.088 TestBallexercise[999:12c03] ball->change state to dead
2014-02-20 14:29:14.089 TestBallexercise[999:12c03] ball->change state to dead
2014-02-20 14:29:14.090 TestBallexercise[999:12c03] ball->change state to dead
2014-02-20 14:29:14.104 TestBallexercise[999:12c03] ball->change state to dead
2014-02-20 14:29:14.105 TestBallexercise[999:12c03] ball->change state to dead
2014-02-20 14:29:14.106 TestBallexercise[999:12c03] ball->change state to dead
-(void)changeState:(CharacterStates)newState {
[self stopAllActions];
[self setCharacterState:newState];
CGSize screenSize1 = [CCDirector sharedDirector].winSize;
characterState = newState;
id action = nil;
switch (newState) {
case kStateScore:
CCLOG(#"ball - score!");
action = [CCSequence actions:
[CCAnimate actionWithAnimation:scoreAnim
restoreOriginalFrame:NO],
[CCDelayTime actionWithDuration:3.5f],
[CCFadeOut actionWithDuration:0.9f],
nil];
break;
case kStateDead:
CCLOG(#"ball->change state to dead");
action = [CCSequence actions:
[CCAnimate actionWithAnimation:deadAnim
restoreOriginalFrame:NO],
[CCDelayTime actionWithDuration:3.5f],
[CCFadeOut actionWithDuration:0.9f],
nil];
break;
}
if (action !=nil)
[self runAction:action];
}
-(void)updateStateWithDeltaTime:(ccTime)deltaTime andListOfGameObjects:(CCArray *)listOfGameObjects {
CGPoint currentSpitePosition = [self position];
CGRect ballBoundingBox = [self adjustedBoundingBox];
if ((currentSpitePosition.x > screenSize3.width*0.95f)) {{
[self changeState:kStateScore];
}
return;
}
if ((currentSpitePosition.y < screenSize3.height*0.10f)) {{
[self changeState:kStateDead];
}
return;
}
if ([self numberOfRunningActions] == 0) {
if (characterState == kStateDead) {
[self setVisible:NO];
[self removeFromParentAndCleanup:YES];
return;
}
if (characterState == kStateScore) {
// [self changeState:kStateDead];
[self setVisible:NO];
[self removeFromParentAndCleanup:YES];
return;
}}}

The update function is called repeatedly with a certain time interval. Once the criteria for lets say, kStateDead is met, changeState is called and update function returns. Inside the changeState function the action would start but meanwhile update function is called again where the condition for kStateDead is still true and therefore changeState is called again which stopsAllActions in the first line. Thus, making it seem that the sprite is not animating. The code never reaches the cleanup code block in the update function.
There are several ways you could work around this problem. Either modify the condition like
if ((currentSpitePosition.y < screenSize3.height*0.10f) && self.characterState != kStateDead) //this will ensure that once dead, changeState to dead is not called again.
However, I would suggest since you want the object to be removed after the animation completes, it would be a good idea to add a callback to let you know the animation has completed and execute the cleanup code in the callback. This could be done by
action = [CCSequence actions:
[CCAnimate actionWithAnimation:deadAnim
restoreOriginalFrame:NO],
[CCDelayTime actionWithDuration:3.5f],
[CCFadeOut actionWithDuration:0.9f],
[CCCallFunc actionWithTarget:self selector:#selector(removeSelf)],
nil];
and you could do the clean up in the removeSelf function
-(void) removeSelf{
//your clean up code
}

Related

NSTableView reloadData method causes NSProgressIndicator in all rows to update and flicker

Note: I have searched Stack Overflow for similar problems, and none of the questions I found seem to address this particular problem.
I've written a small sample app (complete Xcode project with source code available here: http://jollyroger.kicks-ass.org/stackoverflow/FlickeringTableView.zip) that plays all of the sounds in /System/Library/Sounds/ sequentially and displays the sounds in a window as they are played to show the issue I am seeing. The window in MainMenu.xib has a single-column NSTableView with one row defined as a cell template with three items in it:
an NSTextField to hold the sound name
another NSTextField to hold the sound details
a NSProgressIndicator to show play progress while the sound is playing
I have subclassed NSTableCellView (SoundsTableCellView.h) to define each of the items in the cell view so that I can access and set them when the time arises.
I have defined a MySound class that encapsulates properties and methods needed to handle the playing of sound files via AVAudioPlayer APIs. This class defines a MySoundDelegate protocol to allow the app delegate to receive events whenever sounds start or finish playing.
The application delegate adheres to the NSTableViewDelegate and NSTableViewDataSource protocols to allow it to store the table data as an array of MySound objects and update the table with relevant information when needed. It also adheres to the MySoundDelegate protocol to receive events when sounds start or finish playing. The delegate also has an NSTimer task that periodically calls a refreshWindow method to update the progress indicator for the currently playing sound.
The app delegate's refreshWindow method displays and resizes the window if needed based on the number of sounds in the list, and updates the stored reference to the associated NSProgressIndicator for the sound that is playing.
The app delegate's tableView: viewForTableColumn (NSTableViewDelegate protocol) method gets called to populate the table cells. In it, I use Apple's standard "Populating a Table View Programmatically" advice to:
check the table column identifier to ensure it matches the
identifier (sound column) I set in Interface Builder (Xcode) for the table column,
get the corresponding table cell with identifier (sound cell) by calling thisTableView makeViewWithIdentifier,
use the incoming row parameter to locate the matching array element
of the data source (app delegate sounds array), then
set the string values of NSTextFields and set the maxValue and doubleValue of the NSProgressIndicator in the cell to corresponding details of the associated sound object,
store a reference to the associated NSProgressIndicator control in the associated sound object for later updating
Here's the viewForTableColumn method:
- (NSView *)tableView:(NSTableView *)thisTableView viewForTableColumn:(NSTableColumn *)thisTableColumn row:(NSInteger)thisRow
{
SoundsTableCellView *cellView = nil;
// get the table column identifier
NSString *columnID = [thisTableColumn identifier];
if ([columnID isEqualToString:#"sound column"])
{
// get the sound corresponding to the specified row (sounds array index)
MySound *sound = [sounds objectAtIndex:thisRow];
// get an existing cell from IB with our hard-coded identifier
cellView = [thisTableView makeViewWithIdentifier:#"sound cell" owner:self];
// display sound name
[cellView.soundName setStringValue:[sound name]];
[cellView.soundName setLineBreakMode:NSLineBreakByTruncatingMiddle];
// display sound details (source URL)
NSString *details = [NSString stringWithFormat:#"%#", [sound sourceURL]];
[cellView.soundDetails setStringValue:details];
[cellView.soundDetails setLineBreakMode:NSLineBreakByTruncatingMiddle];
// update progress indicators
switch ([sound state])
{
case kMySoundStateQueued:
break;
case kMySoundStateReadyToPlay:
break;
case kMySoundStatePlaying:
if (sound.playProgress == nil)
{
sound.playProgress = cellView.playProgress;
}
NSTimeInterval duration = [sound duration];
NSTimeInterval position = [sound position];
NSLog(#"row %ld: %# (%f / %f)", (long)thisRow, [sound name], position, duration);
NSLog(#" %#: %#", [sound name], sound.playProgress);
[cellView.playProgress setMaxValue:duration];
[cellView.playProgress setDoubleValue:position];
break;
case kMySoundStatePaused:
break;
case kMySoundStateFinishedPlaying:
break;
default:
break;
}
}
return cellView;
}
And here's the refreshWindow method:
- (void) refreshWindow
{
if ([sounds count] > 0)
{
// show window if needed
if ([window isVisible] == false)
{
[window makeKeyAndOrderFront:self];
}
// resize window to fit all sounds in the list if needed
NSRect frame = [self.window frame];
int screenHeight = self.window.screen.frame.size.height;
long maxRows = ((screenHeight - 22) / 82) - 1;
long displayedRows = ([sounds count] > maxRows ? maxRows : [sounds count]);
long actualHeight = frame.size.height;
long desiredHeight = 22 + (82 * displayedRows);
long delta = desiredHeight - actualHeight;
if (delta != 0)
{
frame.size.height += delta;
frame.origin.y -= delta;
[self.window setFrame:frame display:YES];
}
// update play position of progress indicator for all sounds in the list
for (MySound *nextSound in sounds)
{
switch ([nextSound state])
{
case kMySoundStatePlaying:
if (nextSound.playProgress != nil)
{
[nextSound.playProgress setDoubleValue:[nextSound position]];
NSLog(#" %#: %# position: %f", [nextSound name], nextSound.playProgress, [nextSound position]);
}
break;
case kMySoundStateQueued:
case kMySoundStateReadyToPlay:
case kMySoundStatePaused:
case kMySoundStateFinishedPlaying:
default:
break;
}
}
}
else
{
// hide window
if ([window isVisible])
{
[window orderOut:self];
}
}
// reload window table view
[tableView reloadData];
}
During init, the application delegate scans the /System/Library/Sounds/ folder to get a list of AIFF sound files in that folder, and creates a sounds array holding sound objects for each of the sounds in that folder. The applicationDidFinishLaunching method then starts playing the first sound in the list sequentially.
The problem (which you can see by running the sample project) is that rather than only updating the top table row for the sound that is currently playing, the progress indicators in all of the following rows seem to update and flicker as well. The way it displays is somewhat inconsistent (sometimes they all flicker, and sometimes they are all blank as expected); but when they do update and flicker the progress indicators do seem to roughly correspond to the sound that is currently playing. So I am pretty sure the issue must be somehow related to the way I am updating the table; I'm just not sure where the problem is or how to solve it.
Here's a screen shot of what the window looks like to give you an idea:
Table View Screen Shot
Any ideas or guidance would be greatly appreciated!
Here's what I changed.
tableView:viewForTableColumn:row: returns "a view to display the specified row and column". The value of the progress bar is always set.
- (NSView *)tableView:(NSTableView *)thisTableView viewForTableColumn:(NSTableColumn *)thisTableColumn row:(NSInteger)thisRow
{
SoundsTableCellView *cellView = nil;
// get the table column identifier
NSString *columnID = [thisTableColumn identifier];
if ([columnID isEqualToString:#"sound column"])
{
// get the sound corresponding to the specified row (sounds array index)
MySound *sound = [sounds objectAtIndex:thisRow];
// get an existing cell from IB with our hard-coded identifier
cellView = [thisTableView makeViewWithIdentifier:#"sound cell" owner:self];
// display sound name
[cellView.soundName setStringValue:[sound name]];
// display sound details (source URL)
NSString *details = [NSString stringWithFormat:#"%#", [sound sourceURL]];
[cellView.soundDetails setStringValue:details];
// update progress indicators
// [cellView.playProgress setUsesThreadedAnimation:NO];
NSTimeInterval duration = [sound duration];
NSTimeInterval position = [sound position];
[cellView.playProgress setMaxValue:duration];
[cellView.playProgress setDoubleValue:position];
}
// end updates
// [thisTableView endUpdates];
return cellView;
}
refreshWindow is split into refreshProgress and refreshWindow. refreshProgress refreshes the row of the playing sound and is called on a timer.
- (void)refreshProgress
{
if ([sounds count] > 0)
{
[sounds enumerateObjectsUsingBlock:^(MySound *nextSound, NSUInteger rowNr, BOOL *stop)
{
switch ([nextSound state])
{
case kMySoundStatePlaying:
// refresh row
[tableView reloadDataForRowIndexes:[NSIndexSet indexSetWithIndex:rowNr]
columnIndexes:[NSIndexSet indexSetWithIndex:0]];
break;
case kMySoundStateQueued:
case kMySoundStateReadyToPlay:
case kMySoundStatePaused:
case kMySoundStateFinishedPlaying:
default:
break;
}
}];
}
}
refreshWindow refreshes the size and visibility of the window and is called when the number of sounds changes.
- (void) refreshWindow
{
if ([sounds count] > 0)
{
// show window if needed
if ([window isVisible] == false)
{
[window makeKeyAndOrderFront:self];
}
// resize window to fit all sounds in the list if needed
... calculate new window frame
}
else
{
// hide window
if ([window isVisible])
{
[window orderOut:self];
}
}
}
When a sound is removed, the row is also removed so the other rows still display the same sound and don't need an update.
- (void) soundFinishedPlaying:(MySound *)sound encounteredError:(NSError *)error
{
if (error != NULL)
{
// display an error dialog box to the user
[NSApp presentError:error];
}
else
{
// remove sound from array
NSLog(#"deleting: [%#|%#]", [sound truncatedID], [sound name]);
NSUInteger index = [sounds indexOfObject:sound];
[sounds removeObject:sound];
[tableView removeRowsAtIndexes:[NSIndexSet indexSetWithIndex:index] withAnimation:NSTableViewAnimationEffectNone];
}
// refresh window
[self refreshWindow];
// play the next sound in the queue
[self play];
}
[tableView reloadData] isn't called. sound.playProgress isn't used.

objective c WatchKit WKInterfaceController openParentApplication call blocks indefinitely

I'm using the following code to "simply" determine the application state of the parent application from my watch app:
WatchKit Extension:
[WKInterfaceController openParentApplication:[NSDictionary dictionary] reply:^(NSDictionary *replyInfo, NSError *error)
{
UIApplicationState appState = UIApplicationStateBackground;
if(nil != replyInfo)
appState = (UIApplicationState)[((NSNumber*)[replyInfo objectForKey:kAppStateKey]) integerValue];
//handle app state
}];
Main App:
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *replyInfo))reply
{
__block UIBackgroundTaskIdentifier realBackgroundTask;
realBackgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
reply([NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:[[UIApplication sharedApplication] applicationState]], kAppStateKey, nil]);
[[UIApplication sharedApplication] endBackgroundTask:realBackgroundTask];
}];
reply([NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:[[UIApplication sharedApplication] applicationState]], kAppStateKey, nil]);
[[UIApplication sharedApplication] endBackgroundTask:realBackgroundTask];
}
When the app is in the foreground this works 100% of the time. When the app is "minimized" or "terminated" this maybe works 50% of the time (maybe less). When it doesn't work it appears to be blocking indefinitely. If after 1 minute, for example, I launch the parent app, the call (openParentApplication) immediately returns with the state "UIApplicationStateBackground" (the state it was before I launched the app as clearly the app isn't in the background state if I launched it).
BTW: I'm testing with real hardware.
What am I doing wrong? Why is iOS putting my main app to sleep immediately after receiving the call even though I create a background task? This is a complete show-stopper.
Any thoughts or suggestions would be greatly appreciated!!
After some research it looks to be a known issue. For example, the following link identifies this issue and provides a solution:
http://www.fiveminutewatchkit.com/blog/2015/3/11/one-weird-trick-to-fix-openparentapplicationreply
However, this solution did not work for me. As a result I implemented the following solution (its a little sloppy, but this is intentional to help condense the solution):
//start the timeout timer
timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:kTimeOutTime target:self selector:#selector(onTimeout) userInfo:nil repeats:NO];
//make the call
messageSent = [WKInterfaceController openParentApplication:[NSDictionary dictionary] reply:^(NSDictionary *replyInfo, NSError *error)
{
if(nil != _stateDelegate)
{
UIApplicationState appState = UIApplicationStateBackground;
if(nil != replyInfo)
appState = (UIApplicationState)[((NSNumber*)[replyInfo objectForKey:kAppStateKey]) integerValue];
[_stateDelegate onOperationComplete:self timeout:false applicationState:appState];
_stateDelegate = nil;
}
}];
//if the message wasn't sent, then this ends now
if(!messageSent)
{
if(nil != _stateDelegate)
{
//just report that the main application is inactive
[_stateDelegate onOperationComplete:self timeout:false applicationState:UIApplicationStateInactive];
}
_stateDelegate = nil;
}
-(void)onTimeout
{
timeoutTimer = nil;
if(nil != _stateDelegate)
{
[_stateDelegate onOperationComplete:self timeout:true applicationState:UIApplicationStateInactive];
}
_stateDelegate = nil;
}
In a nutshell, if the timer fires before I hear back from the main app I will basically assume that the main app has been put to sleep. Keep in mind that all pending calls will succeed at some point (e.g. app state is restored to active) and, thus, you will need to handle this scenario (if necessary).

How to avoid asynchronous nature of scrollToVisibleRect causing intermittent timing issues

I am writing a Cocoa UI and am calling NSView's scrollRectToVisible repeatedly in a short space of time as a result of the user holding down a certain key (and hence repeatedly firing events on the main queue).
Through logging I can see that successive keyDown() events are firing prior to the scrollRectToVisible having completed its prior changing of the visibleRect. This is resulting in the subsequent scrollRectToVisible being called with incorrect inputs (namely the wrong starting visibleRect) and is leading to non-sensical UI behaviour. This happens about 50% of the time which I guess is to be expected dealing with an asynchronous problem.
How can I address this?
One way I can think of is by somehow turning scrollRectToVisible into a synchronous call. The problem is that it is an AppKit API and the method returns immediately with a boolean indicating whether it is going to scroll or not.
Try putting the code in the view that responds to the -keyDown:.
This example controls the scrolling with a timer instead of the repeated keyDowns from holding the key.
I couldn't get the UI unwanted behavior, so I don't know if this will help.
- (BOOL)acceptsFirstResponder {
return YES;
}
- (BOOL)canBecomeKeyView {
return YES;
}
- (void)keyDown:(NSEvent *)theEvent {
if (timer == nil) {
timer = [NSTimer scheduledTimerWithTimeInterval:0.01
target:self
selector:#selector(doScroll:)
userInfo:nil
repeats:YES];
}
NSLog(#"event %# with Timer%#", theEvent, timer);
}
- (void)keyUp:(NSEvent *)theEvent {
[timer invalidate];
timer = nil;
NSLog(#"keyUp timer:%#", timer);
NSLog(#"event %#", theEvent);
}
- (void)doScroll:theTimer {
NSEvent * current = [NSApp currentEvent]; //use this to parse
NSLog(#"current press %#", current);
[self scrollRectToVisible:NSMakeRect(self.visibleRect.origin.x + 10,
self.visibleRect.origin.y + 10,
self.bounds.size.width,
self.bounds.size.height)];
}

RunAction,CCCallFuncN with multiple Variables - Cocos2D

Is there any way you could send 2 parameters with RunAction?
You see im trying to move a sprite with a label on top, and I have made separate functions for each. Similar to this.
[sprite runAction:
[CCSequence actions:actionMove, actionMoveDone, nil]];
id actionMoveDone = [CCCallFuncN actionWithTarget:self
selector:#selector(spriteLabelMoveFinished:)];
Now, I've got 2 questions,
1-Is there any way to send 2 or more parameters????
2-I was wondering if there's any way to save some memory and do both with one action?
- (void) spriteMoveFinished:(id)sender
{
CCLOG(#"Sprite move finished");
Sprites *sprite = (Sprites *)sender;
[self animateSprite:sprite];
}
- (void) animateSprite:(Sprites *)zprite
{
CCLOG(#"We're animating sprite"):
Sprites *sprite = nil;
sprite = zprite;
int actualDuration = sprite.speed; //property of sprite
// Create the actions
id actionMove = [CCMoveBy actionWithDuration:actualDuration
position:ccpMult(ccpNormalize(ccpSub(_player.position,sprite.position)), 10)];
id actionMoveDone = [CCCallFuncN actionWithTarget:self
selector:#selector(spriteMoveFinished:)];
[sprite runAction:
[CCSequence actions:actionMove, actionMoveDone, nil]];
}
- (void) spriteLabelMoveFinished:(CCLabelTTF *)sender
{
[self animateSpriteLabel:sender];
}
-(void)animateEnemyHP:(CCLabelTTF *)zpriteLabel
{
CCLabelTTF *spriteLabel = nil;
spriteLabel = zpriteLabel;
int actualDuration = spriteSpeed; //another property
id actionMove = [CCMoveBy actionWithDuration:actualDuration
position:ccpMult(ccpNormalize(ccpSub(_player.position,spriteLabel.position)), 10)];
id actionMoveDone = [CCCallFuncN actionWithTarget:self
selector:#selector(spriteLabelMoveFinished:)];
[spriteLabel runAction:
[CCSequence actions:actionMove, actionMoveDone, nil]];
}
Now, this 4 functions are kind of obvious.
1-Move Sprite if sprite ended moving, we move it again.
2-Move Label towards the same position with the same speed, if the label finishes moving, we move it again.
They both go to the same place.
Is there a way to mix this 4 functions into 2?
If so, how can I send 2 parameters when the action finishes?
Thanks for your help and your time, have a great day!
To send more than one parameter, instead CCCallFunc you can use CCCallBlock and call a block of code. Inside the block, you can call your method (selector) with any parameters you want.
A simple example:
CCAction *actionMoveDone = [CCCallBlock actionWithBlock:^()
{
[self spriteMoveFinished:param1 withParam2:param2 andParam3:param3];
}];
And a better way is use "self" as block parameter, to prevent memory allocation:
CCAction *actionMoveDone = [CCCallBlockN actionWithBlock:^(CCNode *myNode)
{
[(MyClass*)myNode spriteMoveFinished:param1 withParam2:param2 andParam3:param3];
}];
try to simplify, that will undoubtedly save 'memory' ... I normally do this with a class that extends CCNode, for example my SoldierMapLayout class does. In the SoldierMapLayout node, i put in the soldier stance animation (idle, walking right-left-up-down), a health bar, an optional label, a 'HP hit' animation when appropriate, an 'XP gained' animation when appropriate, a 'poison cloud' over the head, etc... and i move the node when the whole thing needs to move. A single animation, with a single call back on move completion.

cursorUpdate called, but cursor not updated

I have been working on this for hours, have no idea what went wrong. I want a custom cursor for a button which is a subview of NSTextView, I add a tracking area and send the cursorUpdate message when mouse entered button.
The cursorUpdate method is indeed called every time the mouse entered the tracking area. But the cursor stays the IBeamCursor.
Any ideas?
Reference of the Apple Docs: managing cursor-update event
- (void)cursorUpdate:(NSEvent *)event {
[[NSCursor arrowCursor] set];
}
- (void)myAddTrackingArea {
[self myRemoveTrackingArea];
NSTrackingAreaOptions trackingOptions = NSTrackingCursorUpdate | NSTrackingMouseEnteredAndExited | NSTrackingActiveInKeyWindow;
_trackingArea = [[NSTrackingArea alloc] initWithRect: [self bounds] options: trackingOptions owner: self userInfo: nil];
[self addTrackingArea: _trackingArea];
}
- (void)myRemoveTrackingArea {
if (_trackingArea)
{
[self removeTrackingArea: _trackingArea];
_trackingArea = nil;
}
}
I ran into the same problem.
The issue is, that NSTextView updates its cursor every time it receives a mouseMoved: event. The event is triggered by a self updating NSTrackingArea of the NSTextView, which always tracks the visible part of the NSTextView inside the NSScrollView. So there are maybe 2 solutions I can think of.
Override updateTrackingAreas remove the tracking area that is provided by Cocoa and make sure you always create a new one instead that excludes the button. (I would not do this!)
Override mouseMoved: and make sure it doesn't call super when the cursor is over the button.
- (void)mouseMoved:(NSEvent *)theEvent {
NSPoint windowPt = [theEvent locationInWindow];
NSPoint superViewPt = [[self superview]
convertPoint: windowPt fromView: nil];
if ([self hitTest: superViewPt] == self) {
[super mouseMoved:theEvent];
}
}
I had the same issue but using a simple NSView subclass that was a child of the window's contentView and did not reside within an NScrollView.
The documentation for the cursorUpdate flag of NSTrackingArea makes it sound like you only need to handle the mouse entering the tracking area rect. However, I had to manually check the mouse location as the cursorUpdate(event:) method is called both when the mouse enters the tracking area's rect and when it leaves the tracking rect. So if the cursorUpdate(event:) implementation only sets the cursor without checking whether it lies within the tracking area rect, it is set both when it enters and leaves the rect.
The documentation for cursorUpdate(event:) states:
Override this method to set the cursor image. The default
implementation uses cursor rectangles, if cursor rectangles are
currently valid. If they are not, it calls super to send the message
up the responder chain.
If the responder implements this method, but decides not to handle a
particular event, it should invoke the superclass implementation of
this method.
override func cursorUpdate(with event: NSEvent) {
// Convert mouse location to the view coordinates
let mouseLocation = convert(event.locationInWindow, from: nil)
// Check if the mouse location lies within the rect being tracked
if trackingRect.contains(mouseLocation) {
// Set the custom cursor
NSCursor.openHand.set()
} else {
// Reset the cursor
super.cursorUpdate(with: event)
}
}
I just ran across this through a Google search, so I thought I'd post my solution.
Subclass the NSTextView/NSTextField.
Follow the steps in the docs to create an NSTrackingArea. Should look something like the following. Put this code in the subclass's init method (also add the updateTrackingAreas method):
NSTrackingArea *trackingArea = [[NSTrackingArea alloc] initWithRect:self.bounds options:(NSTrackingMouseMoved | NSTrackingActiveInKeyWindow) owner:self userInfo:nil];
[self addTrackingArea:trackingArea];
self.trackingArea = trackingArea;
Now you need to add the mouseMoved: method to the subclass:
- (void)mouseMoved:(NSEvent *)theEvent {
NSPoint point = [self convertPoint:theEvent.locationInWindow fromView:nil];
if (NSPointInRect(point, self.popUpButton.frame)) {
[[NSCursor arrowCursor] set];
} else {
[[NSCursor IBeamCursor] set];
}
}
Note: the self.popUpButton is the button that is a subview of the NSTextView/NSTextField.
That's it! Not too hard it ends up--just had to used mouseMoved: instead of cursorUpdate:. Took me a few hours to figure this out, hopefully someone can use it.

Resources