UIButton won't enable under some circumstances - ios

in my application i'm using a UIButton (myButton) wich i added via Storyboard.
So, when tapping the button an IBAction gets called:
-(IBAction)buttonAction:(UIButton*)sender{
[myCustomClass doSomeCrazyStuff];
[sender setEnabled:NO];
}
As you can see, i'm calling a method from myCustomClass ( myCustomClass is a REST-Client for my web-service).
The viewController the button lays in is delegate of myCustomClass.
There are two delegate methods implemented, one for success and one for error.
-(void)requestSucceeded{
/* If the request succeeded i want the button to be enabled again, and it's selected
state inverted */
NSLog(#"This gets called");
[myButton setEnabled:YES];
[myButton setSelected:!myButton.selected];
}
This works totally fine: i press the button, stuff is done on myCustomClass, request succeeds, button is set to inverted selected state.
But now for the other delegate method:
-(void)requestFailed{
/* If the request failed i want the button to be enabled again, and it's selected
state stays the same */
NSLog(#"That gets called");
[myButton setEnabled:YES];
}
If requestFailed gets called, the console prints That gets called as expected, but the button stays disabled... and i don't know why.
I tried other things in requestFailed like:
[myButton setHidden:YES];
Just to see if the reference to myButton is working...
And it is.
Probably i'm missing something right now, but i can't figure it out.
Thanks for your help.
EDIT:
I don't think requestFailed could be called from a different thread (as #gonji-dev mentioned), since both requestSucceeded and requestFailed are called from the same method.
In my doSomeCrazyStuff method i set up a completion block wich handles connection success and error. If an error occurred it gets handled in another class. If the connection succeeded i'm asking for HTTP status codes to decide wether requestFailed or requestSucceeded will be called.

Similar situation as in https://stackoverflow.com/a/31952060/218152.
Are you absolutely positive that you are invoking:
[myButton setEnabled:YES];
on the main thread?
The industry standard, when manipulating the UI in response to notifications or multithreaded environment is:
dispatch_async(dispatch_get_main_queue(), ^{
// update UI
});

Related

Calling a function through UIButton

I am not sure if my code is wrong, but there are no errors while compiling.
I have a refresh button where it refreshes a TableView and here's the following code:
- (IBAction)refreshButton:(UIButton *)sender {
UIButton *refreshButton = [UIButton alloc];
[refreshButton addTarget:self action:#selector(scanBLEDevices:) forControlEvents:UIControlEventTouchUpInside]; }
- (void)scanBLEDevices:(id)sender {
[manager scanForPeripheralsWithServices:#[[CBUUID UUIDWithString:BLEService]] options:nil];
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:#selector(stopScan:) userInfo:nil repeats:NO];}
Is there any mistake in this code? I am unsure about this second line, whether this is allowed:
UIButton *refreshButton = [UIButton alloc];
There is no "mistake" in your code, as, like you stated, it complies. The "mistake" here is more than syntax, it's context.
If you are expecting your button to fire and perform your scan,
then you should be calling your method scanBLEDevices: inside your
IBAction method refreshButton:.
In your snippet, by creating a new UIButton in your refresh button method, you are merely assigning an action to a button that has not been initialized yet, and providing no opportunity for the button to be fired (it is created and exists only within this method).
Assuming you have attached your action method correctly in your storyboard, I suggest replacing your simple IBAction method with the following:
- (IBAction)refreshButton:(UIButton *)sender {
[self scanBLEDevices:sender];
}
Because your scanBLEDevices: method requires an id sender, you can pass your button along (since after all you don't use it in the BLE method anway lol).
Hopefully this should help point you in the right direction. Happy coding!
NOTE: If you are unsure about when / where allocation is allowed here, I suggest reading up on some common practices concerning IBActions, targets, and senders when using UIButton elements in your code.
UIButton - UIKit | Apple Developer Documentation

xcode viewcontroller hiding fields not working

I have a view controller with labels, textfields, activity indicator and a button control all tied with IBOutlets and accessible from within my code.
When the user presses the button, I hide a few of the fields, and put up the activity indicator. I then make a synchronous URL call to get some JSON data.
The view controller is not updated to reflect the activity indicator and hidden fields until AFTER the synchronous request returns. I need this to happen before the request returns so the users sees that something is happening.
I have tried putting in a usleep(200000);, and also tried [self.view setNeedsDisplay]; - - both to no avail.
Is there any way I can force the screen update BEFORE the blocking synchronous call? I know I can go to an async call, but I really don't want to do that since I cannot do anything in the app until/unless I get the data i need...
Thanks,
Jerry
here is the code I use to send the sync request: This is the relevant portion of the routine 'SendGetEventsRequest'.
NSURLResponse *response = nil;
NSError *error = nil;
[self.aiActivityIndicator startAnimating];
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
Here are the other routines i use right before the above routine gets called:
- (IBAction)btnGetEvents:(id)sender {
// The user presssed the GetEvents button. Send the requested server to be processed.
[self.aiActivityIndicator startAnimating];
[self.lblCollectingEventNames setHidden:NO];
[self.lblEnterServerName setHidden:YES];
[self.btnGetEvents setHidden:YES];
[self.tfServerName setHidden:YES];
[self.view setNeedsDisplay];
[self SendGetEventsRequest:self.tfServerName.text];
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField{
[self.tfServerName resignFirstResponder];
[self btnGetEvents:self];
return YES;
}
Basically, when the user presses the 'go' button on the keyboard, the routine textFieldShouldReturn gets called. I dismiss the keyboard then simulate pushing the button 'Get Events'.
In btnGetEvents, I start up the activity indicator, hide/show a few fields, then call sendGetEventsRequest. In there is the code where i do the Sync call.
I am setting up the activity indicator and show/hide fields BEFORE the sync call, yet they are not updated until AFTER the sync call returns. I believe this is because the view controller did not perform a screen redraw before the Sync call got executed.
So, I need to figure out how to get the screen to update BEFORE the Sync call. I hope the additional code and additional explanation helps.
Wow, I cannot believe I am the only person to have this issue. I modified my code to use the async call and it is working perfectly.

How can to come back to my view when I confirm form?

I’m I downloaded MZFormSheetController library for my app.
I’ve got a problem on my popup. When I am on my TableViewController, I tap on a row to get popup to open up so that I can change the name. The popup opens, I set the name and when I tap on the button to correct the name, I call the button method but i can’t close my popup while reload my list.
- (IBAction)modifierTournoi:(id)sender {
//code to update database
//this method close the popup but don't call method viewWillAppear to reload database
//I don't know what method i can use..?
[self dismissFormSheetControllerAnimated:YES completionHandler:^(MZFormSheetController *formSheetController) {
}];
}
Before that, I used the method popViewControllerAnimated to come back to my list while recharging my list.
- (IBAction)modifierJoueur:(id)sender {
//code to update database
[self.navigationController popViewControllerAnimated:true];
}
Can you help me please ?
Thank you very much.
It looks like there is a specific completion handler for this purpose built into the library you are using:
- (IBAction)modifierTournoi:(id)sender {
//code to update database
//this method close the popup but don't call method viewWillAppear to reload database
//I don't know what method i can use..?
[self dismissFormSheetControllerAnimated:YES completionHandler:^(MZFormSheetController *formSheetController) {
// Reloading your database should work in here.
}];
}
The reason viewWillAppear will not be being called is because rather than placing a viewController modally above your window, I imagine MZFormSheetController will be adding a UIView above all presented UIViews, so viewWillAppear will never be called. But as I said above, you should be able to reload in the completion handler block.

Determine which UIButton fired event in Objective C

I have a handful of UIButtons that when pressed fire the method, (IBAction)buttonPressed:(id)sender. Right now I have a document label set for each (btnPlay, btnStop, btnPause), but I don't believe I can access this in Objective C. Is there something I can set in xcode that acts as a variable so when buttonPressed() is fired I know which button (btnPlay, btnStop, or btnPause) fired the event?
You should change your IBAction to something like the below
-(IBAction)buttonPressed:(UIButton *)button {
if([button.titleLabel.text isEqualToString:#"Start"]){
//Do Stuff
}
}
In this way you can access the sender as a button directly with no issues or type casting required, you can then use the isEqualToString method to check the title of the button and run code inside the if statement.
You might also like to consider using the tag property which pretty much all Interface Objects have.
if(button.tag == 1){
//Do Stuff
}
Switch statements are also a nice clean way of handling different events..
switch (button.tag) {
case 1:
// Do Something
break;
default:
// Do Default Action
break;
}
you can define which method has to be called when the button pressed after #selector in this case playVideo method.
[videoButton setTitle:#"play video" forState:UIControlStateNormal];
[videoButton setBackgroundImage:nil forState:UIControlStateNormal];
[videoButton addTarget:self action:#selector(playVideo:)forControlEvents:UIControlEventTouchUpInside];
That's what the sender argument is there for - you can compare it against each of your buttons in a chain of if statements to see which one sent that message.
Every UIButton has a titleLabel property, which is a UILabel. Check sender.titleLabel.text and compare it against the three strings.
Alternatively, you can also assign each button a tag (generally an integer), either through the Attributes Inspector in Xcode, or using the tag property in code. Then check sender.tag in your action method.

Unable to cancel NSTimer that executes blocks

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.

Resources