I'm creating a game view controller using an NSTimer to update a progress bar representing the remaining time of the current game round. This game view also displays a button performing some light core data updates.
When the remaining time is up, the timer is invalidated and a new view is pushed.
The first time the game view loads in the application life cycle, it works perfectly: no slowdowns when pressing the button and updating core data.
But when I'm pushing back the same game view in the application life cycle, button presses make the progress bar choppy and irresponsible.
Is the NSTimer not properly invalidated in the run loop? Should I use CADisplayLink instead, though my view isn't using a lot of resources?
Thanks in advance.
Here is my code:
Timer declaration in .h file :
#property (weak, nonatomic) NSTimer *updatetimer;
Timer creation in viewDidLoad:
self.updatetimer = [NSTimer timerWithTimeInterval:counterStep target:self selector:#selector(updateProgress) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.updatetimer forMode:NSRunLoopCommonModes];
updateProgress function :
- (void)updateProgress
{
//compute current time
currentTime = currentTime - counterStep;
//set timer label to int value of current time
self.timerLabel.text = [[NSString alloc] initWithFormat:#"%d",[[NSNumber numberWithDouble:currentTime] intValue]];
//update progress bar accordingly
[self.progressbar setProgress: currentTime / totalTime animated:YES];
if(currentTime <= 0)
{
//call the method that invalidates the timer + pushes to the next view
[self overallTimeEnd];
}
}
Timer invalidation:
[self.updatetimer invalidate];
self.updatetimer = nil;
There's not enough here to diagnose the problem. Given that you've confirmed that your initial timer is successfully invalidated, the next thing to check is to make sure that the problem is really that updateProgress is not getting called with the desired frequency (rather than whatever is updating the state variables that updateProgress relies upon).
But, let's assume that the problem is truly that updateProgress is not getting called with the desired frequency. If so, then it's probably something that you're running on that main thread that is preventing the timer from being able to run with the desired frequency. It's hard to say without seeing what updateProgress is doing, much less anything else you might have running on the main thread. WWDC 2012 video Building Concurrent User Interfaces on iOS illustrates how to use Instruments to find what might be adversely affecting the concurrent UX.
Finally, it's not clear underlying process that updateProgress is keeping us informed of, but if you can go to an event-driven approach (e.g. if network connection, update UI as didReceiveData is called rather than using a timer). Sometimes you have to use timers, but if you can avoid it, you might be able to improve the efficiency of the app.
So, finally found what was causing my timer not being deallocated over the application life cycle: I wasn't using unwind segues in my storyboard to go back between views but standard push segues.
Never used those before and didn't knew about them ...
Thanks to all of you for helping.
Related
While reading an audio file (which can be big), I want to display a progress view on a main view controller subview (it is an UIView).
I wrote a method for that in my UIViewController:
- (void) displayFileReadingView
{
self.fileReadingView = [[UIView alloc] initWithFrame:CGRectMake(20., 100., 300., 120.)];
self.readingProgress = [[UIProgressView alloc] initWithFrame:CGRectMake(20., 90., 200., 8.)];
self.readingMessage = [[UILabel alloc] initWithFrame:CGRectMake(45., 10., 200., 20.)];
... elements parameter settings ...
[fileReadingView addSubview:self.readingMessage];
[fileReadingView addSubview:self.readingProgress];
[self.view addSubview: self.fileReadingView];
}
In the viewDidAppear methods, after all last initializations, I read an audio file (using AVURLAsset) and analyse the data to plot it.
When displayFileReadingView is called from viewDidLoad, or viewWillAppear, the view is displayed correctly. When it is called from viewDidAppear, as a first line, it is not displayed until several seconds after the end of the viewDidAppear execution. This is my first question:
• Why an UI method to allocate views, subviews and add them to the appropriate view, allow display or not depending of the calling method, since the displayFileReadingView method is not dependent of any initialization parameter from the main view managed by the owning UIViewController?
Here is the second problem. My UIProgressView is supposed to be updated every second, using a NSTimer scheduled to call updateReadingProgress
- (void) updateReadingProgress {
[self.readingProgress setProgress:myNewValue animated:YES];
[self.fileReadingView setNeedsDisplay];
}
But unfortunately, the NSTimer is not fired in time. If the allocation (using either ScheduledTimerWithTimerInterval... or TimerWithInterval... + adding in main loop (all modes tested) is made normally, it is never fired. If I put the same allocation in a dispatch_main_async() (or dispatch_after...) the timer is fired many seconds after for the first time, sometimes up to 25 seconds.
The result, depending on my code choices explained here: sometimes the view is not displayed, sometimes not. When it is, its UIProgressView subview is never updated, or updated dozen of seconds after all other calculations are done (after the owner view should be removed from display).
I also tried to replace NSTimer with CADisplayLink to fire updateReadingProgress method, using an "ordinary" call or "dispatched in main queue" included call, but in this CADisplayLink case it is never fired. Spent two days and nights in trying combinations, reading developer websites... no solution!
This is my second question:
• How can I be sure that a method updating the UI get called periodically without giant delay (some .2 or .5 second delay is not a problem?
[Using Xcode9 for iOS10 target, tested on Simulator and devices (iPhone/iPad)]
I have an NSTimer in a view controller which starts counting when the user presses a button. It counts down seconds for 3 minutes. What I want is that if the user wants to go back previous page (maybe go another page) and come back to this timer page, he/she should see counting timer label. I mean, I know timer continues to counting but when I load this page again, I cannot see its label as i left. Also I check my timer and get it's not considered as "valid".
I don't want to use AppDelegate for this purpose because I have many view controllers and this one is not that important. So, how can I achieve this continuous timer label? Should I use static timer or flag?
Thank you.
you cna implement a
static int counter = 0 ;
by having a selector method in the NSTimer and use that counter to continue the counting
[NSTimer scheduledTimerWithTimeInterval:180.0f
target:self
selector:#selector(updateTime:)
userInfo:nil
repeats:YES];
// Implement Method
-(void)updateTime: (NSTimer *) timer {
counter++;
self.label.text = counter;
}
Making the timer static might introduce nasty memory problems as it holds a reference to it's target.
But as that already is no problem for you seemingly, that'd be a good way to go. Better yet: create a singleton object for your timer that encapsulaten the NSTimer and the value it's counting. This way, your view controllers can use KVO on the value and are otherwise completely independent of the timer.
I'm using NSTimer to fire a method that scrolls UITableView
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.1
target:self
selector:#selector(scroller)
userInfo:nil
repeats:YES];
-(void)scroller
{
[self.row1TableView setContentOffset:CGPointMake(self.row1TableView.contentOffset.x, self.row1TableView.contentOffset.y - 50) animated:YES];
}
Problem is that the scrolling seems slow. It's closer to 1 second than .1 seconds in the interval.
What's the problem?
As far as I know, you can not change the default animation duration of setContentOffset:animated:. But what you can do is, setup a Core Animation display link (CADisplayLink - you can search for code samples on how to set up, but it is quite straight-forward. The class documentation should be a good place to start) and it will fire every frame, calling back a method you provide.
Inside that callback method, you can calculate how much you want to scroll your table view (how many points per frame), and call setContentOffset:animated: with the second parameter set to NO (immediate scrolling). You should implement some sort of easing to achieve better results.
Note: The reason for using CADisplayLink instead of NSTimer is, it is more reliable. It is what you would use in games before SpriteKit was available.
Addendum: This blog post has some sample code on how to setup the display link and the respective callback method.
Addendum 2: You can setup an instance variable to act as a "counter", and increment it by the ammount of time ellapsed since last frame, within each call of your callback (use properties duration and/or frameInterval). Once the counter reaches a critical value (that is, the animation has run for enough time) you can stop the display link update by calling the method:
-[CADisplayLink invalidate].
NSTimer that calls a selector on the current threads run loop. It may not be 100% precise time-wise as it attempts to dequeue the message from
the run loop and perform the selector.
On my button to start my game i have a few NSTimers to make things scroll, i want to add a delay to these timers, not too sure how to though.
any advice?
Here's the buttons with 1 of the NSTimers
-(IBAction)StartGame:(id)sender{
StartGame.hidden = YES;
backgroundMovement = [NSTimer scheduledTimerWithTimeInterval:0.06 target:self selector:#selector(backgroundMoving)
userInfo:nil repeats:YES];
}
Thank you
When you call [NSTimer scheduledTimerWithTimeInterval:...] the time interval is the delay. You will be called back after the time interval you specify.
Alternatively, call performSelector:withObject:afterDelay:.
However, note please that there are deep flaws in your proposed architecture. A repeating timer with a .06 interval is a terrible idea, and more than one is an atrocious idea. You need to rethink this completely. Consider using real animation, or a display link, or sprite kit. Or something. Anything, really.
If you'd like to add a delay only once, when starting, you could implement a - (void)startTimers method, where you create and start your timers, then do
[self performSelector:#selector(startTimers) withObject:nil afterDelay:1.0f];
in your IBAction.
^ Don't copy-paste the performSelector method, as I might have made a typo, since I don't have Xcode open to verify.
Options to do this:
1) call a method with performSelector using a delay. In that method you create the NSTimer
[self performSelector:#selector(createTimer_backgroundMovement) withObject:nil
afterDelay:0.1];
2) if the delay is dependent of an action you can use this information in a flag and that flag can be used in the backgroundMoving method:
-(void) backgroundMoving {
if (!blAction_whichEnablesTimer) {
return;
}
// your backgroundMoving timer code
}
3) consider moving views with eg. block animations. Using block animations you can use delay also. Please see doc: https://developer.apple.com/library/ios/documentation/windowsviews/conceptual/viewpg_iphoneos/animatingviews/animatingviews.html
I have a UITableViewCell subclass which contains a UIStepper. When the user interacts with the stepper, I fire off an NSTimer so that the stepper will save its value and write to Core Data if the value is not changed again within 2 seconds.
To put it another way:
UIStepper is contained within UITableViewCell.
User can change stepper value up and down.
Each touch triggers timer with 2 second delay.
Each subsequent touch invalidates the timer.
When stepper has been left alone for 2 seconds, changes are saved.
This works very well for what I need it for. The problem is that if my user is very quick and they change the value, then pop the view controller and go to do something else, the 2 second timer still hasn't fired and the data is not up to date for the next action.
To keep things as simple as possible, I need to be able to tell within that table view cell if the (table) view is getting popped. Then I can expedite the saving process and ensure data is up to date and saved before any other actions take place.
If you want to tell if the table has been popped, put your cleanup/save method in the viewWillDisappear method. Because you're using timers, you do not want to do it it dealloc, so you don't have any unintended strong reference cycles.
It's not clear from your question, but I would want to make sure that you're not putting your NSTimer on your UITableViewCell cells. Obviously, it's a model issue, not a view issue, but also table views do all sorts of optimizations for dequeue and reusing table view cells.
Second, what ever object class you have to keep track of your data (I call it ModelDataItem) should provide not only the mechanism to save, to use the timers, etc., but also a mechanism to force the save of any pending records (which I do through a boolean needSave). So, to support that, in my mind, you ModelDataItem probably should have at least the following four items:
(a) a reference to its own timer;
#property (nonatomic, strong) NSTimer *timer;
(b) a flag that indicates whether the record has a pending save operation
#property (nonatomic) BOOL needSave;
(c) a method that you call whenever the object's values change (e.g. the value was incremented) to schedule the save in 2 seconds:
- (void)scheduleSave
{
self.needSave = YES;
if (self.timer)
[self.timer invalidate];
self.timer = [NSTimer scheduledTimerWithTimeInterval:2.0
target:self
selector:#selector(save)
userInfo:nil
repeats:NO];
}
(d) you need the method that the timer calls that actually saves the record:
- (void)save
{
// do whatever you need to save the record
NSLog(#"%s saving value=%#", __FUNCTION__, self.value);
// now let's clean up the timer
if (self.timer)
{
[self.timer invalidate];
self.timer = nil;
}
self.needSave = NO;
}
Then, in your table view controller, you should:
(a) when the stepper's UIControlEventValueChanged is called, you should obviously change your data model and then call the above ModelDataItem method scheduleSave;
(b) when the table view is being dismissed, should presumably promptly save anything pending:
for (ModelDataItem *item in allModelDataItems)
{
if (item.needSave)
[item save];
}
Note, on that last point, I do not rely on dealloc to clean up and save the model items that need saving, because a scheduled NSTimer retains its target and thus the dealloc won't get call (or at least not until the timer is performed). So, I manually iterate through them and take care of that when I dismiss the view.
You could have your custom cell listen for a notification that the table view is going away and then post that notification in the viewWillDisappear method of your view controller.
When the parent TableView is popped of of the NavigationController that I assume you're using, the TableView should be dealloc'ed. In addition, each of its TableViewCells should also be dealloc'ed.
You could invalidate the timer and process any pending updates inside of the TableViewCell's dealloc method.
Without changing the design, that's what I would try. If that proves to be problematic, you might also try setting up some sort of communication back to the TableView when this value changes and have the TableView be responsible for the data updates.
It's hard to tell from your description, but I don't think you need to use the timer at all. Can't you just assign that stepper value to a property or ivar of that class, and then in the viewDidDisappear method, write that value out to core data?
I've found the best way to do this in the past is to set the UITableViewCell Subclass as a delegate of the view controller... then in your view controllers viewDidDisappear just call the delegate method.
The delegate method would be implemented within your UITableViewCell Subclass and would simply call the same code that you do when your two second timer completes. This is similar to the notification method suggested by Phillip, but a little bit cleaner if you're used to assigning delegates, etc.
Just override dealloc method and do the stuff. UITableViewCell is deallocated when parents view is popped / unloaded.