I'm using an NSTimer to call a method at a specific timer interval, usually more than ten minutes, but if varies. I have a timer label that shows how many minutes are remaining and I'm trying to figure out the best way to update that label whenever the minute value of the timer updates. The only two ways I can think of feel a little hacky...
Use two timers, one to manage the amount of time before calling
the method and a separate timer that is recreated every minute to
update the label.
Use one timer that is recreated every minute
and manually keep track of how much time has elapsed so I know when
to call the method.
Any other ideas?
Thanks so much for your wisdom!
You can do it with a single NSTimer (I guess this counts as your option #2, but slightly expanded):
When you want to schedule a method to be called in the future, calculate the time targetTime when the method needs to run.
Start NSTimer set at the earlier of now + 1 minute and targetTime.
When the timer fires, compare the current time to targetTime. If we're there, run the target method; otherwise, reschedule the timer at the earlier of now + 1 minute and targetTime again.
Implement a KVO-compliant property for the timer duration, with a custom setter that invalidates the timer (if one already exists), throws it away, and creates and schedules a new timer with the new duration.
(Alternatively, observe the property from another object that owns the timer. One object holds the time interval, the other holds the timer. You can then keep the synthesized setter; you'd create/recreate the timer in the observer method.)
Then, in the controller that owns the field, observe that property, and update the field whenever the property changes.
Related
My stopwatch is running 0.5x slower than actual time
(e.g. while the real time is 1 minute, the stopwatch shows ~34 seconds).
Here is the Provider code:
class TimerState extends ChangeNotifier {
late String _stringTimer;
late Duration _duration;
Timer? _timer;
TimerState() {
_stringTimer = '00:00.00';
_duration = const Duration();
}
String get get => _stringTimer;
void start() {
_timer = Timer.periodic(const Duration(milliseconds: 1), (_) => _addTime());
}
void _addTime() {
_duration = Duration(milliseconds: _duration.inMilliseconds + 1);
_formattedTimer();
}
void _formattedTimer() {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final milliseconds = twoDigits((_duration.inMilliseconds.remainder(1000) / 10).floor());
final seconds = twoDigits(_duration.inSeconds.remainder(60));
final minutes = twoDigits(_duration.inMinutes.remainder(60));
_stringTimer = '$minutes:$seconds.$milliseconds';
notifyListeners();
}
}
Your approach adds the timer interval (1ms) on each timer event. That is a bad approach because it assumes that your timer fires exactly on every millisecond with no room for error. You also will lose time if any timer events are missed (which might happen if you ever do work that takes longer than 1ms). Error will accumulate.
Also note that 1ms is very short. Redrawing your widget every millisecond would be updating it at a rate of 1000 frames per second.
A much better approach would be to record the start time and, on each timer event, compute the difference from DateTime.now() to the start time (e.g. var elapsed = DateTime.now().difference(startTime);). That will prevent error from accumulating. Better yet, use a Stopwatch object which does that for you.
You also should pick a more reasonable timer interval; picking a rate faster than your screen's refresh rate is wasteful.
If you don't use cocoa2d etc., how would you control the speed of a frame if you had to code this manually?
i.e. if you wanted things to operate in 50 frames per second (or whatever the industry best practice is?)
use CADisplayLink to get called at every frame. It will be max 60 FPS. If your code do too much work, you'll be called less often, and your UI will feel slow below 40 FPS.
Alternative is to schedule NSTimers, but it has some issues. If your runloop is not ready to call the timer on time, calls will be skipped, thus not guaranteeing any frame rate.
from apple's doc
A repeating timer always schedules
itself based on the scheduled firing
time, as opposed to the actual firing
time. For example, if a timer is
scheduled to fire at a particular time
and every 5 seconds after that, the
scheduled firing time will always fall
on the original 5 second time
intervals, even if the actual firing
time gets delayed. If the firing time
is delayed so far that it passes one
or more of the scheduled firing times,
the timer is fired only once for that
time period; the timer is then
rescheduled, after firing, for the
next scheduled firing time in the
future.
I have a requirement in my app which requires me to display some message to the user if there is no activity happening for about 3 hours on the app/ipad.
I considered the solution of having an NSTimer that would start at launch. If the user performs any action, I invalidate the timer and start a new one. However, there is video playback on the app and for all I know, the user may be watching the video for about 3 hours and performs no other action during that time and would still get the message.
So, an alternative is to invalidate and start the timer every time I detect the ipad/iphone has moved. In other words, use the accelerometer and in the call back to detect acceleration, I can invalidate and create the timer again.
But my worry with this approach is that even for smallest of movements, the timer would have to be invalidated and recreated. Will this approach in any way impact performance?
Thanks and Regards,
hetal
Creating a timer is not that expensive, but it's still a little expensive.
The good news is that you can arbitrarily change the fire date:
[timer setFireDate:[NSDate dateWithTimeIntervalSinceNow:3*60*60]];
Alternatively, for very slightly less overhead:
CFRunLoopTimerSetNextFireDate((CFRunLoopTimerRef)timer, CFAbsoluteTimeGetCurrent()+3*60*60);
(I think the CFAbsoluteTimeGetCurrent() overhead is more than the object-creation overhead, but meh.)
A slightly better solution might be to leave the timer alone most of the time; simply update the "last activity" timestamp. When the timer fires, look at the "last activity" timestamp. If it's more than 3 hours ago, thenshow the notification. If it's less than 3 hours ago, then set the next fire date appropriately; this means the timer fires (on average) at most every 1.5 hours, which is probably not as costly as repeatedly changing the fire-date.
See mach_absolute_time() for a relatively low-overhead timebase (pre-calculating what 3 hours is in mach_absolute_time units). It still takes about 3 microseconds, which is practically forever (1000 clocks cycles!).
If you're really worried about overhead, simply set an "activity" flag every time something happens, and use (e.g.) a 1 hour timer. When the timer fires, do something like if (activity) {counter = 0; activity = 0; } else { counter ++; if (counter == 3) { ... } }. It's debatable whether a couple of microseconds here and there are more costly than a timer firing every hour, but they're both pretty negligible.
The far bigger problem is that the accelerometer eats power and CPU time (and delivering updates takes CPU time). Setting updateInterval = 10 or so will reduce the overhead, and it's capped to a sensible value (around 1 s) by the OS.
Apple has a feature in multitasking that they call "Task finishing" use by - (UIBackgroundTaskIdentifier)beginBackgroundTaskWithExpirationHandler:(void(^)(void))handler
Found here and more stuff here
Can't you just send a message to your NSTimer whenever a video starts, to invalidate the timer? Then when the movie ends, start the timer back up.
I am wondering what happen if my NSTimer fires every 1 second and my function takes 2 seconds to perform the task.
Thanks for your answer.
Thierry
Is your function blocking? According to the documentation,
A timer is not a real-time mechanism; it fires only when one of the run loop modes to which the timer has been added is running and able to check if the timer’s firing time has passed. If a timer’s firing time occurs while the run loop is in a mode that is not monitoring the timer or during a long callout, the timer does not fire until the next time the run loop checks the timer. Therefore, the actual time at which the timer fires potentially can be a significant period of time after the scheduled firing time.
Therefore, your timer will not fire during the task if it is blocking.
I need to run my game loop with very accurate timing. I am trying to use NSTimer to do this, and I am getting ok results, but there is a bit of drift.
When NSTimer fires does the next time event start counting when the handler finishes or does it start counting straight away.
If the former is it reasonable for me to use setFireDate to try to offset the next timer firing - when I tried this things seemed worse.
My timer is set to fire every 44ms and I would like to stop it drifting by more than 20ms. If it does drift I would like to correct it for the next fire.
Is this a reasonable thing to try and do with NSTimer?
I don't think NSTimer will give you "very" accurate timing. It fires on the run loop, so if it's in your main thread then it'll get delayed by everything from UI updates to event handling.
You might try creating a thread and scheduling your timer on that thread's run loop. Having that timer as the only thing on that run loop should limit the number of things that can interfere with it.
If that doesn't work, well, you'll already have it working on a thread, so you may as well switch to a usleep() loop.
Also keep in mind that the NSTimer documentation states:
Because of the various input sources a
typical run loop manages, the
effective resolution of the time
interval for a timer is limited to on
the order of 50-100 milliseconds. If a
timer’s firing time occurs while the
run loop is in a mode that is not
monitoring the timer or during a long
callout, the timer does not fire until
the next time the run loop checks the
timer. Therefore, the actual time at
which the timer fires potentially can
be a significant period of time after
the scheduled firing time.