I have an iOS App that provides test for users with time limit.
The test will span across multiple view controllers, where these view controllers may re-open during the test flow. I think of the following flow:
In AppDelegate.h, add a NSTimer and time spent in float:
#property (strong, nonatomic) NSTimer *timer;
#property (nonatomic) float timeSpent;
- (void)startTimer;
- (void)stopTimer;
Without forgetting #synthesize the above, make a start & stop timer function in AppDelegate.m:
- (void)startTimer {
self.timer = [NSTimer scheduledTimerWithTimeInterval: 0.1f target: self
selector: #selector(updateTime) userInfo: nil repeats: YES];
}
- (void)stopTimer {
[self.timer invalidate];
}
In the periodically-called updateTime function, increment the value of timeSpent by 1.
Last, in each View Controller, obtain the timeSpent value and format it with the format I want, such as "01min 56s", or "01:56".
Is there a simpler way to do so?
Note:
No Internet connection available, and the test will last for around 10 minutes only; thus using Google Analytics is an overkill & not applicable in this case
You are correct that you need one NSTimer and variable to track the global time and..
What you are proposing, using the app delegate as a glorified singleton will work..
But please don't, this is not very good practice. This blog post has a nice brief description on why, in my opinion at least.
http://www.cocoawithlove.com/2008/11/singletons-appdelegates-and-top-level.html
Personally if it was me I would probably just use a dependency injection model and pass the NSTimer between the viewControllers. http://www.objc.io/issue-15/dependency-injection.html
In short, app delegate is probably the easiest and quickest way. But I would advise something a little more scalable if it is going to be anything other than a trivial app.
Hope this was useful :)
*Edit Sample code for singleton.
This should go at the top of singleton class to initialise it.
#interface
#property (nonatomic, strong) NSTimer *myTimer;
#end
#implementation MySingletonClass
+ (instancetype)shared
{
static MySingletonClass *_shared = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_shared = [[self alloc] init];
// any other initialisation you need
});
return _shared;
}
- (void)startTimer {
self.myTimer = [NSTimer scheduledTimerWithTimeInterval: 0.1f target: self
selector: #selector(updateTime) userInfo: nil repeats: YES];
}
- (void)stopTimer {
[self.myTimer invalidate];
}
then you can access it from any other classes in your program like this
#import "MySingletonClass.h"
//some method
- (void)myMethod
{
CGFloat currentTime = [MySingletonClass shared].globalTimeProperty;
// do something with the time
}
-(void)startTimer
{
[[MySingletonClass shared] startTimer];
}
-(void)updateTime
{
// do your update stuff here
}
Singleton header
#interface
#property (nonatomic, assign) CGFloat globalTimeProperty;
+ (instancetype)shared;
- (void)startTimer;
- (void)stopTimer;
#end
I may have missed some stuff but it should be enough to get you going.
Related
Question:
How can I make sure that the code executed due to a runloop event (timer, user interaction, performSelector, etc) have the same concept of "now"?
Background:
Say that event handler takes 100ms to execute, that means that [NSDate date] will return a slightly different "now" depending on when in the execution you make the call. If you are very unlucky with the timing you might even end up with different dates between the calls.
This creates problems for things that rely on the current time for doing various calculations since those calculations can differ during the execution.
Of course, for a specific event handler you could just store the date in the AppDelegate or similar or pass it on in each call starting from the entry point.
However, I want something safer and automatic. Ideally I want to know at what time the current run loop started processing the event. Something I can simply replace [NSDate date] with and always get the same result until the next event is fired.
I looked into the documentation of NSRunLoop without much luck. I also looked into CADisplayLink for potential workarounds. Neither provided a clear cut answer.
It feels like this should be a common thing to need, not something that needs "workarounds". My guess is that I am looking in the wrong places or using the wrong search terms.
Code Example:
UIView *_foo, _fie;
NSDate *_hideDate;
- (void)handleTimer
{
[self checkVisible:_foo];
[self checkVisible:_fie];
}
- (void)checkVisible:(UIView *)view
{
view.hidden = [_hideDate timeIntervalSinceNow] < 0];
}
In this case we could end up with _fie being hidden when _foo is still visible since "now" has changed by a very small amount between calls.
This is a very simplified example in which a fix is trivial by simply calling [NSDate date] and sending that instance to all callers. It is the general case that I am interested in though where call chains might be very deep, cyclic, re-entrant, etc.
NSRunLoop is a wrapper for CFRunLoop. CFRunLoop has features that NSRunLoop doesn't expose, so sometimes you have to drop down to the CF level.
One such feature is observers, which are callbacks you can register to be called when the run loop enters different phases. The phase you want in this case is an after-waiting observer, which is called after the run loop receives an event (from a source, or due to a timer firing, or due to a block being added to the main queue).
Let's add a wakeDate property to NSRunLoop:
// NSRunLoop+wakeDate.h
#import <Foundation/Foundation.h>
#interface NSRunLoop (wakeDate)
#property (nonatomic, strong, readonly) NSDate *wakeDate;
#end
With this category, we can ask an NSRunLoop for its wakeDate property any time we want, for example like this:
#import "AppDelegate.h"
#import "NSRunLoop+wakeDate.h"
#implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSTimer *timer = [NSTimer timerWithTimeInterval:0.5 repeats:YES block:^(NSTimer *timer){
NSLog(#"timer: %.6f", NSRunLoop.currentRunLoop.wakeDate.timeIntervalSinceReferenceDate);
}];
[NSRunLoop.currentRunLoop addTimer:timer forMode:NSRunLoopCommonModes];
return YES;
}
#end
To implement this property, we'll create a WakeDateRecord class that we can attach to the run loop as an associated object:
// NSRunLoop+wakeDate.m
#import "NSRunLoop+wakeDate.h"
#import <objc/runtime.h>
#interface WakeDateRecord: NSObject
#property (nonatomic, strong) NSDate *date;
- (instancetype)initWithRunLoop:(NSRunLoop *)runLoop;
#end
static const void *wakeDateRecordKey = &wakeDateRecordKey;
#implementation NSRunLoop (wakeDate)
- (NSDate *)wakeDate {
WakeDateRecord *record = objc_getAssociatedObject(self, wakeDateRecordKey);
if (record == nil) {
record = [[WakeDateRecord alloc] initWithRunLoop:self];
objc_setAssociatedObject(self, wakeDateRecordKey, record, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return record.date;
}
#end
The run loop can run in different modes, and although there are a small number of common modes, new modes can in theory be created on the fly. If you want an observer to be called in a particular mode, you have to register it for that mode. So, to ensure that the reported date is always correct, we'll remember not just the date but also the mode in which we recorded the date:
#implementation WakeDateRecord {
NSRunLoop *_runLoop;
NSRunLoopMode _dateMode;
NSDate *_date;
CFRunLoopObserverRef _observer;
}
To initialize, we just store the run loop and create the observer:
- (instancetype)initWithRunLoop:(NSRunLoop *)runLoop {
if (self = [super init]) {
_runLoop = runLoop;
_observer = CFRunLoopObserverCreateWithHandler(nil, kCFRunLoopEntry | kCFRunLoopAfterWaiting, true, -2000000, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
[self setDate];
});
}
return self;
}
When asked for the date, we first check whether the current mode is different from the date in which we recorded the mode. If so, then the date wasn't updated when the run loop awoke in the current mode. That means the observer wasn't registered for the current mode, so we should register it now and update the date now:
- (NSDate *)date {
NSRunLoopMode mode = _runLoop.currentMode;
if (![_dateMode isEqualToString:mode]) {
// My observer didn't run when the run loop awoke in this mode, so it must not be registered in this mode yet.
NSLog(#"debug: WakeDateRecord registering in mode %#", mode);
CFRunLoopAddObserver(_runLoop.getCFRunLoop, _observer, (__bridge CFRunLoopMode)mode);
[self setDate];
}
return _date;
}
When we update the date, we also need to update the stored mode:
- (void)setDate {
_date = [NSDate date];
_dateMode = _runLoop.currentMode;
}
#end
An important warning about this solution: the observer fires once per pass through the run loop. The run loop can service multiple timers and multiple blocks added to the main queue during a single pass. All of the serviced timers or blocks will see the same wakeDate.
I have this below error
-[__NSPlaceholderArray initWithObjects:count:]: attempt to insert nil object from objects[1539]
It happens sometimes I try to tap several times on screen, because code is little, so all the code is pasted below
#interface ViewController ()
#property (nonatomic,weak) NSTimer *timer;
#property (nonatomic,strong)NSMutableArray * testArray;
#property (nonatomic,strong) dispatch_queue_t queue1;
#property (nonatomic,strong) dispatch_queue_t queue2;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.testArray = [NSMutableArray array];
_queue1 = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
_queue2 = dispatch_queue_create("test",DISPATCH_QUEUE_SERIAL);
NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:#selector(addObjectforArray) userInfo:nil repeats:YES];
[timer fire];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
dispatch_async(_queue2, ^{
NSLog(#"touchesBeganThread:%#",[NSThread currentThread]);
NSArray * testTempArray = [NSArray arrayWithArray:self.testArray];
for (UIView *view in testTempArray) {
NSLog(#"%#",view);
}
});
}
- (void)addObjectforArray{
dispatch_async(_queue1, ^{
NSLog(#"addObjectThread:%#",[NSThread currentThread]);
[self.testArray addObject:[[UIView alloc]init]];
});
}
I can not understand why this happens, if I change _queue1 to DISPATCH_QUEUE_SERIAL, it becomes normal.
How can I understand this issue? If anyone could shed some light, that would be wonderful.
There are multiple problems in your code. They can cause all sorts of bugs randomly.
UIView should be created in the main thread using dispatch_get_main_queue().
https://developer.apple.com/documentation/uikit
For the most part, use UIKit classes only from your app’s main thread or main dispatch queue. This restriction applies to classes derived from
UIResponder
or that involve manipulating your app’s user interface in any way.
The property testArray is nonatomic but being accessed in two threads. The property should be atomic. It runs fine at this moment but it is fragile. If in the future testArray mutates, the app will crash randomly.
NSArray is not thread-safe. It should be locked while accessing in multiple threads or protected by other means.
As pointed out by #Nirmalsinh, the dispatch_async is redundant (actually harmful).
I am not sure if you have heavily simplified your code or only to test something. If you are not doing long running work, you might want to use dispatch_get_main_queue() in dispatch_async. It will save you from a lot of troubles.
It seems you are inserting nil value into your array. You cannot add nil to array or dictionary.
- (void)addObjectforArray{
NSLog(#"addObjectThread:%#",[NSThread currentThread]);
UIView *view = [[UIView alloc] init];
if(view != nil)
[self.testArray addObject:view];
}
There is no required to use a queue in the method. You are already using NSTimer for same.
Try to check above. It will help you.
I am trying to make a timer app. I am fine with Play button but I couldnt get Pause button working. I have seen some tutorials on Timer Apps and most of them have only used: [timer invalidate] code for that method that solely stops the time that is currently being shown in the label (display). Even that code doesn't work for me so I tried doing this which makes kinda more sense but still, of no luck.
#implementation ViewController
int timerCounter=0;
NSTimer *timer;
NSString *label;
BOOL isPaused=NO;
-(IB Action) playButton:(id)sender{
[timer invalidate];
isPaused=NO;
timer=[NSTimer scheduledTimerWithTimeInterval:1 target:self selector: #selector(tick) userInfo:nil repeats:YES];
}
-(IBAction) pauseButton:(id)sender{
[timer invalidate];
isPaused=YES;
label=[NSString stringWithFormat:#"%d",timerCounter];
_labelTimer.text=label;
}
-(void) tick{
if (isPaused==NO){
timerCounter++;
}
label=[NSString stringWithFormat:#"%d",timerCounter];
_labelTimer.text=label;
}
The NSTimer API do not have any method for pausing. What is available is either fire or invalidate. About your code, You are using global variables - not a good practice, most probably the instance of timer you are calling is not the same, remove them and add a property in the class extension in .m instead:
#property (nonatomic, strong) NSTimer * timer;
you then address that property with self.timer.
If this does not help, check if the button call the method when you press it.
I can't figure out what is wrong in code pasted below, im learing xcode with Todd Moore book, this is the chapter 2-hello pong, Im getting breakpoint 1.1 crash at scheduledTimerWithTimeInterval. In book at the end of timer configuration is added ] retain]; but xcode 5 says that's deperecated.I modificate this to compile without retain, but the problem is crash.
- (void)animate
{
_puck.center = CGPointMake(_puck.center.x + dx*speed,_puck.center.y + dy*speed);
}
- (void)start
{
if (timer == nil) {
timer = [NSTimer scheduledTimerWithTimeInterval:0.016 target:self selector:#selector(animate) userInfo:NULL repeats:YES];
}
_puck.hidden = NO;
}
Because you are in an ARC setting, automatic reference counting, the object that is doing the animate/start may not be in memory anymore depending on how the rest of the code worked.
If whatever object that is used to have a retain it may not be in memory when that timer gets called.
You may need to do a quick search for how to start a project without automatic reference counting so you can follow the examples in the book more directly.
someObject = [[something alloc] init] retain];
[someObject start];
If you are not storing someObject somewhere that is likely the problem.
[edit]
I found the source code for that Todd Moore example.
These changes should hopefully let it work with ARC.
get rid of this in PaddlesViewController.h
NSTimer *timer;
add (near other #properties)
#property (nonatomic, retain) NSTimer *timer;
in PaddlesViewController.m add (near other #synthesize)
#synthesize timer;
This should allow it to run without the retain
I don't want to create NSTimer object. How do I invalidate timer? I want to invalidate timer in viewWillDisappear.
-(void) viewDidLoad
{
[super viewDidLoad];
[NSTimer scheduledTimerWithTimeInterval:10 target:self selector:#selector(onTimer:) userInfo:nil repeats:YES];
}
A
you have to hold on to the timer you create:
#interface MONObject ()
#property (nonatomic, retain) NSTimer * timerIvar;
#end
#implementation MONObject
...
- (void)viewDidLoad
{
[super viewDidLoad];
self.timerIvar = [NSTimer scheduledTimerWithTimeInterval:10 target:self selector:#selector(onTimer:) userInfo:nil repeats:YES];
}
- (void)invalidateTimer
{
[self.timerIvar invalidate];
self.timerIvar = nil;
}
- (void)viewWillDisappear:(BOOL)animated
{
...
[self invalidateTimer];
}
B
another option would be to invalidate the timer that is passed in the callback, but that won't occur within viewDidUnload:. therefore, it doesn't quite apply in this scenario:
- (void)onTimer:(NSTimer *)pTimer
{
[pTimer invalidate];
}
If you want to be able to cancel the timer, you have to refer to the timer you’re cancelling, and that means you have to keep the pointer to the timer around, see justin’s answer.
Keeping a reference to the timer is the right way to do it, but for the sake of completeness you may also use the -performSelector:withObject:afterDelay: method as a poor man’s timer. That call may be invalidated using +cancelPreviousPerformRequestsWithTarget:. Sample code:
- (void) viewDidLoad
{
[super viewDidLoad];
[self performSelector:#selector(timerTick) withObject:nil afterDelay:10];
}
And then:
- (void) viewWillDisappear
{
[NSObject cancelPreviousPerformRequestsWithTarget:self];
[super viewWillDisappear];
}
But this is not the right way to do it, because there might be other perform-selector requests pending on your object that you would cancel. It’s best to keep your timer around, that way you know exactly what you’re cancelling.
By the way, it’s also probably a bad idea to run a timer in -viewDidLoad. View loading may happen anytime, without any relation to view being displayed.
Maybe this method can help you:
[self performSelector:#selector(onTimer:) withObject:nil afterDelay:10];
If you don't want to hold on to your timer, the NSTimer object will be passed to the timer method (in your case onTimer:), so in that method you could check whether the timer is still needed and invalidate it. However, you will run into trouble if the view comes back before you invalidated the timer, and you create a new one.
By far the best way is to store the timer into an instance variable. It works, no clever tricks, and you'll know six months later what you did. I'd probably write a
#property (readwrite, nonatomic) BOOL hasTimer;
getter returns YES iff the timer is not nil, setter invalidates the timer or creates a new one.