How to prevent multiple event on same UIButton in iOS? - ios

I want to prevent continuous multiple clicks on the same UIButton.
I tried with enabled and exclusiveTouch properties but it didn't work. Such as:
-(IBAction) buttonClick:(id)sender{
button.enabled = false;
[UIView animateWithDuration:1.0 delay:0.0 options:UIViewAnimationOptionAllowAnimatedContent animations:^{
// code to execute
}
completion:^(BOOL finished){
// code to execute
}];
button.enabled = true;
}

What you're doing is, you simply setting enabled on/off outside of the block. This is wrong, its executing once this method will call, thus its not disabling the button until completion block would call. Instead you should reenable it once your animation would get complete.
-(IBAction) buttonClick:(id)sender{
button.enabled = false;
[UIView animateWithDuration:1.0 delay:0.0 options:UIViewAnimationOptionAllowAnimatedContent animations:^{
// code to execute
}
completion:^(BOOL finished){
// code to execute
button.enabled = true; //This is correct.
}];
//button.enabled = true; //This is wrong.
}
Oh and yes, instead of true and false, YES and NO would looks nice. :)

Instead of using UIView animation I decided to use the Timer class to enable the button after a time interval. Here is the answer using Swift 4:
#IBAction func didTouchButton(_ sender: UIButton) {
sender.isUserInteractionEnabled = false
//Execute your code here
Timer.scheduledTimer(withTimeInterval: 2, repeats: false, block: { [weak sender] timer in
sender?.isUserInteractionEnabled = true
})
}

This is my solution:
NSInteger _currentClickNum; //Save the current value of the tag button is clicked
//Button click event
- (void)tabBt1nClicked:(UIButton *)sender
{
NSInteger index = sender.tag;
if (index == _currentClickNum) {
NSLog(#"Click on the selected current topic, not execution method, avoiding duplicate clicks");
}else {
[[self class] cancelPreviousPerformRequestsWithTarget:self selector:#selector(tabBtnClicked:) object:sender];
sender.enabled = NO;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
sender.enabled = YES;
});
_currentClickNum = index;
NSLog(#"Column is the current click:%ld",_currentClickNum);
}
}

In my case setting isEnabled was not fast enough to prevent multiple taps. I had to use a property and a guard to prevent multiple taps. And the action method is calling a delegate which normally dismisses the view controller but with multiple button taps it was not dismissing. dismiss(...) must cancel itself if code is still executing on the view controller, not sure. Regardless, I had to add a manual dismiss in the guard.
Here's my solution...
private var didAlreadyTapDone = false
private var didNotAlreadyTapDone: Bool {return !didAlreadyTapDone}
func done() {
guard didNotAlreadyTapDone else {
self.dismiss(animated: true, completion: nil)
return
}
didAlreadyTapDone = true
self.delegate.didChooseName(name)
}

Related

Why are completion handlers of [UIView animate] called in a wrong order?

In my project there's a ViewController which contains a few subviews(e.g. buttons).
It shows/hides those buttons, always with animation.
It has an interface like this:
#interface MyViewController: UIViewController
- (void)setMyButtonsVisible:(BOOL)visible;
#end
And an implementation looks like this:
- (void)setMyButtonsVisible:(BOOL)visible
{
if( visible )
{
// "show"
_btn1.hidden = NO;
_btn2.hidden = NO;
[UIView animateWithDuration:0.2 animations:^{
_btn1.alpha = 1.0;
_btn2.alpha = 1.0;
}];
}
else
{
// "hide"
[UIView animateWithDuration:0.2 animations:^{
_btn1.alpha = 0.0;
_btn2.alpha = 0.0;
} completion:^(BOOL finished) {
_btn1.hidden = YES;
_btn2.hidden = YES;
}];
}
}
When [_myVC setMyButtonsVisible:NO] is called, and then after some time ( > 0.2s) [_myVC setMyButtonsVisible:YES] is called, everything is OK.
However, it setMyButtonsVisible:YES is called immediately after ( < 0.2s) setMyButtonsVisible:NO, the animations overlap and setMyButtonsVisible:NO callback is called the last.
I've tried to change "hide" duration to 0.1, it doesn't help - after "hide(0.1)"+"show(0.2)" calls, "hide" callback is called after the "show" callback and my buttons are not visible.
I've added a quick-fix by caching the visible param and checking in "hide" completion handler if the state should be !visible.
My questions are:
Why the first animation completion handler is called the last if animations overlap?
What are better approahes to discard a previous "overlapping" animation?
Check the finished flag on completion:
if (finished) {
_btn1.hidden = YES;
_btn2.hidden = YES;
}

Prevent multiple Tap on the same UIButton

Some times in my app I get this error because the UI freezes and the users tap more than once the buttons:
"pushing the same view controller instance more than once is not
supported"
I have tried this:
How to prevent multiple event on same UIButton in iOS?
And it works like a charm but if my tabbar has more than 5 elements if I tab the button that shows an element greater than 5 the more button animates from left to right.
Is there other way to prevent the double tab in an easy way that does not use animations?.
This is the code I'm using:
- (IBAction)btnAction:(id)sender {
UIButton *bCustom = (UIButton *)sender;
bCustom.userInteractionEnabled = NO;
[UIView animateWithDuration:1.0 delay:0.0 options:UIViewAnimationOptionAllowAnimatedContent animations:^{
[self selectTabControllerIndex:bCustom.tag];
} completion:^(BOOL finished){
bCustom.userInteractionEnabled = YES;
}];
}
First a tip, if you only have button's calling that selector, you can change the id to UIButton* and drop the extra variable bCustom.
Now, to solve your issue, you just need to ensure you turn userInteractionEnabled back to YES after you'd done whatever else you needed to do. Using the animation block is just an easy way because it has a completion handler built in.
You can do this simply by having selectTabControllerIndex method do the work for you.
Something like this:
- (IBAction)btnAction:(UIButton*)sender {
sender.userInteractionEnabled = NO;
[self selectTabControllerForButton:sender];
}
- (void)selectTabControllerForButton:(UIButton*)sender {
// Whatever selectTabControllerIndex does now goes here, use sender.tag as you used index
sender.userInteractionEnabled = YES;
}
If you possibly had other code you needed to execute afterwards, you could add a completion handler to your selectTabControllerIndex method instead and then call the completion handler. Inside that you'd include the sender.userInteractionEnabled = YES; line. But if it's always the same code, the first way is easier and faster.
Using userInteractionEnable=false to prevent double tap is like using a Rocket Launcher to kill a bee.
Instead, you can use myButton.enabled=false.Using this, you may be able to change ( if you want ) the layout of your button when it is deactivated.
In Swift, you can also use defer keyword, to execute a block of code that will be executed only when execution leaves the current scope.
#IBAction func btnAction(_ sender: UIButton) {
sender.isUserInteractionEnabled = false
defer {
sender.isUserInteractionEnabled = true
}
// rest of your code goes here
}
Note: This will only be helpful if the "rest of your code" is not async, so that the execution actually leaves the current scope.
In async cases you'd need to set isUserInteractionEnabled = true at the end of that async method.
Disable isUserInteractionEnabled or disable the button not work some cases, if have background API calling in next controller, push process will work asynchronously.
After some work around i thought its better to go with the other way, i found Completion handler in Objective-C or Closure in Swift can be good here.
Here is the example which i used in Objective c:
-(void)didSettingClick:(id) sender
{
if (!isPushInProcess) {
isPushInProcess = YES;
SettingVC *settings = [[SettingVC alloc] initWithcomplition:^{
isPushInProcess = NO;
}];
[self.navigationController pushViewController:settings animated:YES];
}
}
Here is method description:
dispatch_block_t pushComplition;
-(instancetype) initWithcomplition:(dispatch_block_t)complition{
self = [super init];
if (self) {
pushComplition = complition;
}
return self;
}
Inside viewDidAppear()
-(void)viewDidAppear:(BOOL)animated
{
pushComplition();
}
In swift using defer keyword is also can be good idea.
Hope It help!!!
You can disable the userInteraction for that button when user taps for first time.
Then new view controller will appear, while leaving to new View Controller call this
-(IBAction)btnAction:(UIButton *)sender {
sender.userInteractionEnabled=NO;
//do your code
}
if it is moving to another view then call below one
-(void)viewWillDisappear {
buttonName.userInteractionEnabled=YES;
}
if not moving from present view
you can call
sender.userInteractionEnabled=YES;
at the end of btnAction method.
It will work for sure.
myButton.multipleTouchEnabled = NO;
Swift 4 version of #Santo answer that worked for me:
Button code:
#IBAction func btnMapTap(_ sender: UIButton) {
sender.isUserInteractionEnabled = false
//put here your code
Add override method viewWillDisappear:
override func viewWillDisappear(_ animated: Bool) {
btnMap.isUserInteractionEnabled = true
}
Use this code: This is bool condition
button.ismultipleTouchEnabled = false
it seems that under iOS 14.x it will happen automatically when You tap.
I have written small demo app with a nav controller, a controller of class "ViewController" with a button invoking an action "pushIt".
(see code)
I have set Storyboard ID to a separated controller to "ColoredVCID" and added a global counter, just to see...
Long way SHORT: it seems working correctly.
// compulsiveTouch
//
// Created by ing.conti on 03/08/21.
import UIKit
fileprivate var cont = 0
class ViewController: UIViewController {
#IBAction func pushIt(_ sender: Any) {
cont+=1
print(cont)
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "ColoredVCID")
self.navigationController!.present(vc, animated: true)
// OR:
self.navigationController!.pushViewController(vc, animated: true)
}
}
In PAST days I usually did:
#objc func pushItOLD(_sender: Any){
// prevent compulsive touch:
self.setButtonActive(btn: self.pushBtn!, active: false)
// now re-eanble it... after 1 second:
let when = DispatchTime.now() + 1
DispatchQueue.main.asyncAfter(deadline: when, execute: { () -> Void in
self.setButtonActive(btn: self.pushBtn!, active: true)
})
}
func setButtonActive(btn: UIButton?, active: Bool){
guard let btn = btn else{
return
}
btn.isEnabled = active
btn.alpha = (active ? 1 : 0.5)
}
that CAN BE very useful nowadays if your button for example invokes a network request... to prevent double calls.
(I added some cosmetics to use alpha.. to let user see it as "disabled" ..)
I did it like this
var callInProgress = false
func call(){
if callInProgress == true{
return
}
callInProgress = true
//Make it false when your task done
}
it will not allow user to call the function one more time untill you make callInProgress false
This is the only thing working

How to know if view is being animated?

Is there a way to know if my custom implementation of setFrame: (or an other setter of an animatable property) is being called from an animation block i.e. it will be animated or just set directly?
Example:
- (void)setFrame:(CGRect)newFrame {
[super setFrame:newFrame];
BOOL willBeAnimated = ?????
if (willBeAnimated) {
// do something
} else {
// do something else
}
}
In the above setter willBeAnimated should be YES it is called like this:
- (void)someMethod {
[UIView animateWithDuration:0.2
animations:^{view.frame = someRect;}
completion:nil];
}
and NO in this case:
- (void)someMethod {
view.frame = someRect;
}
someMethod here is a private method inside UIKit that I can't access or change, so I have to somehow determine this from the "outside".
You should be able to check the animationKeys of the layer of your UIView subclass right after changing the frame to see if it is being animated.
- (void)setFrame:(CGRect)newFrame {
[super setFrame:newFrame];
BOOL willBeAnimated = [super.layer animationForKey:#"position"] ? YES : NO;
if (willBeAnimated) {
// do something
} else {
// do something else
}
}
You can also to check if there are any animations by using animationsKeys which in this case would just return position.
In addition, if you want to force a change to not be animated you can use performWithoutAnimation:
[UIView performWithoutAnimation:^{
[super setFrame:newFrame];
}];
EDIT
Another tidbit I found by testing is that you can actually stop the animation if it is already in progress and instead making the change instantly by removing the animation from the layer and then using the above method instead.
- (void)setFrame:(CGRect)newFrame {
[super setFrame:newFrame];
BOOL willBeAnimated = [super.layer animationForKey:#"position"] ? YES : NO;
BOOL shouldBeAnimated = // decide if you want to cancel the animation
if (willBeAnimated && !shouldBeAnimated) {
[super removeAnimationForKey:#"position"];
[UIView performWithoutAnimation:^{
[super setFrame:newFrame];
}];
} else {
// do something else
}
}

Prolong UIPickerView [selectRow: inComponent: animated:]'s animation

I'm using a UIPickerView to display random numbers. The user can press a button, and thus trigger a random selection of a number inside the UIPickerView.
It does not matter how many objects or numbers are being displayed inside the UIPickerView, when I call the method:
[self.picker selectRow:randomRow inComponent:0 animated:YES];
It always gets animated with the same time interval.
Is there any option or method to prolong the animation time interval of above method?
I have tried placing it in an animation block:
[UIView beginAnimations:#"identifier" context:nil];
// code
[UIView commitAnimations];
but this seems to be a dead end.
I have also tried executing it in a completion blocks:
// 0.34f is the approximate defualt apple animation
[UIView animateWithDuration:0.34f animations:^{
[self.picker selectRow:randomRow inComponent:0 animated:YES];
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.34f animations:^{
[self.picker selectRow:randomRow inComponent:0 animated:YES];
} completion:nil];
}];
Any help would be appreciated.
I made a project and play for a while till I find out this tricky solution. It base on the method performSelector:afterDelay
Here is the touch up inside code of your button:
- (void)click:(id)sender
{
int randomRow = <>;//Your random method
int currentRow = [_picker selectedRowInComponent:0];
int i = 0;
while(1)
{
i++;
NSString *rowToSelectString = [NSString stringWithFormat:#"%d", currentRow];
NSDictionary *rowToSelectDictionary = #{#"row":rowToSelectString};
if(randomRow < currentRow)
{
// Go backward
currentRow--;
}
else
{
// Go forward
currentRow++;
}
[self performSelector:#selector(selectRowInPicker:) withObject:rowToSelectDictionary afterDelay:i*0.1];//Change the delay as you want
if(currentRow == randomRow)
{
break;
}
}
}
And the trick:
-(void)selectRowInPicker:(NSDictionary *)rowToSelectDictionary
{
NSInteger row = [[rowToSelectDictionary objectForKey:#"row"] integerValue];
[_picker selectRow:row inComponent:0 animated:YES];
}
This work well for me. Tell me if you're having problem with it.
Finally, I have implemented a "thinner" version of the chosen answer:
- (IBAction)spinTheWheelButtonPressed:(UIButton *)sender
{
for (int i = 0; i < 30; i++) {
NSUInteger randomRow = arc4random_uniform((int)[self.dataSource count]);
[self performSelector:#selector(selectRowInPicker:) withObject:#(randomRow) afterDelay:i*0.1];
}
}
- (void)selectRowInPicker:(NSNumber *)randomRow
{
[self.picker selectRow:[randomRow integerValue] inComponent:0 animated:YES];
}
I had a similar problem , but unfortunately there is no easy way to delay the animation thats as far as I know.
So I went around and placed a UIView on top of the pickerView. I subclassed that view to pass all of its touches to the pickerView and when something was selected I just drew a layer with less opacity in the view where the row would usually come to rest and animated it in a animation block since the selection is always right at the middle of the pickerView.

How can I tap into a UIView's changing values during an animateWithDuration block?

I am using animateWithDuration to change the value of a UISlider:
[UIView animateWithDuration:1.0f
delay:0.0f
options:UIViewAnimationOptionCurveEaseInOut
animations:^{ [myUISlider setValue:10]; }
completion:^(BOOL finished){ }];
The problem is, I need to be able to display the current value of the UISlider while it is changing. Is this even possible using animateWithDuration? I tried creating a UISlider subclass and overriding setValue in hopes of getting access to the slider's value as it is being changed:
-(void)setValue:(float)newValue
{
[super setValue:newValue];
NSLog(#"The new value is: %f",newValue);
}
But that code only gets called at the very end of the animation block. In a way that makes perfect sense, since I really only called setValue once. But I was hoping that the animation block would somehow call it over and over again within its internal mechanisms, but that appears not to be the case.
If UIView animateWithDuration is a dead-end here, I wonder if there is a better way to acheive this same functionality with something else? Perhaps another slick little block-driven part of the SDK I don't know about which allows animating more than just UIView parameters?
I think the best way is to handle it is to code a custom Slide you can creat it like progress bar which there are many demo on github.
You can also use NSTimer to do it , but I do not think it is a very better way.
When you tap , you can creat a timer :
_timer = [NSTimer scheduledTimerWithTimeInterval:.1 target:self selector:#selector(setValueAnimation) userInfo:nil repeats:YES];
and set a ivar: _value = oldValue;
in the setValueAnimation method :
- (void)setValueAnimation
{
if (_value >= newValue) {
[_timer invalidate];
_value = newValue;
[self setVale:_value];
return;
}
_value += .05;
[self setVale:_value];
}
UPDATE
how to add block:
1st:you can define a block handler :
typedef void (^CompleteHandler)();
2nd: creat your block and added it into the userInfo :
CompleteHandler block = ^(){
NSLog(#"Complete");
};
NSDictionary *userInfo = [[NSDictionary alloc] initWithObjectsAndKeys:block,#"block", nil];
3rd: make the NSTimer:
_timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(setValueAnimation:) userInfo:userInfo repeats:YES];
4th:achieve your timer method:
- (void)setValueAnimation:(NSTimer *)timer
{
if (_value >= newValue) {
[_timer invalidate];
_value = newValue;
[self setVale:_value];
// also can use [_delegate complete];
CompleteHandler block = [timer.userInfo objectForKey:#"block"];
if (block) {
block();
}
return;
}
_value += .05;
[self setVale:_value];
}
I do not know if there is any sort of notification that you could "tap into" to get the UIView animation "steps", but you can do the animations "manually" using an NSTimer, and that will allow you to continuously update your value.
If there's an out of the box solution using UIView - I'm all ears. This animation framework (only 2 classes!) - PRTween https://github.com/chris838/PRTween will give you access to convenience timing functions and also the changed value.
Here's an updated project of mine https://github.com/jdp-global/KACircleProgressView/
PRTweenPeriod *period = [PRTweenPeriod periodWithStartValue:0 endValue:10.0 duration:1.0];
PRTweenOperation *operation = [PRTweenOperation new];
operation.period = period;
operation.target = self;
operation.timingFunction = &PRTweenTimingFunctionLinear;
operation.updateSelector = #selector(update:)
[[PRTween sharedInstance] addTweenOperation:operation]
- (void)update:(PRTweenPeriod*)period {
[myUISlider setValue:period.tweenedValue];
}

Resources