There's one style issue I want to address, and then I'll talk bigger picture.
This comment may turn irrelevant in this specific case, because bigger picture, the better option is probably going to be to use NSTimer
, and I'll go over some samples of how to use that, but this comment is still useful for other scenarios, so here it goes:
-(void) update {
switch (self.state) {
case CountdownNotStarted:
break;
case CountdownCounting:
[self doCountdown];
break;
case CountdownPaused:
break;
case CountdownFinished:
break;
default:
break;
}
}
We have 4 possible conditions (not including default:
), but only 2 possible results. What you're doing here is on par with something like this:
NSUInteger foo;
// stuff
switch (foo) {
case 0: break;
case 1: [self doCountdown];
case 2: break;
case 3: break;
case 4: break;
// etc ad naseum
case NSUIntegerMax: break;
default: break;
}
Listing EVERY possible value for an NSUInteger
in a switch statement in which we only care about 1 (or even a couple) value seems absurd, right? It's no less absurd to do this for an NS_ENUM
.
After all, your NS_ENUM
is actually backed by some sort of primitive data type. And in this case we have only 4 different values, but it doesn't mean there can't be more. For my current project at work, one of my enums already has 100+ defined values, and will easily have hundreds more by the time the project is done, yet most of the time when I switch on variables of this type, I only care about one specific value, sometimes two or three more.
There are three possible options on how to rewrite this into something better.
- A simple
if-else
. If the specific scenario means it's extraordinarily unlikely that we'll only ever care whether or not the variable matches this one specific variable, and we don't need an if-else if-else
or anything more complex than a simple if-else
, then we're fine to just use an if-else
:
if (self.state == CountdownCounting) {
[self doCountdown];
}
And in this specific case, we don't even need the else.
- Let conditions with that should execute the same block of code simply fallthrough:
switch(self.state) {
case CountdownCounting:
[self doCountdown];
break;
case CountdownNotStarted:
case CountdownPaused:
case CountdownFinished:
default:
break;
}
This one explicitly lists each condition, and this is fine if the enum is well defined, small, and unlikely to contain more than a handful of values. However, this specific syntax will not work with swift switch
statement. You'd have to add the fallthrough
keyword to each of those empty cases to achieve the exact same code, and at that point, we're not any better than the original situation of just break
-ing from each. Since iOS development will likely move quickly to swift, I'd recommend against getting into the habit of this sort of switch
structure if you're not already in it.
- Let
default:
handle the default cases. Any case for which the code you want to be executed matches exactly to the code in default:
can simply be omitted.
switch (self.state) {
case CountdownCounting:
[self doCountdown];
default:
break;
}
Now this is the same logic as the if-else
above. It makes it more clear that we only care about this specific case. However, compared to the if-else
above, this is easier to add other values and scenarios. And this structure will work perfectly fine exactly as-is barring syntax discrepancies if/when you make the transition to swift. This is absolutely the style I would recommend, although all three options are better than what you currently have I believe.
Now then, as I mentioned before (and as you mentioned in the question), NSTimer
is probably the answer here.
You said you'd heard of it, but not messed with it yet. NSTimer
is no different from any of the other Apple classes. It may seem a little confusing or overwhelming at first, but the documentation is great, and it really only takes using it for yourself once or twice to get pretty settled in and comfortable with how the class works, so here's a few pointers on NSTimer
.
There are 5 methods for creating a timer. Of these 5, I personally only ever use 1 of them. One of the methods is an init
method, where the other 4 are factory methods, convenience methods that do some extra set up for you.
Of these 4 factory methods, there's two groups, scheduled and unscheduled. The two methods that start with the phrase timerWithTimeInterval:
create unscheduled timers. The two that start with the phrase scheduledTimerWithTimeInterval:
are scheduled timers. The only difference between creating timers with these two groups is that the former need to be scheduled after they've been created while the latter do not. The latter are scheduled to start counting time from the instant they're instantiated.
So let's focus on the two scheduled timers as these are the ones that are really the most useful.
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds
invocation:(NSInvocation *)invocation
repeats:(BOOL)repeats;
+ (NSTimer *)scheduledTimerWithTimerInverval:(NSTimeInterval)seconds
target:(id)target
selector:(SEL)aSelector
repeats:(BOOL)repeats;
So, there are three parts to these timers.
- The timer interval between now and when the timer should fire.
- The action that should be taken when the timer fires.
- Whether or not the action should repeat.
The first method solves part two with a single argument, an NSInvocation
class, something I'm not familiar with, but it's basically an Objective-C message rendered static--an action turned into an argument.
The second method solves part two with two arguments. A target and a selector. The target is a reference to the object you want to call a method on, and the selector is the method you want the timer to call on that target.
The method should take a single argument of type NSTimer
as the timer object sends itself as the argument to the method it calls. This is particularly useful because NSTimer
has a NSDictionary
property called userInfo
in which you can store any information you want when you create the timer so that the method that gets called can retrieve the information stored.
So, a quick rewrite of your class using NSTimer
might look something like this:
In interface:
@property (nonatomic,strong) NSTimer *timer;
In implementation:
- (void)startTimer {
self.state = CountdownCounting;
self.timer = [NSTimer scheduledTimerWithInterval:0.25f
target:self
selector:@selector(update:)
repeats:YES];
}
- (void)restartTimer {
[self cancelTimer];
[self startTimer];
}
- (void)cancelTimer {
if (self.timer.isValid) {
[self.timer invalidate];
}
self.state = CountdownNotCounting;
}
- (void)update:(NSTimer *)timer {
// logic for whatever you want to happen each time the timer fires
}
In the update:
method, you don't need any special logic relating to the timer.
When we call invalidate
on the timer, it stops the timer. Because we sent YES
for the repeats argument, it will automatically recall update:
every 0.25 seconds until we stop the timer.
Although, as you can see, NSTimer
is really quite simple to use, and arguably doesn't warrant a wrapper class.
One important thing to watch out for with NSTimer
however. While the timer is valid, the timer object holds a strong reference to the value sent for the target:
argument. This is important to note because in most cases the timer is a property on a given object with a strong reference to it (so the timer doesn't deallocate) and the object will typically send itself as the target
argument. So we're knowingly and intentionally creating a retain cycle for the timer period during which the timer is valid.
This is mostly fine. If we sent NO
as the argument for repeats:
, then the retain cycle only lasts until the timer fires, at which point the method is called on the object (which is better than a method being called on a deallocated object, which would crash the app). Once the method returns, NSTimer
releases its strong reference to the object, the retain cycle is broken, the object deallocates, leaving no strong references to the NSTimer object, so it deallocates, and now everything is fine.
NSTimer also breaks its strong reference when it is invalidated however, so all you have to do to break an NSTimer retain cycle is simply call invalidate
on it. This is important to remember when you've sent YES
for the repeats:
argument.