Monday 13 February 2012

Enable or Disable NavBar Done Button

You've got your table based form looking just great now. However, the user experience isn’t ideal. For one thing, the Done button is active as soon as the add scene appears; it would be better if it became active after the user taps a key on the keyboard.

In my last post I told you about the <UITextFieldDelegate> and the textFieldDidEndEditing delegate function. We're going to look at the textField delegate again, but before we go on, let's tidy that up what we did last time a little bit. [Remember all this is based on Your second iOS App tutorial.]

Next... Next... Next....
Let's make the return button 'next' for both fields, see the last post if you can't remember how, so we pass the user backwards and forwards between the two fields. (If you have more than two... you can work it out!) Change the delegate method to this:


- (BOOL)textFieldShouldReturn:(UITextField *)textField 
{
    if (textField == self.birdNameInput) {

        [self.locationInput becomeFirstResponder];
        
        // scroll to row!
        [self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:YES];
   
    } else if (textField == self.locationInput) {
        
        [self.birdNameInput becomeFirstResponder];
        
        // scroll to row!
        [self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:YES];
        
    }
    
    return YES;
}


The 'else if' statement part is just the reverse of the first part, taking us back to the first row etc. Now, you might find a more efficient way to call all this if you have 3 or more fields. If you do, let us all know!

Disable Done button when view appears
There are a few ways to do this, but in this post I'll just give you the simplest. I'm assuming you have put a button item in the right side of the navigation item bar.

Change viewDidLoad in the view controller .m file:


- (void)viewDidLoad
{
    [super viewDidLoad];
    self.navigationItem.rightBarButtonItem.enabled = NO; // Easy way!!!
}


It works by saying that item is not enabled. This method will always act on the farthest top right nav bar button item. [Again, it was here in the UIBarItem class reference all along under enabled, although I'd never have got it simply from looking at that...]

Done button Enabled - Method 1:
- After the text field resigns its first responder status...


- (void)textFieldDidEndEditing:(UITextField *)textField 
{  
    // after the text field resigns its first responder status, enable done button... 
    self.navigationItem.rightBarButtonItem.enabled = YES;
}



- After the text field resigns its first responder status and it has text entered...


- (void)textFieldDidEndEditing:(UITextField *)textField 
{  
    if ([textField.text length] != 0) { 

        self.navigationItem.rightBarButtonItem.enabled = YES;

    }




This 'if' statement says that, when the text field resigns its first responder status (focus shifts to next field) if the text field has at least one character in it, enable the button.

Done button Enabled - Method 2: 
- Enable the done button only if both fields have at least one character in them...


Ok, I need to confess: this is an edit. The first attempt I wrote about here worked but only sort of. Then I discovered this way of doing things. What a revelation!
  • In the form view controller .h file, create a new action in the @interface section: 
- (IBAction)textField:(id)sender;
  • Go to the Storyboard scene for your 'form'.
  • Select a text field, then ctr-click. Up should pop a display of connections for the Text Field. 
  • Drag a connection from 'Editing Changed' to the View Controller icon as shown:

  •  Then select the IBAction textField which you just created, as shown:

  • Do this for each text field. This hooks up the action code with the interface builder action. When any key presses happen in the text field, each key press will call this action.
  • In the view controller .m file, add a brand new function with this code:
- (void)updateDoneButtonStatus:(id)sender {
    int a = [self.birdNameInput.text length];
    int b = [self.locationInput.text length];
        
    if ((a != 0) && (b != 0)) {

        self.navigationItem.rightBarButtonItem.enabled = YES;
        
    } else if ((a == 0) || (b == 0))  {

        self.navigationItem.rightBarButtonItem.enabled = NO
        
    }
}

[self.birdNameInput.text length] gets the length of string within the text field every time a key is pressed. I assigned these to int a and int b to keep the code tidy and easy to read and I used short names because in the scope they are against easy to read and see what they do; and are only accessible within this function action anyway. 

The first part of the 'if' statement will only enable the 'done' button if both fields have at least one character in them. The second part will disable the button if any fields character count goes back to zero. Using this function makes the code reusable, as you will see in the next step.
  • Change the textFieldDidEndEditing function, and add the code for the new action as here:
- (void)textFieldDidEndEditing:(UITextField *)textField 
{  
    [self.birdNameInput addTarget:self action:@selector(updateDoneButtonStatus:)    
                             forControlEvents:UIControlEventEditingChanged];
    [self.locationInput addTarget:self action:@selector(updateDoneButtonStatus:) 
                             forControlEvents:UIControlEventEditingChanged];    


- (IBAction)textField:(id)sender {
    [self.birdNameInput addTarget:self action:@selector(updateDoneButtonStatus:) 
                             forControlEvents:UIControlEventEditingChanged];
    [self.locationInput addTarget:self action:@selector(updateDoneButtonStatus:) 
                             forControlEvents:UIControlEventEditingChanged];
}

Yes, the code in both these functions/actions is exactly the same. This is the body of the IBAction we hooked up at the start of this method.

action:@selector(updateDoneButtonStatus:) selects the function we made just that turns the done button on and off; forControlEvents:UIControlEventEditingChanged hooks us back to the 'Sent Events' connection we made in the Storyboard/interface builder.
Currently the only way I know is to specify one for each text field we want this to work on, but I'll be on the look out for more efficient coding methods!


And finally...
You could use this to refer to the Done button specifically:

    UIBarButtonItem *donebutton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(ondoneButtonPressed)];
    self.navigationItem.rightBarButtonItem = donebutton;
    //make done button inactive
    donebutton.enabled=NO;

But that's enough for now.

Table View forms: scroll to a selected text field's row

If, like me, you sometimes completely fail to understand Apple's Class Reference documentation, then we're here for each other!

I had reached the end of "Your Second iOS Application" and decided to try to learn new skills by extending it. What better than to have the keyboard appear as soon as you enter a form like table. Then, to press next and it go to the next field. When you are in the next field, scroll the view up so it is visible and usable.

The first two ideas were easy. But the last, scrolling the text field's row in to view, seemed a simple problem really, but after searching for two hours and coming no where close, I was near to giving up when stumbled up on the solution.

(If you only need to implement the scroll to action, scroll to the 'BUT!' of this post!)

Making the keyboard appear when the View Appears
This one is simple. You want the table view controller of your 'form' to make the keyboard appear when the view appears.

This example assumes you have textfields in your table view, that they are set up in a nib file or storyboard, and connected as outlets to your code with useful names. In the case of the "Second iOS App" tutorial, this would mean you control dragged from the textfields in the storyboard to create the IBOutlet code in the AddSightingViewController.h file, resulting in code like this:


@interface AddSightingViewController : UITableViewController <UITextFieldDelegate>

@property (weak, nonatomic) IBOutlet UITextField *birdNameInput;
@property (weak, nonatomic) IBOutlet UITextField *locationInput;

Two things to note, you must have <UITextFieldDelegate> for everything that follows to work.

To make the keyboard appear I used becomeFirstResponder in the  viewDidAppear: set up.

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    [self.birdNameInput becomeFirstResponder];
}

[At the moment my first field is 'birdNameInput', but what if this changes? We will come to that another time.]

Making 'Return' go to the next input field
You textfield will have 'Return' on the keyboard by default. You can change this in Storyboard by:
  1. Selecting the textfield
  2. Going to 'attributes' (alt-cmd-4)
  3. In the 'Return Key' drop down list, select 'Next' or which ever is appropriate.
Now this is why the <UITextFieldDelegate> declaration in @interface was important. It enables us to use the delegate protocol which gives us all sorts of lovely extra functions and customisations. Currently in the tutorial, if you press return in either field they keyboard will be dismissed. It looks like this:

- (BOOL)textFieldShouldReturn:(UITextField *)textField 
{
    if ((textField == self.birdNameInput) || (textField == self.locationInput) {
        [textField resignFirstResponder];
    }    
        
    return YES;
}

If the textfield is either of them and return is press, dismiss the keyboard. To make our example select the next textfield, we want to use our becomeFirstResponder method to get to it. Change the if statement:

- (BOOL)textFieldShouldReturn:(UITextField *)textField 
{
    if (textField == self.birdNameInput) {
        [self.locationInput becomeFirstResponder];
        
        // scroll to row!

    } else if (textField == self.locationInput) {
        [textField resignFirstResponder];
    }
    return YES;
}
And return (or whatever you made the button) will go to the next from the first field, and on the last it will dismiss itself. Great. 

BUT! How do I scroll to the row of tableView? 
Obviously for this small example it isn't an issue in portrait view, but if you are in landscape, the second field is obscured by the keyboard, and you have to scroll the view to see it as you type. A terrible user experience!

As I have hinted in the code above, I want to scroll to the row that I have just made 'First Responder'. You can look at Apple's Developer Document if you want. It's in the 'UITableView Class Reference' part  
under '-scrollToRowAtIndexPath'. Look at it here now if you really want, but it's not pretty. Well, not for noobs. 

It turns out the answer is simple. In the tutorial we've used a Table View with static Cells, so we know we have one section (section 0) and two rows (Row 0 and row 1). We've made the textfield in row 1 the first responder. [In more complex examples you could get this programmatically, but that's not this post's point. ] So we can now add this very simple code:


        // scroll to row!
        [self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:YES];

When I tap 'next'/'return'/whatever(!) the textfield comes magically in to view. 

It works by telling (sending a message if you like) to tableView that it needs to enact the scrollToRow action. You KNOW the index path for the row: 1. So NOW it all 'just works'. Now we can understand more fully what the class reference is getting at...! 

The UITableViewScrollPosition accepts a few different options: top, middle or bottom. I used Bottom and it worked great, but experiment with them yourself to see which best fits your needs. Find out more about that here: "TableViewScrollPosition"

In Conclusion
Hopefully seeing some code actually 'in action' will help you understand just a little bit more how it all works and comes together. Eventually I suppose I will understand how to read and put these reference documents in to practice, but at the moment they do seem to just make me go cross eyed. 

Any comments, additions or corrections etc. are very welcome!

Firstly: I am no expert!

This blog is mainly for all the newbies at iOS who get frustrated at all the so called 'help' that they find online. You know, the terse one line replies or the "you simply do this..." and leave you still scratching your head.

I started this blog because one night (tonight) I was searching for the answer to a seemingly simple problem. Aren't they always the case, except you can't find a simple answer, or even one that helps you?

Tonight's 'little' question ended up having the simplest of answers, but too many "technical" people out there can't seem to explain simply what I need to learn. So I thought, share what I learn when I learn it, and pass it on. (I did used to be a teacher I suppose!)

But know this, I really am no expert! I'm probably just like you, a relative newbie to all this. If you are further on than me, or know a better way, then please: share and help each other.

In the mean time, happy iOS programming!