Tuesday, September 13, 2011

iOS: Dismissing the keyboard and other devilish details

Now that we've finally managed to set the default setting on the HUGE UIPicker control, we're shifting our focus to editing the beginning and ending quiz numbers on the settings screen. Note that, unlike the Android application, our first offering on for the iPhone won't have a full feature set. For example, I'm not planning to include SRS or autoplay mode to start out. So, for now we'll have space on the settings view.

So, the first step is to bring up the project in Xcode. Well, what are the widgets?

Here we go:

IBOutlet UITextField *startNum;
IBOutlet UITextField *endNum;

Let's hook them up in the IB. Right click on file - drag and drop to the related fields. This is getting to be old hat!

What next? We need to figure out how to make the field numeric only, for starters. Let's take a look at the documentation.

It looks like we have some notifications:

<blockquote>UITextFieldTextDidBeginEditingNotification
Notifies observers that an editing session began in a text field. The affected text field is stored in the object parameter of the notification. The userInfo dictionary is not used.
Availability

Available in iOS 2.0 and later.

Declared In
UITextField.h
UITextFieldTextDidChangeNotification
Notifies observers that the text in a text field changed. The affected text field is stored in the object parameter of the notification.
Availability

Available in iOS 2.0 and later.

Declared In
UITextField.h
UITextFieldTextDidEndEditingNotification
Notifies observers that the editing session ended for a text field. The affected text field is stored in the object parameter of the notification. The userInfo dictionary is not used.
Availability

Available in iOS 2.0 and later.

Declared In
UITextField.h
</blockquote>


Googling for how to edit these, there seem to be a lot of choices. Let's pick one almost at random. How about this:
<blockquote>
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{

NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
[numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle];

NSNumber* candidateNumber;

NSString* candidateString = [textField.text stringByReplacingCharactersInRange:range withString:string];

range = NSMakeRange(0, [candidateString length]);

[numberFormatter getObjectValue:&candidateNumber forString:candidateString range:&range error:nil];

if (([candidateString length] > 0) && (candidateNumber == nil || range.length < [candidateString length])) {

return NO;
}
else
{
return YES;
}
}

</blockquote>

It also says to "be sure to set your text field delegate."

I think this is where the class uses angled brackets to declare a protocol, and then somehow sets itself as the protocol for a given field.

Here's a good response from SO:

http://stackoverflow.com/questions/1051842/simple-delegate-example

First of all, a nice description of delegates:


<blockquote>A delegate is an object whose objects are (generally) called to handle or respond to specific events or actions.
You must "tell" an object which accepts a delegate that you want to be the delegate. This is done by calling

[object setDelegate:self];

or setting

object.delegate = self;

in your code.
The object acting as the delegate should implement the specified delegate methods. The object often defines methods either in a protocol, or on NSObject via a category as default/empty methods, or both. (The formal protocol approach is probably cleaner, especially now that Objective-C 2.0 supports optional protocol methods.)
When a relevant event occurs, the calling object checks to see if the delegate implements the matching method (using -respondsToSelector:) and calls that method if it does. The delegate then has control to do whatever it must to respond before returning control to the caller.
</blockquote>

This was the OP's question:

<blockquote>I am learning ObjC and iPhone development. I wouldn't reach so far up as to say that I understand delegates and their use perfectly. Your First iPhone Application, found on the developer portal on Apple site, walks through in detail a very simple example that makes use of the TextField's delegate to override a method to make the keyboard disappear when editing on the TextField is done. For example, if I can paste relevant snippets from there:
</blockquote>

This looks good, because I want to make the keyboard disappear as well.

Here's the sample code:
<blockquote>

@interface MyViewController : UIViewController <UITextFieldDelegate> {
UITextField *textField;
UILabel *label;
NSString *string;
}

@property (nonatomic, retain) IBOutlet UITextField *textField;
@property (nonatomic, retain) IBOutlet UILabel *label;
@property (nonatomic, copy) IBOutlet NSString *string;

- (IBAction)changeGreeting:(id)sender;

@end


// MyViewController.m
#import "MyViewController.h"

@implementation MyViewController

@synthesize textField;
@synthesize label;
@synthesize string;

- (BOOL)textFieldShouldReturn:(UITextField *)theTextField {
if (theTextField == textField) {
[textField resignFirstResponder];
}
return YES;
}


// I read something about the above. This gets rid of the keyboard when
// the user pressed done, or enter or something like that.
// But in order to do it, it needs to be a delegate for that field.


@end</blockquote>


So, we know we have to add this to the interface declaration in the header:

<UITextFieldDelegate>

So, now we have this:


<blockquote>@interface SettingsController : UIViewController <UIPickerViewDelegate, UIPickerViewDataSource, UITextFieldDelegate>
</blockquote>


What I don't see is anything in the example code that sets the delegate for the field as self.

Well, let's add it in, and then experiment with taking it out.

startNum.delegate = self;
endNum.delegate = self;


Sugar, even with it in there, it's not working.

Ah, here we go:

<blockquote>

- (BOOL)textFieldShouldReturn:(UITextField *)theTextField {
if (theTextField == startNum) {
[theTextField resignFirstResponder];
}
if (theTextField == startNum) {
[theTextField resignFirstResponder];
}

return YES;
}
</blockquote>


Nope. Still no good.

Hmmm. Well, I set a breakpoint on the textFieldShouldReturn: method, and it didn't get called. Obviously, I'm doing *something* wrong, but what?


This is confusing. It all seems to come down to this method:

<blockquote>
shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
</blockquote>

It's used for both checking the input range *and* dismissing the dialog box from what I can see.

Let's see if we can find any docs on it?

Well, first let's try this method:

http://stackoverflow.com/questions/274319/how-do-you-dismiss-the-keyboard-when-editing-a-uitextfield<blockquote>

-(IBAction)userDoneEnteringText:(id)sender
{
UITextField theField = (UITextField*)sender;
// do whatever you want with this text field
}

Then, in InterfaceBuilder, link the DidEndOnExit event of the text field to this action on your controller (or whatever you're using to link events from the UI). Whenever the user enters text and dismisses the text field, the controller will be sent this message.
</blockquote>

Ah, yes. That did it. At last. Now, the question is, how to mark the field as invalid if it's not a number that I like? How to advance to the next field? Really, I find that keyboard so annoying not going away, I'm so glad I found this method. Let's hook up the other field to that method as well.

Well, how do I check the name of the field, now that I have two of them? I think that was addressed in an earlier part of this post:


<blockquote> if (theTextField == startNum) {
[theTextField resignFirstResponder];
}
if (theTextField == startNum) {
[theTextField resignFirstResponder];
}
</blockquote>

Let's change that to this:

<blockquote>


NSLog(@"theField is: %@", theField);

if (theField == startNum) {
NSLog(@"StartNum: %@", startNum);
}
if (theField == endNum) {
NSLog(@"EndNum: %@", endNum);
}
</blockquote>


And there it is:

2011-09-13 19:01:48.235 JlptQuizApp[2779:207] theField is: <UITextField: 0x62419e0; frame = (126 281; 97 31); text = '34'; clipsToBounds = YES; opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x6243210>>

Actually, that's not working - it doesn't identify it as either field. So, maybe a better idea would be to have a separate method for each and eliminate the problem altogether.

We're making progress here. Slowly, but surely making progress.

Ok, how's this:

<blockquote>
-(IBAction)userDoneEnteringStartNum:(id)sender
{
UITextField *theField = (UITextField*)sender;

// do whatever you want with this text field

NSLog(@"Start Num is: %@", [theField text] );


}


-(IBAction)userDoneEnteringEndNum:(id)sender
{
UITextField *theField = (UITextField*)sender;

// do whatever you want with this text field

NSLog(@"End Num is: %@", [theField text] );


}
</blockquote>

With this result:

<blockquote>2011-09-13 19:15:33.261 JlptQuizApp[2860:207] End Num is: 40
2011-09-13 19:15:33.262 JlptQuizApp[2860:207] Start Num is: 40
</blockquote>


Actually, I had to delete the start num as an attached method by right-clicking on the file owner.

Ok, what's next on the agenda? Is there a way to default to a numeric keypad?

Btw, here is a good link describing keyboard input and options in iOS:

http://developer.apple.com/library/ios/#documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/KeyboardManagement/KeyboardManagement.html

Ah, this might help:

http://www.macfanatic.net/blog/2008/03/07/quick-iphone-sdk-observation/

<blockquote>
aTextField.borderStyle = UITextFieldBorderStyleRounded;
aTextField.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter;
aTextField.placeholder = @”Your name”;
aTextField.keyboardType = UIKeyboardTypeEmailAddress;

Some other examples of keyboard types:

UIKeyboardTypeDefault
UIKeyboardTypeNamePhonePad
UIKeyboardTypeNumberPad
UIKeyboardTypeNumbersAndPunctuation
UIKeyboardTypePhonePad
UIKeyboardTypeURL

Let's try this:

startNum.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
endNum.keyboardType = UIKeyboardTypeNumbersAndPunctuation;

Hmm...maybe the delegate assign is the problem; let's comment them out:

//startNum.delegate = self;
//endNum.delegate = self;

startNum.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
endNum.keyboardType = UIKeyboardTypeNumbersAndPunctuation;

Ah - it works. I could show an image of a numeric keypad - but you get the idea.

No comments:

Post a Comment