I'm making a game that involves buttons moving across the screen. When one button reaches the edge of the screen without being tapped, you lose a bar of health.
-(void) moveStickFig:(NSTimer *)timer {
UIButton *stick = (UIButton *)timer.userInfo;
CGPoint oldPosition = stick.center;
stick.center = CGPointMake(oldPosition.x + 1 , oldPosition.y);
if (oldPosition.x == 900) {
[stick removeFromSuperview];
healthCount--;
NSLog(#"%d", healthCount);
}
}
When you click on a button it disapears using [btn removeFromSuperview] The problem with this is the button still exists and continues moving across the screen. Is there a way to delete it completely? I've tried [stick release] but for some reason it just causes the app to freeze
It looks like you're using a repeating timer to move the button. If you don't explicitly end that timer, the timer is going to keep running, and moving the button.
Normally when you send the removeFromSuperview message to something like a button, it would deallocate or "delete" that object. This is because when the button is added to the superview, the superview retains the button, giving it a retain count of 1, and when it's removed from the superview, it releases it, giving it a retain count of 0.
However, because the button is stored as the userInfo of the timer, the timer also retains the object giving it a retain count of 2, and after you remove it from the superview it still has a retain count of 1. If you simply send the release message to the button, it will lower the retain count to 0 and it will deallocate the button, but it won't stop the timer. The next time the timer runs, it will cause problems because you're trying to access deallocated memory.
What you really want to do is to invalidate the timer: [timer invalidate]. This will stop the timer, and the timer will send a release message to the button, causing the button to be deallocated.
NSTimer retains its userInfo, which is the button object in your case. You should kill the timer using [timer invalidate].
Related
I am using xcode and am having a problem moving a button automatically. I have this function that whenever I call it, I expect the button to move to the coordinates that I set:
[movebutton setCenter:CGPointMake(164,50)];
Previously I tried to set an NStimer in an IBAction function and then use the timer to call this movebutton function - the button moved but if I call the same function without an NStimer it no longer works.
The code for the timer is:
[NSTimer scheduledTimerWithTimeInterval:(0.1/3) target:self selector:#selector(movebutton:) userInfo:nil repeats:YES];
Can anyone spot what I am missing?
Update:
I try to print out the x and y coordinates of the button and actually the button position has been updated.
NSLog(#"x position %f",movebutton.frame.origin.x);
however, on the UI screen it does not reflect at all.
Try placing
[self.view layoutIfNeeded];
in your moveButton: method so that when it's called from the timer it knows to update the UI.
Where do you call -[UIButton setCenter] manually? Couldn't comment because I don't have permission to comment.
The NSTimer works because it fires the method every 0.1/3 because the NSTimer instance is added to run loop
I've got a NSTimer which fires every few milliseconds to load and then assign a new image into a NSImageView. The NSTimer has been setup using various run loops (NSRunLoopCommonModes, NSModalPanelRunLoopMode, NSEventTrackingRunLoopMode) just so that it continues to do what it's doing when events are firing.
This works great however this ceases to work when the actual window is being dragged around the screen. I have confirmed that the timer is in fact firing during the drag, and the image is being assigned to the NSImageView as well. The only problem is that the screen contents don't refresh for some reason when the window is being dragged. This is possibly a built-in optimization but I'd like to disable this so that the imageview continues to refresh and re-draw even when the window is being dragged around.
I've tried the following inside the NSTimer firing block but this still did not force the contents of the NSImageView to be refreshed and redrawn:
- (void)animateTimerFired:(NSTimer *)aTimer {
// .... load imageObj
// set image
[imgBackgroundView setImage: imageObj];
// try and refresh the window forcibly
[[imgBackgroundView window] viewsNeedDisplay];
[[imgBackgroundView window] display];
[[imgBackgroundView window] update];
[[imgBackgroundView window] flushWindow];
}
Is there any way to disable this 'optimization'?
I'm going to describe what I'm trying to do generally (in case there's a better way) and then the stumbling block I've run into (in case my way is the best way).
What I want to do: I want to add an invocation to my undo manager that has a time limit. If the undo is not triggered within the time limit, it won't be available when the device is shaken and thus nothing will happen.
What I'm doing: My approach was to use an NSUndoManager with an NSTimer. When I add the invocation to the undo manager, I kick off a 5-second timer as well. When the timer fires it checks !self.undoManager.isUndoing and if it's true than it goes ahead and removes all actions from the undo manager. Testing it in the simulator works: a shake gesture kicks off the undo before 5 seconds, but not after.
The problem is that if I get a shake gesture under the 5 second limit, the undo manager shows the alert, but if the user waits until after the 5s limit to actually tap the undo button, nothing happens: the timer happily cleared the stack away, even though the alert view was visible.
Is there a way to check and see if the alert view is visible? Best would be if I could figure out if the user hit undo or cancel as well, and clear the undo manager's action stack if the cancel button was pressed.
Or is there a better way besides using a timer in this manner?
Thanks!
Edited to add: My other thought was to capture the shake event myself (via the motionEnded:withEvent: call) and manually manage the alert and undo stack. Thoughts on this compared to the above are also welcome.
I ended up doing what I suggested in my edit—to use motionEnded:withEvent to manually manage the alert and undo. The downside to this is that you don't get the built-in undo alert which has a slightly different style from a UIAlertView and enters the screen with a shaking motion.
The upside is that I now have a undo that expires after 10 seconds. What follows is the general structure of the code in case you want the same.
First, make sure your app can receive shake events and that you have an NSUndoManager you can access. You also need a timer; I have my code set up with an NSTimer that kicks off when the undoable event occurs and lasts 10 seconds. Make sure you add your undo target at the same timer your timer starts so that there's actually something to undo.
Next, implement motionEnded:withEvent like so:
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event {
if (motion == UIEventSubtypeMotionShake && [self.undoManager canUndo]) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Undo something?" message:nil delegate:self cancelButtonTitle:#"Cancel" otherButtonTitles:#"Undo", nil];
[alert show];
undoAlertIsVisible_= YES;
}
}
I'm using an ivar called undoAlertIsVisible_ here to track if my alert is on screen.
In your timer's callback, do something like:
if (!self.undoManager.isUndoing && !undoAlertIsVisible_) {
// Clear away the possible undo
[self.undoManager removeAllActionsWithTarget:self];
}
undoTimer_ = nil;
Here we check to see we're not currently undoing and the alert isn't visible. If so, remove the undo actions and set the timer (another ivar) to nil. I'm setting the timer to nil so that I can check whether it's fired in my alert callback, which is here:
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
if (buttonIndex != alertView.cancelButtonIndex) {
if (self.undoManager.canUndo) {
[self.undoManager undo];
}
}
else {
if (!undoTimer_) {
// Timer fired while we were staring at the alert
[self.undoManager removeAllActionsWithTarget:self];
}
}
undoAlertIsVisible_= NO;
}
In the alert callback we either make the undo happen or, if the timer fired while the alert was visible and the alert was canceled, we clear possible undo actions. Otherwise the undo action would hang around after canceling with no timer to clear it.
Hope this helps someone!
I'd like to reproduce this behavior in my iPad application.
I have a subView that contains four custom buttons.
The view has an alpha value of 0.0
I have another custom button outside of the view descripted above that is always visible.
When the user touches the visible button the view appear animating its alpha to 1.0 showing the other 4 buttons.
Now I'like to start a timer that fires the view fadeOut after 2 seconds
When and if the user interact (say touchDown or whatever) with the buttons the timers need to be reset.
In other words the view may disappear only when nobody touches a button inside It.
Can you help me with this.
I've managed to learn the basics of UIView animation but I don't know how to queue them.
My iPad has iOS 3.2.2. installed.
Sorry for the bad explanation but this is my first iPad application and my first obj-c project.
You'd keep an NSTimer instance variable for that. As soon as your view is completely visible, which you can notice by e.g. implementing the fade in animation's delegate, you initialize it like this:
_fadeTimer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:#selector(fade:) userInfo:nil repeats:NO];
Make sure _fadeTimer is an instance variable, you need to be able to reset it. Then implement the fade out method:
- (void)fade:(NSTimer *)aTimer {
// Forget about timer!
_fadeTimer = nil;
[UIView beginAnimations:nil context:NULL];
// fade here
[UIView commitAnimations];
}
Upon every user interaction you just call a method that delays the fade. To do this, delete and re-create the timer. Or change it's fire date:
- (void)delayFade {
[_fadeTimer setFireDate: [NSDate dateWithTimeIntervalSinceNow: 2.0]];
}
PS: There is no need to explicitly retain the timer. It's retained by the runloop until it fires. After the callback, it will be released anyways. Just make sure you always reset the variable to nil, otherwise your app may crash on an invalid access. If you need to delete the time beofre it fired, call the invalidate method.
I've got a statusbar item that pops open an NSMenu, and I have a delegate set and it's hooked up correctly (-(void)menuNeedsUpdate:(NSMenu *)menu works fine). That said, that method is setup to be called before the menu is displayed, I need to listen for that and trigger an asynchronous request, later updating the menu while it is open, and I can't figure out how that's supposed to be done.
Thanks :)
EDIT
Ok, I'm now here:
When you click on the menu item (in the status bar), a selector is called that runs an NSTask. I use the notification center to listen for when that task is finished, and write:
[[NSRunLoop currentRunLoop] performSelector:#selector(updateTheMenu:) target:self argument:statusBarMenu order:0 modes:[NSArray arrayWithObject:NSEventTrackingRunLoopMode]];
and have:
- (void)updateTheMenu:(NSMenu*)menu {
NSMenuItem *mitm = [[NSMenuItem alloc] init];
[mitm setEnabled:NO];
[mitm setTitle:#"Bananas"];
[mitm setIndentationLevel:2];
[menu insertItem:mitm atIndex:2];
[mitm release];
}
This method is definitely called because if I click out of the menu and immediately back onto it, I get an updated menu with this information in it. The problem is that it's not updating -while the menu is open-.
Menu mouse tracking is done in a special run loop mode (NSEventTrackingRunLoopMode). In order to modify the menu, you need to dispatch a message so that it will be processed in the event tracking mode. The easiest way to do this is to use this method of NSRunLoop:
[[NSRunLoop currentRunLoop] performSelector:#selector(updateTheMenu:) target:self argument:yourMenu order:0 modes:[NSArray arrayWithObject:NSEventTrackingRunLoopMode]]
You can also specify the mode as NSRunLoopCommonModes and the message will be sent during any of the common run loop modes, including NSEventTrackingRunLoopMode.
Your update method would then do something like this:
- (void)updateTheMenu:(NSMenu*)menu
{
[menu addItemWithTitle:#"Foobar" action:NULL keyEquivalent:#""];
[menu update];
}
(If you want to change the layout of the menu, similar to how the Airport menu shows more info when you option click it, then keep reading. If you want to do something entirely different, then this answer may not be as relevant as you'd like.)
The key is -[NSMenuItem setAlternate:]. For an example, let's say we're going to build an NSMenu that has a Do something... action in it. You'd code that up as something like:
NSMenu * m = [[NSMenu alloc] init];
NSMenuItem * doSomethingPrompt = [m addItemWithTitle:#"Do something..." action:#selector(doSomethingPrompt:) keyEquivalent:#"d"];
[doSomethingPrompt setTarget:self];
[doSomethingPrompt setKeyEquivalentModifierMask:NSShiftKeyMask];
NSMenuItem * doSomething = [m addItemWithTitle:#"Do something" action:#selector(doSomething:) keyEquivalent:#"d"];
[doSomething setTarget:self];
[doSomething setKeyEquivalentModifierMask:(NSShiftKeyMask | NSAlternateKeyMask)];
[doSomething setAlternate:YES];
//do something with m
Now, you'd think that that would create a menu with two items in it: "Do something..." and "Do something", and you'd be partly right. Because we set the second menu item to be an alternate, and because both menu items have the same key equivalent (but different modifier masks), then only the first one (ie, the one that is by default setAlternate:NO) will show. Then when you have the menu open, if you press the modifier mask that represents the second one (ie, the option key), then the menu item will transform in real time from the first menu item to the second.
This, for example, is how the Apple menu works. If you click once on it, you'll see a few options with ellipses after them, such as "Restart..." and "Shutdown...". The HIG specifies that if there's an ellipsis, it means that the system will prompt the user for confirmation before executing the action. However, if you press the option key (with the menu still open), you'll notice they change to "Restart" and "Shutdown". The ellipses go away, which means that if you select them while the option key is pressed down, they will execute immediately without prompting the user for confirmation.
The same general functionality holds true for the menus in status items. You can have the expanded information be "alternate" items to the regular info that only shows up with the option key is pressed. Once you understand the basic principle, it's actually quite easy to implement without a whole lot of trickery.
The problem here is that you need your callback to get triggered even in menu tracking mode.
For example, -[NSTask waitUntilExit] "polls the current run loop using NSDefaultRunLoopMode until the task completes". This means that it won't get run until after the menu closes. At that point, scheduling updateTheMenu to run on NSCommonRunLoopMode doesn't help—it can't go back in time, after all. I believe that NSNotificationCenter observers also only trigger in NSDefaultRunLoopMode.
If you can find some way to schedule a callback that gets run even in the menu tracking mode, you're set; you can just call updateTheMenu directly from that callback.
- (void)updateTheMenu {
static BOOL flip = NO;
NSMenu *filemenu = [[[NSApp mainMenu] itemAtIndex:1] submenu];
if (flip) {
[filemenu removeItemAtIndex:[filemenu numberOfItems] - 1];
} else {
[filemenu addItemWithTitle:#"Now you see me" action:nil keyEquivalent:#""];
}
flip = !flip;
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
NSTimer *timer = [NSTimer timerWithTimeInterval:0.5
target:self
selector:#selector(updateTheMenu)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
Run this and hold down the File menu, and you'll see the extra menu item appears and disappears every half second. Obviously "every half second" isn't what you're looking for, and NSTimer doesn't understand "when my background task is finished". But there may be some equally simple mechanism that you can use.
If not, you can build it yourself out of one of the NSPort subclasses—e.g., create an NSMessagePort and have your NSTask write to that when it's done.
The only case you're really going to need to explicitly schedule updateTheMenu the way Rob Keniger described above is if you're trying to call it from outside of the run loop. For example, you could spawn a thread that fires off a child process and calls waitpid (which blocks until the process is done), then that thread would have to call performSelector:target:argument:order:modes: instead of calling updateTheMenu directly.