UIButton still calls IBAction after it's disabled - objective-c

I'm having a problem with the enabling/disabling of a UIButton, disabling the button works fine if I don't enable it again later in my code. When I do enable it later on it shows up as though it's disabled (the opacity changes) yet when I press it the attached IBAction function is still called.
The code:
- (void)loadDataFromURL:(NSURL *)URL withLoadIndicator:(UIActivityIndicatorView *)loadIndicator errorName:(NSString *)name sender:(id)sender andCallback:(SEL)selector{
// Start loading indicator, block button so we will have only one call at a time
[loadIndicator startAnimating];
if ([sender isKindOfClass:[UIButton class]]) {
UIButton *button = sender;
[button setEnabled:NO];
}
// Run the data load sequence
self.dispatchQueue = dispatch_queue_create("com.companyname.settingsqueue", 0);
dispatch_async(self.dispatchQueue, ^{
// Downloading JSON and using CoreData to put it into the sqlite database here
});
// After loading is complete stop animating and re-enable the button
dispatch_async(dispatchQueue, ^{
[loadIndicator stopAnimating];
if ([sender isKindOfClass:[UIButton class]]) {
UIButton *button = sender;
button.enabled = YES;
}
});
}
The strange thing is it works perfectly for the loadIndicator. The button I get from the sender parameter in the function does exist (it's not null). When I remove button.enabled = YES; it stays disabled as it should. Is there a way to enable it again after my async code has executed without the button still being enabled during the async execution?
Thanks in advance for helping me.

I found the answer to my problem after extensive searching. Because I wasn't running all my UIView functions on the main thread the UI didn't update even though my function finished. Therefor the button disabled for a really short time and then enabled again, even though my overlaying UIView and UIActivityIndicator were not yet removed from the screen.
I hope this helps someone else with similar issues.

Related

shouldPerformSegueWithIdentifier always executed first

I have a function called "openButtonPressed" which gets executed when a button on the ui gets pressed. Now I would like to show a loading view at first and then execute the segue. For some reason, the segue always gets called first.
Does somebody have a clue?
Thank you!
- (void)openButtonPressed:(id)sender
{
NSDebug(#"Open Button");
[self showLoadingView];
static NSString* segueToContinue = kSegueToContinue;
if ([self shouldPerformSegueWithIdentifier:segueToContinue sender:self]) {
[self performSegueWithIdentifier:segueToContinue sender:self];
}
}
- (void)showLoadingView
{
if(!self.loadingView) {
self.loadingView = [[LoadingView alloc] init];
[self.view addSubview:self.loadingView];
}
[self.loadingView show];
}
It looks like LoadingView is being initialized without a size.
It also may be executing the segue very quickly and you don't have any time to see the loading view.
You can set a breakpoint at the performSegue call and use the view debugger in Xcode to confirm if the view was loaded and what size the view was initialized to.
Maybe the view is not in the right place or in the right size. But even if you fix that you need to add a delay so the user can see the loading... I replicated your scenario here and after giving the view a initWithFrame it appears, but cannot be seen because is too fast...
If you want to show the loading for a period of time before the performSegue, you could use the performSelector:withObject:afterDelay.
If you need to wait an action from the loadingView, create a completionHandler in the loadingView initializer...
The hints below were very helpful to me and totally a better solution but I ended up using [CATransaction flush]; to force the refresh.
- (void)openButtonPressed:(id)sender
{
NSDebug(#"Open Button");
[self showLoadingView];
[CATransaction flush];
static NSString* segueToContinue = kSegueToContinue;
if ([self shouldPerformSegueWithIdentifier:segueToContinue sender:self]) {
[self performSegueWithIdentifier:segueToContinue sender:self];
}
}

iOS10 UITextView touch event crash

I have a very strange issue, UITextView touch event crash on double tap whereas same code works with < iOS10 version. (It means below iOS10 version there is no crash for press gesture recognizer)
Actually, I am adding the double tap and log press gesture based on permission. If the user has permission to comment then add gestures in viewDidLoad methods. Comment is allowed only with double tap or long press
singleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(singleTapGestureAction:)];
singleTapGesture.numberOfTapsRequired = 1;
// adding gesture to open window for commenting only when he has writing access
if (canComment) {
longPressgesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(longPressGestureAction:)];
longPressgesture.minimumPressDuration = 0.2;
doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(doDoubleTap:)];
doubleTap.numberOfTapsRequired = 2;
}
On single tap
-(void)singleTapGestureAction:(UITapGestureRecognizer*)tapGestureRecognizer{
if (isSingleTapped) {
isSingleTapped = NO;
return;
}
isSingleTapped = YES;
UITextView *textView = (UITextView *)tapGestureRecognizer.view;
[self.commentView becomeFirstResponder]; // becomeFirstResponder
}
On double tap
-(void)doDoubleTap:(UITapGestureRecognizer*)tapGestureRecognizer
{
UITextView *textView = (UITextView *)tapGestureRecognizer.view;
[self.commentView becomeFirstResponder]; // becomeFirstResponder
// To show the UIMenuController menu
[self setCommentMenuToolTipWithRect:completeRect];
}
NOTE: I am adding [self.commentView becomeFirstResponder]; on every gesture action
UITextView delegate methods
- (void)textViewDidBeginEditing:(UITextView *)inView
{
[self.commentView becomeFirstResponder];
range=[self.commentView selectedRange];
}
USE CASE:
When I double tap to select any word then APP CRASH and UIMenuController does not appear,
but if I add the following line app does not crash
- (void)textViewDidChangeSelection:(UITextView *)textView{
[textView resignFirstResponder];
} // app does not crash
and UIMenuController appears with comment menu items that's great. I was happy that I have fixed the crash issue.
But there is another problem, when I press outside, menu hides and
select any word AGAIN then It does not appear SECOND time.
I have tried all the possible way to show the menu for returns
YES/TRUE to canBecomeFirstResponder. I know, there has to be a view
that claims firstResponder for the menu to show. but how ?
On second time touch, not even calling any gesture recognizer method
From the logs it is clear that when double tap is recognized, same touch update is also sent to another gesture recognizer, which fails.
So, a simple solution would be to avoid detection of other gestures on double tap.
This can simply be achieved by making all other gestures on commentView require doubleTap to fail using requireGestureRecognizerToFail. just add the condition in addGestureToTextView method as shown below.
if (withDoubleTap && self.canScreenPlayEdit) {
[self.commentView removeGestureRecognizer:singleTapGesture];
[self.commentView addGestureRecognizer:doubleTap];
[self.commentView addGestureRecognizer:longPressgesture];
for (UIGestureRecognizer *recognizer in self.commentView.gestureRecognizers) {
[recognizer requireGestureRecognizerToFail:doubleTap];
}
}
This does solve the crash and also shows the menu without calling resignFirstResponder in textViewDidChangeSelection.
However, there seem to be many issues in your code. PLSceneDetailsVC is too complicated and you need to simplify the code. You need to streamline the gesture management or you will end up facing many more such issues.
longPressgesture.minimumPressDuration = 0.2;
My guess the problem is here. 0.2s is way too small to be used for longPress. Probably both were triggered (longPress and double tap).
Change it to higher like 1.5s.

UISwitch does sometimes not send UIControlEvents

I have a model object which includes a boolean flag. I want to show the value of the flag using a UISwitch. The value can be changed in two ways:
First by the user by toggling the switch. I register the UIControlEventTouchUpInside for that. I also tried UIControlEventValueChanged – it has the exact same effect.
Second by some external state change. I use a timer to check for that state change and set the on property of the switch accordingly.
However, setting the value of the switch in the timer method has the effect that the touchUpInside action is sometimes not triggered even though the user has touched the switch.
So I face the following problem: If I set the switch state in the timer when the state changes externally, I loose some state changes from the user. If I don't use the timer I get all state changes from the user. However, I miss all the external state changes, then.
Now I have run out of ideas. How can I achieve what I want, getting both types of state changes in the model and reflected them correctly in the switch view?
Here is a minimal example that shows the problem. I have replaced the model object by a simple boolean flag, and in the timer I don't change the flag at all, I just call setOn:animated:. I count the invocations of the action method. Like that I can easily find out how many touches were missed:
#import "BPAppDelegate.h"
#import "BPViewController.h"
#implementation BPAppDelegate {
NSTimer *repeatingTimer;
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions: (NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
BPViewController *viewController = [[BPViewController alloc] init];
self.window.rootViewController = viewController;
[self.window makeKeyAndVisible];
[self startTimer];
return YES;
}
- (void) startTimer {
repeatingTimer = [NSTimer scheduledTimerWithTimeInterval: 0.2
target: self.window.rootViewController
selector: #selector(timerFired:)
userInfo: nil
repeats: YES];
}
#end
#import "BPViewController.h"
#implementation BPViewController {
UISwitch *uiSwitch;
BOOL value;
int count;
}
- (void)viewDidLoad
{
[super viewDidLoad];
value = true;
uiSwitch = [[UISwitch alloc] init];
uiSwitch.on = value;
[uiSwitch addTarget:self action:#selector(touchUpInside:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:uiSwitch];
}
- (void)touchUpInside: (UISwitch *)sender {
count++;
value = !value;
NSLog(#"touchUpInside: value: %d, switch: %d, count: %d", value, sender.isOn, count);
}
- (void) timerFired: (NSTimer*) theTimer {
NSLog(#"timerFired: value: %d, switch: %d, count: %d", value, uiSwitch.isOn, count);
// set the value according to some external state. For the example just leave it.
[uiSwitch setOn:value animated:false];
}
#end
Use UIControlEventValueChanged to determine when the switch has been toggled (programmatically or by the user). Don't use touchUpInside. Also touchUpInside doesn't work well for when the user drags the UISwitch.
Also, don't replicate a property (e.g. your value property) that is already maintained by the UISwitch (i.e. the "on" property)...its redundant and will just get you into trouble
EDIT: The following code does exactly what you want, if not exactly the way you want it. If insures that both timer changes and touch changes to the control receive an action method. The method is only going to be sent once per change, which is hopefully what you want.
Note the switch to UIControlEventValueChanged.
[sw addTarget:self action:#selector(touched:) forControlEvents:UIControlEventValueChanged];
- (void) timerFired: (NSTimer*) theTimer
{
// set the value according to some external state. For the example just leave it.
BOOL oldOn = sw.on;
BOOL newOn = YES;
[sw setOn:newOn];
if(newOn != oldOn) {
for(id target in [sw allTargets]) {
for(NSString *action in [sw actionsForTarget:target forControlEvent:UIControlEventValueChanged]) {
[target performSelector:NSSelectorFromString(action) withObject:self];
}
}
}
}
EDIT2:
I [original poster] just saw the new code in your answer and tried it out. I am not sure it is doing exactly what I want. However, I am not sure if I understand it correctly. I don't want to do exactly the same thing in both cases.
Q: When the user touches the switch I want to toggle the boolean value in the model. I not necessarily need to change the switch programmatically because it is done automatically by touching it anyway. I want to count the touches to make sure none are lost.
A: Well, that's not really possible. However, you can certainly add a counter to the action method and see if the taps you make equal the counter. When the user taps the switch, then "sender == theSwitch". If you send the action method otherwise, you can use a different sender (to differentiate them).
If I get the timer event in my minimal example I just want to leave the boolean value like it currently is. However, I need to set the switch state programmatically so that the view reflects the correct model state. – Bernhard Pieber 3 mins ago
Thats fine - but you keep saying you want the action method called. This code does that. If you misstated that, and you don't want the action method called, then delete the "if" block.
- (void)insureValueIs:(BOOL)val
{
BOOL oldVal = sw.on;
if(val != oldVal) {
for(id target in [sw allTargets]) {
for(NSString *action in [sw actionsForTarget:target forControlEvent:UIControlEventValueChanged]) {
[target performSelector:NSSelectorFromString(action) withObject:self];
}
}
}
}
Of course, I do want to achieve the same thing: That the model state is correctly reflected in the view. Does that make sense?
Frankly, you have not done a good job in describing EXACTLY what you want, and I keep responding to your comments, but even now I am still not totally clear. I believe you have all the information you need here to do whatever it is you want.
Your timer is firing so often it probably prevents the switch ever finishing the animated transition to the alternative value, so it never sends the valueChanged event message. You are setting the value to YES every 0.2 seconds which is faster, I think, than the duration of the animation that occurs when the user taps the switch.
My original code works as expected in iOS 7. So it seems to have been a bug in iOS 6.

Grand Central Dispatch. When using GCD [spinner startAnimating] similar to [myView setNeedsDisplay]?

In Grand Central Dispatch I want to start a spinner - UIActivityIndicatorView - spinning prior to beginning long running task:
dispatch_async(cloudQueue, ^{
dispatch_async(dispatch_get_main_queue(),
^{
[self spinnerSpin:YES];
});
[self performLongRunningTask];
dispatch_async(dispatch_get_main_queue(),
^{
[self spinnerSpin:NO];
});
});
Here is the spinnerSpin method:
- (void)spinnerSpin:(BOOL)spin {
ALog(#"spinner %#", (YES == spin) ? #"spin" : #"stop");
if (spin == [self.spinner isAnimating]) return;
if (YES == spin) {
self.hidden = NO;
[self.spinner startAnimating];
} else {
[self.spinner stopAnimating];
self.hidden = YES;
}
}
One thing I have never seen discussed is the difference - if any - between [myView setNeedsDisplay] and [myActivityIndicatorView startAnimating]. Do they behave the same?
Thanks,
Doug
The [UIView setNeedsDisplay] method has nothing to do with a UIActivityIndicatorView's animation state.
setNeedsDisplay simply informs the system that this view's state has changed in a way that invalidates its currently drawn representation. In other words, it asks the system to invoke that view's drawRect method on the next drawing cycle.
You very rarely need to invoke setNeedsDisplay from outside of a view, from code that is consuming the view. This method is meant to be invoked by the view's internal logic code, whenever something changes in its internal state that requires a redraw of the view.
The [UIActivityIndicatorView startAnimating] method is specific to the UIActivityIndicatorView class and simply asks the indicator to start animating (e.g. spinning). This method is instant, without requiring you to call any other method.
On a side note, you could simplify your code by simply calling startAnimating or stopAnimating without manually showing/hiding it. The UIActivityIndicatorView class has a hidesWhenStopped boolean property that defaults to YES, which means that the spinner will show itself as soon as it starts animating, and hide itself when it stops animating.
So your spinnerSpin: method could be refactored like this (as long as you haven't set the hidesWhenStopped property to NO):
- (void)spinnerSpin:(BOOL)spin {
if (YES == spin) {
[self.spinner startAnimating];
} else {
[self.spinner stopAnimating];
}
}

Don't allow user interaction when activity indicator view is visible

I have a view which contains two views. One of those views contains two buttons and some text labels. The other one, with alpha set to 0.25, has an UIActivityIndicatorView to tell the user that the app is working and he must wait until it finishes. If the user touch a button while the UIActivityIndicatorView is spinning, when the UIActivityIndicatorView stops, the app remember the user action and responds to it. How can I discard the user interaction that occur while the UIActivityIndicatorView is spinning?
Thanks for reading.
P.D.: Like is commented in this thread, I prefer do not to use any modal solution.
EDITED:
I am currently using this code and it does not work right.
- (void)viewDidAppear:(BOOL)animated {
// The view appears with an UIActivityIndicatorView spinning.
[self showResults]; // The method that takes a long time to finish.
[self.activityIndicator stopAnimating];
// When the showResults method ends, the view shows the buttons to the user.
[self.activityIndicatorView setHidden:YES];
[self.menuButton setEnabled:YES];
[self.menuButton setUserInteractionEnabled:YES];
[self.playButton setEnabled:YES];
[self.playButton setUserInteractionEnabled:YES];
[self.view setUserInteractionEnabled:YES];
[self.interactionView setUserInteractionEnabled:YES];
}
I found these methods very useful:
[[UIApplication sharedApplication] beginIgnoringInteractionEvents];
[[UIApplication sharedApplication] endIgnoringInteractionEvents];
In Swift 3.0
To Disable interaction :-
UIApplication.shared.beginIgnoringInteractionEvents()
To restore interaction :-
UIApplication.shared.endIgnoringInteractionEvents()
[_button setUserInteractionEnabled:NO];
That should disable it, just set YES for when you want to user to tap it.
BOOL i_am_ready_to_submit = NO;
-(void)action_finished{
[self.activityIndicator stopAnimating];
i_am_ready_to_submit = YES;
}
-(IBAction)submit_button{
if(i_am_ready_to_submit){
[self submit];
}
}
just add
[self.view setUserInteractionEnabled:NO];
before the
[self.activityIndicator startAnimating];
and reenable it after
[self.activityIndicator stopAnimating];
[self.view setUserInteractionEnabled:YES];
To disable touch event in a view,
[[UIApplication sharedApplication] beginIgnoringInteractionEvents];
To enable touch event in a view
[[UIApplication sharedApplication] endIgnoringInteractionEvents];
For swift 5 you can use:
self.view.isUserInteractionEnabled = false
and to enable all again:
self.view.isUserInteractionEnabled = true
If you have some lag problem, like freeze for about a couple of seconds just put this in:
DispatchQueue.main.async{}
You could disable/enable the UIButtons based on the UIActivityIndicatorView being shown or not. Or, if you just want to "discard the user interaction" while the spinner is shown, in the button handler method:
- (void)buttonTapped:(id)sender {
if ([spinner superview] != nil && [spinner isAnimating]) {
return;
}
// ... the rest of your code
}
This example assumes that when you hide the UIActivityIndicatorView you call one of:
[spinner removeFromSuperview];
or
[spinner stopAnimating];
Use SVProgressHUD WrapperClass It have so many options to show ActivityIndicator
For Source Code Click Here !
[SVProgressHUD showWithMaskType:SVProgressHUDMaskTypeBlack];
use above statement to disable background touches
[SVProgressHUD dismiss]
To enable background touches.
#IBAction func yourButtonPressed(sender: UIButton) {
if self.activityIndicator.isAnimating() {
//remember the action user asked of you using the sender
} else {
//do your stuff
return
}
yourButtonPressed(yourButton)
}
or you code use
self.activityIndicator.animationDidStop to determine when to run your stuff
A quick solution: add a transparent or pseudo transparent view that cover the whole screen. Add your activity indicator on top of this view. When the wait period finishes, remove both views. Get some inspiration.
A better solution, because you can't hide the whole screen in all situations, is to manage the state of the app (ignore actions when the app is 'busy') and disable/enable the appropriate buttons and other controls depending on each app state.
Though answer is replied in earlier response, just like to add for information purpose "[self.activityIndicatorView setHidden:YES];" no need to call this method explicitly, because startAnimating/stopAnimating already take care of this. I'm assuming you are using default value of "hidesWhenStopped" property.