I am making a timer app on XCode 7 using Objective-C and I would like to disable the button that is connected to the action startCount while the following code is being executed
- (IBAction)startCount:(id)sender {
countInt = 0;
self.Label.text = [NSString stringWithFormat:#"%i", countInt];
timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(countTimer) userInfo:nil repeats:YES];
}
What line of code would I need to add to this to disable the button connected to the action startCount?
Thanks
You need to disable the sender via the enabled flag:
((UIButton*)sender).enabled = false;
Don't forget to re-enable the button after the timer finishes.
If the enabled state is NO, the control ignores touch events [...]
Alternatively to the cast I made in the above code: Change your method signature to take in a UIButton*, not just an id, that way you can make sure the cast will not fail. A slight variation of the cast would be to cast to UIControl* instead.
If I understand you correctly: to make your code easier to understand (and to avoid unpleasant race conditions if there are multiple buttons that might be tapped/disabled), I would suggest avoiding using a timer. Instead, consider dispatch_after() like this:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
sender.userInteractionEnabled = false;
});
We get sender being passed into the method, and it will be the button that was tapped. 1 * NSEC_PER_SEC means "delay for one second."
Related
I have a number of methods that insert data into database table.These methods are called one after the other. While the insertion process in on, I want my UI to show appropriate message.
eg-
[dbManager insertIntoTable_ZADDED_FOOD_ITEMS_WithData:[resultObj objectForKey:#"food"]];
[dbManager insertIntoTable_ZUSDA_FOODS_WithData:[resultObj objectForKey:#"usda_food"]]
[dbManager insertIntoTable_ZDAILY_MENU_RECIPE_WithData:[resultObj objectForKey:#"daily_menu_recipe"]];
So, when I say
[dbManager insertIntoTable_ZADDED_FOOD_ITEMS_WithData:[resultObj objectForKey:#"food"]];
label on ui must show 'Updating Added Food Items...'
then when next line comes
[dbManager insertIntoTable_ZUSDA_FOODS_WithData:[resultObj objectForKey:#"usda_food"]];
label should read as 'Updating Usda Food' and so on.
I want the text on label to remain at least for a second. I have used 'dispatch_after' but still not able to get the desired effect.
Is there a way to insert delays between method calls and also to make sure that second method is called only when first one is completed.
Use the Command Pattern its a well known design pattern, basically its a list and each item has state, when one finishes the next one starts.
this case will allow you to not have a completion block inside a completion block inside a completion block.
You can make use of completion block :)
change your method to take a completion block as an arguement and once done with the job execute the completion block :) and in completion block start the next method call :)
You can modify your method signature as follow
-(void)insertIntoTable_ZADDED_FOOD_ITEMS_WithData:(NSData *)data andCompletionBlock:(void (^)())completionBlock{
//do whatever you want to do here
//once done simply say
completionBlock();
}
and you can call this method as :)
[dbManager insertIntoTable_ZADDED_FOOD_ITEMS_WithData:[resultObj objectForKey:#"food"] andCompletionBlock:^{
//call next method
//[dbManager insertIntoTable_ZUSDA_FOODS_WithData:[resultObj objectForKey:#"usda_food"] andCompletionBlock:^{
}];
}];
And in case you dont wanna send any completion block simply pass nil :)
But in that case dont forget to add the check as if(completionBlock) before calling completionBlock() app will crash in case it is passed as nil :)
on a second thought :)
if your method insertIntoTable_ZADDED_FOOD_ITEMS_WithData: is synchronous :) you can change the label directly like
yourLabel.text = #"Updating Added Food Items...";
[dbManager insertIntoTable_ZADDED_FOOD_ITEMS_WithData:[resultObj objectForKey:#"food"]];
yourLabel.text = #"Doing something else ...";
[dbManager insertIntoTable_ZUSDA_FOODS_WithData:[resultObj objectForKey:#"usda_food"]];
yourLabel.text = #"Doing something else again...";
[dbManager insertIntoTable_ZDAILY_MENU_RECIPE_WithData:[resultObj objectForKey:#"daily_menu_recipe"]];
No need of any delay or completion block here buddy :)
Finally if your concern is methods are executing much faster and label wont even appear for a fraction of a second and you intentionally want to add a delay between each method :)
-(void)insertIntoTable_ZADDED_FOOD_ITEMS_WithData:(NSData *)data andCompletionBlock:(void (^)())completionBlock{
//do whatever you want to do here
//once done simply say
double delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
completionBlock();
});
}
Hope my answer helped you :) Happy coding :)
This will be helpful for what you required.I did this successfully.
- (void)button_circleBusy:(id)sender {
firstButton.enabled = NO;
// 60 milliseconds is .06 seconds
[NSTimer scheduledTimerWithTimeInterval:.06 target:self selector:#selector(goToSecondButton:) userInfo:nil repeats:NO];
}
- (void)goToSecondButton:(id)sender {
firstButton.enabled = YES;
secondButton.enabled = NO;
[NSTimer scheduledTimerWithTimeInterval:.06 target:self selector:#selector(goToThirdButton:) userInfo:nil repeats:NO];
}
I hope it will work for you also!!
When you are not running you function in background then it should call one by one (after first finish). You don't need to bother about completion of first then start next.
And if you want to show message notification to hold for a sec on your screen then remove label with delay about a sec.
You can also delay your method call for a sec and so.
[self performSelector:#selector(removeMessageLabel) withObject:nil afterDelay:1.0];
Here "afterDelay" is responsible to make a pause for that particular method which is declared inside but compiler will move for further line execution.
I have created a drum sequencer using Objective C. I want the slider to control the tempo. At the moment, everything works, and the interval between each step is being controlled by:
while (self.running)
{
// sleep until the next step is due
[NSThread sleepUntilDate:time];
// update step
int step = self.step + 1;
// wrap around if we reached NUMSTEPS
if (step >= NUMSTEPS)
step = 0;
// store
self.step = step;
// time duration until next step
time = [time dateByAddingTimeInterval:0.2];
}
So the time between each step is 0.2 seconds. I have tried to implement a tempo slider like so in the view controller .m (the slider has a range of 0.3 to 1.0 so will output similar value to what is currently in time):
- (IBAction)sliderMoved:(UISlider *)sender
{
AppDelegate* app = [[UIApplication sharedApplication] delegate];
app.tempo = sender.value;
}
and by changing the line in the while(self.running) thread to:
time = [time dateByAddingTimeInterval: (NSTimeInterval) _tempo];
However, this causes the time between steps to be far too short (tempo is crazy fast) and when any control in the app is touched, it crashes.
I wonder if I need to set up a function like this, but I'm not sure what would go inside to enable the tempo slider to work:
- (void)setTempo:(float)tempo
{
}
I have tried to be as clear as I can, if anyone can help me I'd be very grateful, thanks in advance
-(void) startDrumTick{
[self.myDrumTimer invalidate]; // stop any current existing timer
// perform the call to the method 'drumMethodOperation:'
// every 0.2 sec. NB: drumMethodOperation will run on main thread.
// this means that if you expect to do long-blocking operation,
// you will need to move that op to an async thread, in order to avoid
// the UI blocking
self.myDrumTimer = [NSTimer scheduledTimerWithTimeInterval:0.2
target:self
selector:#selector(drumMethodOperation:)
userInfo:nil
repeats:YES];
}
-(void)drumMethodOperation:(id)sender
{
// update step
int step = self.step + 1;
// wrap around if we reached NUMSTEPS
if (step >= NUMSTEPS)
step = 0;
// store
self.step = step;
// any other needed operation to run every 0.2 secs
}
Below an example for an async thread management using GCD
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){
// Long blocking operation ( DO NOT PERFORM ANY UI OPERATION, like changing a text label, setting an image to an UIImageView, etc. )
[self myLongDbQuery];
dispatch_async(dispatch_get_main_queue(), ^(void){
//Perform you UI Updates here
self.myLabel.text = #"Query done!!!";
});
});
Hope it helps
Luca is right about using GCD. If talk about your initial solution.
Did you set initial value for _tempo? Looks like your bug can be caused by _tempo = 0 initially. As you understand sliderMoved will be called only after some user action so you need to set initial value.
I'm working on a project, where i have to update the text of a UILabel really regularly (0.085f). So, I insert the update of the label in a loop like this :
MetresNumber = MetresNumber + 0.25;
DisplayMetres.text = [NSString stringWithFormat:#"%07.0f", MetresNumber];
I precise that "MetresNumber" is a float, and "DisplayMetres" the UILabel.
And this sort of code really really makes bad performances ! It's incredible how slower it goes since i've added those lines.
I made some searches, and found elements like :
[DisplayMetres setNeedsDisplay];
But it didn't change and update the text on the label.
It's in a loop called with :
timer = [NSTimer scheduledTimerWithTimeInterval:0.085 target:self selector:#selector(myLoop) userInfo:nil repeats:YES];
So my question is, could my code be improve, to get better performances, or should i forget my UILabel because it's too slow with ?
Thanks !
(void)setNeedsLayout
Call this method on your application’s main thread when you want to adjust the layout of a view’s subviews. This method makes a note of the request and returns immediately. Because this method does not force an immediate update, but instead waits for the next update cycle, you can use it to invalidate the layout of multiple views before any of those views are updated. This behavior allows you to consolidate all of your layout updates to one update cycle, which is usually better for performance.
Another problem is that a scheduledTimer will not get called while the main thread is tracking touches. You need to schedule the timer in the main run loop.
So instead of doing
[NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:#selector(updateLabel:) userInfo:nil repeats:YES];
use
NSTimer* timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:#selector(updateLabel:) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
Also Use Timer selector method like below:
- (void) updateLabel:(id) sender {
MetresNumber = MetresNumber + 0.25;
NSString *text = [NSString stringWithFormat:#"%07.0f", MetresNumber];
dispatch_sync(dispatch_get_main_queue(), ^{
DisplayMetres.text = text;
});
}
There shouldn't be any performance issues when you are just updating a single label every 85ms. First find out what actually causes the lags. Use the Time Profiler instrument.
I think most time will be spent on the string drawing.
Here are some tips how you can optimize your code:
You don't need to call setNeedsLayout or setNeedsDisplay explicitely on the label. Just set the text property (on the main thread of course).
The resulting string in your code will always be a 7-digit integer. Consider using an integer instead of a float. Formatting an integer will be faster.
stringWithFormat can be slow sometimes. You could try using a NSNumberFormatter or simply generate the string with: [#(MetresNumber) stringValue]
In your code the string actually doesn't change everytime the timer fires. Only every 4th time. You can set the time interval to 4*0.085 and replace MetresNumber = MetresNumber + 0.25 with MetresNumber = MetresNumber + 1.
Try using this custom UILabel class
Don't use UILabel at all. Use pre-drawn images for each digit.
Schedule the timer with NSRunLoopCommonModes (see answer from Lightygalaxy)
I know that these NSTimer questions have come up numerous times, however since none seem to involve executing blocks that change the UI, I figured this is still an original question.
I have a subclass of UIButton that, for convenience sake (me, coming from an Android background), has an onClick and onHoldClick function. onClick simply takes a block and executes it in the selector that responds to UIControlEventTouchUpInside. The click function works great. For example:
[myButton setOnClick:^{
NSLog(#"clicked");
}];
The hold click functionality is not working so well.
[myButton setOnHoldClick:^{
NSLog(#"still holding click...");
}];
This listens for the UIControlEventTouchDown event, and performs a task after a delay:
- (void)clickDown:(id)sender
{
isClicked = YES;
[self performSelector:#selector(holdLoop:) withObject:nil afterDelay:delay];//For the sake of the example, delay is set to 0.5
}
The hold loop runs a repeated timer on another function, which handles the block execution (the timer variable is an NSTimer declared in the header file):
-(void)holdLoop:(id)sender
{
[self cancelTimers];
_timer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:#selector(death:) userInfo:nil repeats:YES];
}
-(void)death:(id)_delay
{
if (isClicked)
{
_holdBlock();
}
else
{
[self cancelTimers];
}
}
The block that executes changes the value of a float, which is used to update the value of a label, which is then redrawn.
The first time the hold click event occurs, this works great. After that, it seems like timers don't get canceled, and new timers are still added. This is what my cancelTimers function looks like (calls here are retrieved from a collection of the other questions on this topic):
-(void)cancelTimers
{
[_timer invalidate];
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(death:) object:nil];
}
What am I doing wrong, and how do I fix it?
Edit
I do, in fact, already have the function that responds to touch up inside:
- (void)clickUp:(id)sender
{
isClicked = NO;
[self cancelTimers];
_clickBlock();
}
Furthermore, I have realized that the issue comes from an unhandled cancel event. Is there a reason why iOS would auto-cancel my long press?
Solved
Since the block redrew the UI, it was also redrawing the buttons (and resetting their functionality). This event was causing a cancel event to be called on the button - which was not handled. Adding the following:
[self addTarget:self action:#selector(cancelClick:) forControlEvents:UIControlEventTouchCancel];
[self addTarget:self action:#selector(cancelClick:) forControlEvents:UIControlEventTouchUpOutside];
-(void)cancelClick:(id)sender
{
isClicked = NO;
[self cancelTimers];
}
As well as reconsidering what changes are made in the block, has gotten me past this issue.
As I understood from the comments and the code, the clickDown: is called for UIControlEventTouchDown so isClicked is set to YES when the first time the button is touched down. You need to add a selector to the event UIControlEventTouchUpInside. It's called when the user lifts his finger while being iside the bound of the button. Inside that method, set isClicked to NO.
I have a view-based template app and have UILabel & UIButton. For debugging purposes I'm showing and hiding the button whilst changing the UILabel.text.
In C++ I would 'thread root();' to execute the root method but I don't know how to in Objective-c. How to run my 'root' method once the view loads?
-(void) root
{
[bombButton1 setHidden:NO];
int s = 0;
int j = 10;
while ( s < j )
{
[bombButton1 setHidden:YES];
NSString *debugLabelString = [NSString stringWithFormat:#"%d", s];
debugLabel.text=debugLabelString;
s++;
}
Edit:
Right, now I have: (but I get ERROR: Expected method body on the "-(void) rootMethod: NSTimer * timer {" line)
-(void) applicationDidFinishLaunching : (UIApplication *) application {
spawnTimer = [NSTimer scheduledTimerWithTimeInterval: 1.0 target:self selector:#selector(rootMethod:) userInfo:nil repeats: YES];
}
-(void) rootMethod: NSTimer * spawnTimer {
int s = 0;
int j = 10;
while ( s < j )
{
NSString *debugLabelString = [NSString stringWithFormat:#"%d", s];
debugLabel.text=debugLabelString;
//debugLabel.text=#"debug test complete";
s++;
}
}
Several ways to do this, I think. Here's one:
[self performSelectorInBackground:#selector(root) withObject:nil];
You'd make this call in say, your -(void)viewDidAppear: method.
You may run into issues running code on threads other than the main thread that tries to manipulate the UI.
That sleep(1) is worrisome. You could use a repeating NSTimer instead and eliminate the sleep(1) entirely. Something like:
[NSTimer scheduledTimerWithInterval:2.0 target:self
selector:#selector(root:) userInfo:nil repeats:YES];
For NSTimer, you'd have to change your method, root, to have a signature like
- (void)root:(NSTimer*)theTimer
You need to implement a called viewDidLoad.
- (void) viewDidLoad() {
// your code here
}
I'm sure you have your reasons, but you really shouldn't iteract with UI components in anything other than the UI thread. What you actually need to do is use an NSTimer to call a method on the UI thread multiple times.
What you should be doing is performSelectorOnMainThread when you want to update the UI Thread.
Do your running in the background and update variables that will contain the updated values, then use performSelectorOnMainThread on the View, sending it to a method that will merely update the Textbox with the data in the variables.
You can do anything in a background thread, except update the ui.
Edit: Furthermore I dont recommend using Timers in place of background threads, I have had instances when using Timers, where only so many get created and when I expected a background thread to fire, it never did. The timer actually never fired, even tho it was created.