A button on my inferface, when held down, will perform a series of changes on other parts of my interface.
For instance, for one second some text will turn blue, then a UImageView will change its image for two secs ...etc etc...
This series of changes will keep looping thru the same steps as long as the button is held down.
I've never used NSTimer before, but would this be the way to go?
You don't need an NSTimer for this, you can simply use performSelector:withObject:afterDelay: sequencing from one method to the next until you start again. Start the process when the button is held down, and call cancelPreviousPerformRequestsWithTarget:selector:object: when the button is released. Something like:
- (void) step1
{
// turn blue
[self performSelector:#selector(step2) withObject:nil afterDelay:1.0];
}
- (void) step2
{
// change image
[self performSelector:#selector(step3) withObject:nil afterDelay:2.0];
}
- (void) step3
{
// turn red
[self performSelector:#selector(step1) withObject:nil afterDelay:3.0];
}
- (void) stopSteps
{
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(step1) object:nil];
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(step2) object:nil];
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(step3) object:nil];
}
You could remember the currently executing "performSelector" selector and only cancel that one, but its hardly worth remembering it.
Alternatively, you could use an NSTimer and a state machine, but for what you describe, the above is probably easier - it depends on how consistent your sequence is and whether it is easier to specify your sequence as a set of steps like the above, or a set of data (in which case, use a state machine of some sort, with either an NSTimer or performSelector:withObject:afterDelay:)
Related
I run my animations in a UITAbleViewCell.
Each cell has its own animation and the cells are reusable.
I use [mView performSelectorInBackground:#selector(layoutSubview) withObject:nil];
There in the background thread I initiate the runLoop to perform tasks like this:
- (void)startAnimation
{
NSRunLoop *mLoop = [NSRunLoop currentRunLoop];
self.animationTimer = [NSTimer scheduledTimerWithTimeInterval:animationInterval target:self selector:#selector(setNeedsDisplay) userInfo:nil repeats:YES];
mRunLoop = YES;
while (mRunLoop == YES && [mLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.01]]);
}
and stop it:
- (void)stopAnimation
{
if (![NSThread isMainThread]) {
[[NSThread currentThread] cancel];
}
mRunLoop = NO;
self.animationTimer = nil;
CFRunLoopStop(CFRunLoopGetCurrent());
}
I run into problems when I fast scroll through table, because on the first cell initiation I begin the animation, so the first runLoop call occures which performs a setNeedDisplay and all the methods from it. But before finishing the first runLoop cycle the cell disappears from the view and is already available for reuse. So I begin clearing it, while the cycle is still performing operations and here I meet situations like
message sent to deallocated instance
So could you please give me some hints of how should I correctly stop performing the operations in that thread? I mean if I want to realese for example an object, which is performing some actions how to immediately stop'em?
Hope I gave enough info.
Thanks
UPDATE: No ideas at all?
I'll take a completely different stab on it:
Get rid of the cell's timers and background threads altogether!
Animation is not something where NSTimer is a good fit in the first place and having multiple timers won't help much, either.
UITableView has a method visibleCells and a method indexPathsForVisibleRows. I'd suggest to use a single CADisplayLink — which is suited for animation, as it calls you back with the actual refresh rate of the display or a fraction thereof — in your tableview-controller and in the callback of that display-link iterate over the visible cells.
If you want to schedule the display-link on the run-loop of a secondary thread, feel free to do so, but I'd check if you can get away without extra threading first.
Some code:
#interface AnimatedTableViewController ()
#property (strong, nonatomic) CADisplayLink *cellAnimator;
- (void)__cellAnimatorFired:(CADisplayLink *)animator;
#end
#implementation AnimatedTableViewController
#synthesize cellAnimator = cellAnimator_;
- (void)setCellAnimator:(CADisplayLink *)animator
{
if (animator == cellAnimator_)
return;
[cellAnimator_ invalidate];
cellAnimator_ = animator;
[cellAnimator_ addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSCommonRunLoopModes];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
self.cellAnimator = [CADisplayLink displayLinkWithTarget:self selector:#selector(__cellAnimatorFired:)];
...
}
- (void)viewWillDisappear:(BOOL)animated
{
self.cellAnimator = nil;
...
[super viewWillDisappear:animated];
}
- (void)__cellAnimatorFired:(CADisplayLink *)animator
{
NSArray *visibleCells = [self.tableView visibleCells];
[visibleCells enumerateObjectsUsingBlock:^(UITableViewCell *cell, NSUInteger unused, BOOL *stop){
[cell setNeedsDisplay];
}];
}
...
#end
NSTimer has a -cancel method that stops the timer from firing. Calling it in -prepareForReuse (and, for that matter, in -stopAnimation) may help.
However, this code looks rather dangerous. Nesting run loops like this is almost never a good idea—and moreover, as far as I can tell it's totally unnecessary. If you let -startAnimation return, your animation timer will still get run on the main run loop. And if you're doing it this way because there's some code after -startAnimation that you want to delay, you should restructure your code so this isn't needed.
(If you drop the runloop stuff in -startAnimation, don't stop the runloop in -stopAnimation either.)
Something like the approach danyowdee recommends would be even better, but at least get rid of this runloop stuff. It's just asking for trouble.
I think you can use this method for your problem
[NSObject cancelPreviousPerformRequestsWithTarget:yourTarget selector:aSelector object: anArgument];
I think that the best way to avoid that behavior is assigning the delegate that receives the cancel method in other class that won't be reused. For example, you can have a private array of instances that process all the cancel methods, each row mapped into an array element.
I recommend you the lazy tables example provided by Apple in Xcode documentation. It's a great example of how to load images asynchroniously in background with a table. I think that also it would be useful for you for the scrolling subjects (decelerating and paging).
Only one more consideration, i don't recommend messing up with several cfrunloopstop, test it hard!
I have an asset manager that needs to notify the owner it's assets are ready. I'm sending a token back for the consumer to listen to listen for a notification to avoid tighter coupling. The issue is when the assets are already loaded I need to call the loadComplete after a delay. What's the best way to do this in objective-c?
Asset Manager
-(tokenString*) loadAssetPath:(NSString*) asset {
//start asynchronous load
//or if assets ready send complete <-- issue
return nonceToken;
}
-(void)loadComplete {
[[NSNotificationCenter defaultCenter]
postNotificationName:tokenString object:self];
}
Consumer
NSString* token;
-(void) loadSomething {
if(token)
[self removeListener];
token = [[AssetManager sharedManager]
loadAssetPath:#"http://server.dev/myLargeImage.png"];
[[NSNotificationCenter defaultCenter]
addObserver:[AssetManager sharedManager]
selector:#selector(assetLoaded:) name:token];
}
-(void)assetLoader:(NSNotifcation*)aNotification {
[self removeListener];
//continue on with stuffing stuff
}
Use NSObject's performSelector function which allows it to be called after a delay.
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay
You can even use a form of this function to run it on another thread, which is useful to not blocking the main thread when doing lengthy operations (just don't muck with the UI objects in this thread).
#DavidNeiss is correct about performSelector:withObject:afterDelay:, but you almost certainly don't want an actual time delay here. At most you want to perform your selector on the next event loop, just so things are consistent for the listener. So you should make the delay 0. This differs from the normal performSelect:withObject: which will immediately perform the selector synchronously.
-(tokenString*) loadAssetPath:(NSString*) asset {
//start asynchronous load
if (<load is actually complete>) {
// -loadComplete will execute on the next event loop
[self performSelector:#selector(loadComplete) withObject:nil afterDelay:0];
}
return nonceToken;
}
When my main viewController is first clicked, it starts showing a demo (on repeat) as follows:
showingDemo = YES;
[self startDemo];
- (void)startDemo {
if (showingDemo) {
[self performSelector:#selector(stepone) withObject:nil afterDelay:1.5f];
[self performSelector:#selector(steptwo) withObject:nil afterDelay:2.0f];
[self performSelector:#selector(stepthree) withObject:nil afterDelay:3.8f];
[self performSelector:#selector(stepfour) withObject:nil afterDelay:4.3f];
[self performSelector:#selector(startDemo) withObject:nil afterDelay:5.6f];
}
}
When it is clicked a second time, I bring a new ViewController to the screen
showingDemo = NO;
[self.view addSubview:newView];
I thought this would stop the endless loop.
When the user returns back to my main viewController:
[newView.view removeFromSuperview];
And clicks on the screen again:
showingDemo = YES;
[self startDemo];
In testing my app, if I click back quickly (before the loop has had time to end, the program seems to be running through the loop twice - the one that was previously going and the new one - and therefore it looks all weird, with the 'stepthree' function happening before 'stepone' and so forth.
Anybody know a better way to STOP the loop I've programmed for good so that when I go to start it again later, it doesn't run multiple loops thinking that the previous one hasn't been finished?
Thanks so much!
When you set showingDemo to NO, call NSObject's cancelPreviousPerformRequestsWithTarget: to cancel any pending perform requests:
showingDemo = NO;
[NSObject cancelPreviousPerformRequestsWithTarget:self];
[self.view addSubview:newView];
I have a UITabBarController that nests a UIView-Subclass (ImageViewer) as it's third tab.
In this ImageViewer Subclass I call the viewDidAppear method:
- (void) viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
/* ... start custom code ...
NSLog(#"viewDidAppear tag 1 passed); /* BREAKPOINT 1 here
[myUIActivityIndicator stopAnimating];
NSLog(#"viewDidAppear tag 2 passed); /* BREAKPOINT 2 here
/* ... end custom code ...
}
the method is called automatically, but strangely the view only appears after this method has been processed completely?
When I set breakpoints (1 and 2) as indicated, the processing (upon selecting the tab) stops whilst the previous tab is still showing. Only when clicking continue after the second breakpoint, the view will be displayed. (FYI the NSLogs are carried out immeldiately).
In this case viewDidAppear behaves more like viewWillAppear ....
Any clues what might be going on?
Cheers
If you want to allow the screen to be re-drawn when your view loads, but to trigger some other updating code in -viewDidAppear:, use performSelector:withObject:afterDelay: like this:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self performSelector:#selector(updateUI) withObject:nil afterDelay:0.0];
}
…
- (void)updateUI
{
// Do your UI stuff here
}
When you do it this way, the current event loop will finish quickly, and UIKit will be able to re-draw the screen after your view has loaded. updateUI will be called in the next event loop. This is a good way to get snappy view transitions if you have to perform computationally intensive calculations or updates after a view has loaded.
From the sound of it, if you are actively calling the method, the device might not have time to actually display the view while it is running the "custom code" in your viewDidAppear method. I that case you should let the program call the viewDidAppear method itself.
Your program may also be working on other code which would slow down the appearance of the view, this can be solved using timers. i.e. instead of:
[self otherCode];
you would write:
[NSTimer scheduledTimerWithTimeInterval:.5
target:self
selector:#selector(otherCode)
userInfo:nil
repeats:NO];
you might want to try simply delaying your "custom code" with a timer in this way.
I've added an observer for my method:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(closeViewAfterUpdating)
name:#"labelUpdatedShouldReturn"
object:nil];
Then my relevant methods:
-(void)closeViewAfterUpdating; {
NSLog(#"Part 1 called");
[self performSelector:#selector(closeViewAfterUpdating2) withObject:nil afterDelay:2.0];
}
-(void)closeViewAfterUpdating2; {
NSLog(#"Part 2 called");
[self dismissModalViewControllerAnimated:YES];
}
The only reason why I've split this method into two parts is so that I can have a delay before the method is fired.
The problem is, the second method is never called. My NSLog output shows Part 1 called, but it never fires part 2. Any ideas?
EDIT: I'm calling the notification from a background thread, does that make a difference by any chance?
Here's how I'm creating my background thread:
[NSThread detachNewThreadSelector:#selector(getWeather) toTarget:self withObject:nil];
and in getWeather I have:
[[NSNotificationCenter defaultCenter] postNotificationName:#"updateZipLabel" object:textfield.text];
Also, calling:
[self performSelector:#selector(closeViewAfterUpdating2) withObject:nil];
does work.
EDITx2: I fixed it. Just needed to post the notification in my main thread and it worked just fine.
The background thread is the problem. It has a non running run loop, thus the selector is never called. Just let the NSRunLoop or CFRunLoopRef object of the thread run while the selector isn't fired.
I tried your code and it works fine on my side. You might be doing something funky in the background that is interrupting your selector.
You have a semi-colon in the method definition:
-(void)closeViewAfterUpdating2; {
Is this present in the code or a copy/paste issue? That would be the problem on why you never see it called.