So I am trying to set up a basic timer but I am failing miserably. Basically all I want is to start a 60 second timer when the user clicks a button, and to update a label with the time remaining(like a countdown). I created my label and button and connected them in IB. Next I created a IBAction for the button. Now when I tried to update the label based on the timer, my app screws up. Here's my code:
NSTimer *t = [NSTimer scheduledTimerWithTimeInterval: 1
target: self
selector:#selector(updateLabelDisplay)
userInfo: nil repeats:YES];
I also have an updateLabelDisplay function that determines how many times the timer has ran and then subtracted that number from 60 and displays that number in the countdown label. Can anyone tell me what I am doing wrong?
Ok, well for starters, check this out if you haven't already: Official Apple Docs about Using Timers
Based on your description, you probably want code that looks something like this. I've made some assumptions regarding behavior, but you can suit to taste.
This example assumes that you want to hold on to a reference to the timer so that you could pause it or something. If this is not the case, you could modify the handleTimerTick method so that it takes an NSTimer* as an argument and use this for invalidating the timer once it has expired.
#interface MyController : UIViewController
{
UILabel * theLabel;
#private
NSTimer * countdownTimer;
NSUInteger remainingTicks;
}
#property (nonatomic, retain) IBOutlet UILabel * theLabel;
-(IBAction)doCountdown: (id)sender;
-(void)handleTimerTick;
-(void)updateLabel;
#end
#implementation MyController
#synthesize theLabel;
// { your own lifecycle code here.... }
-(IBAction)doCountdown: (id)sender
{
if (countdownTimer)
return;
remainingTicks = 60;
[self updateLabel];
countdownTimer = [NSTimer scheduledTimerWithTimeInterval: 1.0 target: self selector: #selector(handleTimerTick) userInfo: nil repeats: YES];
}
-(void)handleTimerTick
{
remainingTicks--;
[self updateLabel];
if (remainingTicks <= 0) {
[countdownTimer invalidate];
countdownTimer = nil;
}
}
-(void)updateLabel
{
theLabel.text = [[NSNumber numberWithUnsignedInt: remainingTicks] stringValue];
}
#end
It may be a little late to post a second answer to this question but I've been looking for a good place to post my own solution to this problem. In case it is of use to anyone here it is. It fires 8 times but of course this can be customised as you please. The timer deallocates itself when time is up.
I like this approach because it keeps the counter integrated with the timer.
To create an instance call something like:
SpecialKTimer *timer = [[SpecialKTimer alloc] initWithTimeInterval:0.1
andTarget:myObject
andSelector:#selector(methodInMyObjectForTimer)];
Anyway, here are the header and method files.
//Header
#import <Foundation/Foundation.h>
#interface SpecialKTimer : NSObject {
#private
NSTimer *timer;
id target;
SEL selector;
unsigned int counter;
}
- (id)initWithTimeInterval:(NSTimeInterval)seconds
andTarget:(id)t
andSelector:(SEL)s;
- (void)dealloc;
#end
//Implementation
#import "SpecialKTimer.h"
#interface SpecialKTimer()
- (void)resetCounter;
- (void)incrementCounter;
- (void)targetMethod;
#end
#implementation SpecialKTimer
- (id)initWithTimeInterval:(NSTimeInterval)seconds
andTarget:(id)t
andSelector:(SEL)s {
if ( self == [super init] ) {
[self resetCounter];
target = t;
selector = s;
timer = [NSTimer scheduledTimerWithTimeInterval:seconds
target:self
selector:#selector(targetMethod)
userInfo:nil
repeats:YES];
}
return self;
}
- (void)resetCounter {
counter = 0;
}
- (void)incrementCounter {
counter++;
}
- (void)targetMethod {
if ( counter < 8 ) {
IMP methodPointer = [target methodForSelector:selector];
methodPointer(target, selector);
[self incrementCounter];
}
else {
[timer invalidate];
[self release];
}
}
- (void)dealloc {
[super dealloc];
}
#end
Related
I have a piece of ARC code written in Objective-C that is creating a block and will want to provide it to some Objective-C MRC code as a callback, to be called when an operation finishes. What is the safest way to do that? My hunch is to just provide the MRC code with a copy.
Is there a guide somewhere that explains the memory management issues that arise when mixing ARC and MRC code?
Provided you handle the request in-place, you don't need to do anything special under MRR, since blocks themselves make const copies of the referenced values. Thus in this example the callback parameter is retained automagically (and doesn't get released until after dispatch_after's block finishes):
- (void)calculateResult:(void (^)(NSInteger))callback {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
callback(_value);
});
}
If you want to retain the callback and use it later on, then you should explicitly copy the block passed OR still copy it via synthesised properties with copy semantic:
NS_ASSUME_NONNULL_BEGIN
#interface TDWMrrObject ()
#property (copy, nonatomic, nullable) void(^callback)(NSInteger);
#property (assign, nonatomic, nullable) NSTimer *timer;
#property (assign, nonatomic) NSInteger value;
- (void)asyncCalculate;
#end
NS_ASSUME_NONNULL_END
#implementation TDWMrrObject
#pragma mark Lifecycle
- (instancetype)init {
if (self = [super init]) {
_value = 4;
}
return self;
}
- (void)dealloc {
[_callback release];
[_timer invalidate];
[super dealloc];
}
#pragma mark Actions
- (void)calculateResult:(void (^)(NSInteger))callback {
self.callback = callback;
[self asyncCalculate];
}
#pragma mark Private
- (void)asyncCalculate {
if (_timer) {
[_timer invalidate];
}
self.timer = [NSTimer scheduledTimerWithTimeInterval:4 repeats:NO block:^(NSTimer * _Nonnull timer) {
_callback(_value);
self.callback = nil;
self.timer = nil;
}];
}
#end
I've developed a Category that gives the NSOperation the ability to be executed in the background at timed intervals. I would really appreciate getting some feedback on this, especially any potential problems with this approach that I'm not thinking of.
Thank you!
Here's the code:
NSOperation+Repeat.h
#import <Foundation/Foundation.h>
#interface NSOperation (repeat)
#property (readonly, nonatomic) NSTimeInterval repeatInterval;
#property (readonly, nonatomic) NSOperationQueue *repeatOperationQueue;
- (void)performUsingOperationQueue:(NSOperationQueue *)operationQueue;
- (void)performAtRepeatingInterval:(NSTimeInterval)interval usingOperationQueue:(NSOperationQueue *)operationQueue;
#end
NSOperation+Repeat.m
#import "NSOperation+repeat.h"
#import <objc/runtime.h>
static char const * const RepeatPropertiesKey = "RepeatProperties";
#implementation NSOperation (repeat)
#dynamic repeatInterval;
#dynamic repeatOperationQueue;
static NSString * RepeatIntervalKey = #"interval";
static NSString * RepeatOperationQueueKey = #"operationQueue";
static NSString * RepeatTimerKey = #"timer";
- (NSMutableDictionary *)repeatProperties {
NSMutableDictionary * properties = objc_getAssociatedObject(self, RepeatPropertiesKey);
if (properties == nil) {
properties = [NSMutableDictionary new];
objc_setAssociatedObject(self, RepeatPropertiesKey, properties, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return properties;
}
- (NSTimeInterval)interval {
NSNumber * interval = [[self repeatProperties] objectForKey:RepeatIntervalKey];
return [interval doubleValue];
}
- (NSOperationQueue *)repeatOperationQueue {
NSOperationQueue * operationQueue = [[self repeatProperties] objectForKey:RepeatOperationQueueKey];
return operationQueue;
}
- (void)performUsingOperationQueue:(NSOperationQueue *)operationQueue {
[operationQueue addOperation:[self copy]];
}
- (void)performAtInterval:(NSTimer *)timer {
[self performUsingOperationQueue:self.repeatOperationQueue];
}
- (void)performAtRepeatingInterval:(NSTimeInterval)interval usingOperationQueue:(NSOperationQueue *)operationQueue {
// Save interval and operationQueue in repeatProperties
[self.repeatProperties setValue:[NSNumber numberWithDouble:interval] forKey:RepeatIntervalKey];
[self.repeatProperties setValue:operationQueue forKey:RepeatOperationQueueKey];
// Create timer to call performAtInterval on self
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:(interval*60)
target:self
selector:#selector(performAtInterval:)
userInfo:nil
repeats:YES];
// Save the timer in repeatProperties
[self.repeatProperties setValue:timer forKey:RepeatTimerKey];
[self performUsingOperationQueue:operationQueue];
}
#end
Here's an example of a NSOperation subclass that can repeat:
MSScheduleImportOperation.h
#import <Foundation/Foundation.h>
#import "NSOperation+Repeat.h"
#interface MSScheduleImportOperation : NSOperation <NSCopying>
#property (readonly, strong, nonatomic) NSString* employeeId;
- (id)initWithEmployeeId:(NSString *)employeeId;
#end
MSScheduleImportOperation.m
#import "MSScheduleImportOperation.h"
#implementation MSScheduleImportOperation
#synthesize employeeId = __employeeId;
- (id)initWithEmployeeId:(NSString *)employeeId {
self = [super init];
__employeeId = [employeeId copy];
return self;
}
- (id)copyWithZone:(NSZone *)zone {
MSScheduleImportOperation* copy = [[MSScheduleImportOperation alloc] initWithEmployeeId:self.employeeId];
return copy;
}
- (void)main
{
...
}
#end
The Apple documentation says:
An operation object is a single-shot object—that is, it executes its task once and cannot be used to execute it again.
So the first problem is that there might be internals that stop it from working. Although, I see you try to get around the problem by making a copy.
This leads us to the other problem is that NSOperation is not advertised to conform to NSCopying.
[operationQueue addOperation:[self copy]];
This line should throw an exception.
Instead of a category on NSOperation where the object copies itself and adds the copy to an NSOperationQueue - it would be simpler to manage this at a higher level. For example:
+[RepeatingOperation operationBlock:(InitOperationBlock)operationBlock
queue:(NSOperationQueue*)queue
interval:(NSTimeInterval)interval];
where InitOperationBlock would be a block where the operation was created and configured.
The main benefit is the API would be harder to mess up. For example in category in the original post, performUsingOperationQueue: will fail silently if you forget to set repeatOperationQueue.
I have problem with NSTimer in Objective-C. This is my source code:
Main.m
#import <Foundation/Foundation.h>
#import "TimerTest.h"
int main(int argc, const char * argv[]) {
#autoreleasepool {
TimerTest *timerTest = [[[TimerTest alloc] init] autorelease];
}
return 0;
}
TimerTest.h
#import <Foundation/Foundation.h>
#interface TimerTest : NSObject {
NSTimer *_timer;
}
#property (nonatomic, retain) NSTimer *timer;
- (id) init;
#end
TimerTest.m
#import "TimerTest.h"
#implementation TimerTest
#synthesize timer = _timer;
- (id) init {
if (self = [super init]) {
[NSTimer timerWithTimeInterval:0.5f
target:self
selector:#selector(tick:)
userInfo:nil
repeats:YES];
}
return self;
}
- (void) tick: (NSDate *) dt {
NSLog(#"Tick! \n");
}
- (void) dealloc {
self.timer = nil;
[super dealloc];
}
#end
My program should log "Tick! \n" every 0.5 second. But then my program is finished, xcode console is clear, that's meaning that NSLog in
-(void)tick:(NSDate *)dt method didn't work . Where is my mistake?
My program should log "Tick! \n" every 0.5 second.
No it shouldn't (at least not according to the code you posted). You need a run loop. Timers only fire as events on run loops. So, in your main, you need to set one up and run it.
Not only do you need an event loop, but you've created a timer, and you haven't scheduled it in said run loop. Instead of:
[NSTimer timerWithTimeInterval:0.5f
target:self
selector:#selector(tick:)
userInfo:nil
repeats:YES];
do this:
[NSTimer scheduledTimerWithTimeInterval:0.5f
target:self
selector:#selector(tick:)
userInfo:nil
repeats:YES];
I set your code up in the context of a Cocoa application (because it comes with a run loop), executing the TimerTest allocation in the applicationDidFinishLaunching of the delegate, and your code otherwise works.
A couple of other things: The method whose selector is passed to scheduledTimerWithTimerInterval:... should be of the form
- (void)timerMethod:(NSTimer *)aTimer
and when you're done with your timer, just invalidate it:
[timer invalidate];
though you have to keep a reference to the timer to do this, which it appears you hadn't.
Need help with a problem.
Goal
I'm putting together an iOS book app that uses NSTimers to fire off several staggered animation events after loading a view. I've created a MethodCallerWithTimer class to help me do this (code at bottom).
My Solution So Far
When I use the MethodCallerWithTimer class, I assign the objectOwningMethod as my UIViewController subclass object (it's a book page), and then the method as an instance method in that class. Here is an example of a method I assign - quite simply turning on some artwork on the screen:
- (void) playEmory {
[emoryRedArt setHidden:NO];
}
My Issue
When I create multiple MethodCallerWithTimer instances then load the view and start them all, I only ever get the FIRST event to happen. None of the other timers call their target methods. I suspect I don't understand what I'm asking NSRunLoop to do or something similar.
Any thoughts?
Here is my MethodCallerWithTimer class:
#interface MethodCallerWithTimer : NSObject {
NSTimer * timer;
NSInvocation * methodInvocationObject;
NSNumber * timeLengthInMS;
}
- (id) initWithObject: (id) objectOwningMethod AndMethodToCall: (SEL) method;
- (void) setTime: (int) milliseconds;
- (void) startTimer;
- (void) cancelTimer;
#end
And implementation:
#import "MethodCallerWithTimer.h"
#implementation MethodCallerWithTimer
- (id) initWithObject: (id) objectOwningMethod AndMethodToCall: (SEL) method {
NSMethodSignature * methSig = [[objectOwningMethod class] instanceMethodSignatureForSelector:method];
methodInvocationObject = [NSInvocation invocationWithMethodSignature:methSig];
[methodInvocationObject setTarget:objectOwningMethod];
[methodInvocationObject setSelector:method];
[methSig release];
return [super init];
}
- (void) setTime: (int) milliseconds {
timeLengthInMS = [[NSNumber alloc] initWithInt:milliseconds];
}
- (void) startTimer {
timer = [NSTimer scheduledTimerWithTimeInterval:([timeLengthInMS longValue]*0.001) invocation:methodInvocationObject repeats:NO];
}
- (void) cancelTimer {
[timer invalidate];
}
-(void) dealloc {
[timer release];
[methodInvocationObject release];
[timeLengthInMS release];
[super dealloc];
}
#end
These look like one-time firings after a delay; have you considered using something like:
[myObject performSelector:#selector(playEmory) withObject:nil afterDelay:myDelay];
where myObject is the instance with the playEmory routine and myDelay is a float of the seconds you want the OS to wait before making the call?
You can find out more information about this flavor of performSelector here.
I'm new with Objective-C, so there probably is a simple solution to this.
I want a number to increment, but each iteration to be show on a label. (for example, it shows 1, 2, 3, 4, 5... displayed apart by an amount of time).
I tried:
#import "testNums.h"
#implementation testNums
- (IBAction)start:(id)sender {
int i;
for(i = 0; i < 10; ++i)
{
[outputNum setIntValue:i];
sleep(1);
}
}
#end
and all it did was wait for 9 seconds (apparently frozen) and then displayed 9 in the text box.
To allow the run loop to run between messages, use an NSTimer or delayed perform. Here's the latter:
- (IBAction) start:(id)sender {
[self performSelector:#selector(updateTextFieldWithNumber:) withObject:[NSNumber numberWithInt:0] afterDelay:1.0];
}
- (void) updateTextFieldWithNumber:(NSNumber *)num {
int i = [num intValue];
[outputField setIntValue:i];
if (i < 10)
[self performSelector:#selector(updateTextFieldWithNumber:) withObject:[NSNumber numberWithInt:++i] afterDelay:1.0];
}
Here's one timer-based solution. You may find it easier to follow. You could set the text field's value from the text field:
#interface TestNums: NSObject
{
IBOutlet NSTextField *outputField;
NSTimer *timer;
int currentNumber;
}
#end
#implementation TestNums
- (IBAction) start:(id)sender {
timer = [[NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:#selector(updateTextField:)
userInfo:nil
repeats:YES] retain];
//Set the field's value immediately to 0
currentNumber = 0;
[outputField setIntValue:currentNumber];
}
- (void) updateTextField:(NSTimer *)timer {
[outputField setIntValue:++currentNumber];
}
#end
Here's an even better (cleaner) timer-based solution, using a property. You'll need to bind the text field to the property in Interface Builder (select the field, press ⌘4, choose your object, and enter currentNumber as the key to bind to).
#interface TestNums: NSObject
{
//NOTE: No outlet this time.
NSTimer *timer;
int currentNumber;
}
#property int currentNumber;
#end
#implementation TestNums
#synthesize currentNumber;
- (IBAction) start:(id)sender {
timer = [[NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:#selector(updateTextField:)
userInfo:nil
repeats:YES] retain];
//Set the field's value immediately to 0
self.currentNumber = 0;
}
- (void) updateTextField:(NSTimer *)timer {
self.currentNumber = ++currentNumber;
}
#end
The property-based solution has at least two advantages:
Your object doesn't need to know about the text field. (It is a model object, separate from the view object that is the text field.)
To add more text fields, you simply create and bind them in IB. You don't have to add any code to the TestNums class.
Yes, because that is what you told it to do. The graphics will not actually update until the main run loop is free to display them. You'll need to use NSTimer or some such method to do what you want.
A better question might be why you want to do this?