Saturday, June 05, 2010

Reset Keyboard for an Entry Field (iPhone)

During my first iPhone application development, I have tried to learn the Objective-C, Cocoa framework, the best practice to make my application in a well design structure. It's been very slow, but I think it worth every minutes and efforts I invested. After about one week hard working and struggling, today, I found a way to resolve an issue to reset keyboard for an entry field as specified in xib.

I have seem some applications have very nice feature to use iPhone keyboard's enter or next key switch an input to the next entry field if available. The issue I had is that bother fields in sequence have the same keyboard but with different mode, one in numeric and the next in alphabet, then the keyboard would not switch back to the required alphabet mode. Use is required to press ABC key to go back numeric mode if he/she needs.

The problem is caused by the setting of the field as responder within the event of textFieldShouldReturn:. The next field's textFieldDidBeginEditing: is called with the ...Return event. This nested call prevent the keyboard to change the default mode as required. Here is an example:


Input in Rate field. Press Next key to the Note field.

The keyboard says in the numeric mode.

I used my NSLog wrapper class to debug the sequence of calls and state messages.

[DEBUG] EditTaxViewController - textFieldDidBeginEditing
[DEBUG] EditTaxViewController - textFieldDidBeginEditing DONE
[DEBUG] EditTaxViewController - textFieldShouldReturn
--[DEBUG] EntryFieldHelper - findNextEntryFieldAsResponder:
----[DEBUG] EditTaxViewController - textViewDidBeginEditing
----[DEBUG] EditTaxViewController - textViewDidBeginEditing DONE
--[DEBUG] EntryFieldHelper - findNextEntryFieldAsResponder: return: true
[DEBUG] EditTaxViewController - textFieldShouldReturn DONE


As seem in the log, The event textFieldDidBeginEditing: was fired with the event textFiledDidEntEditing. The keyboard mode is not changed, since the keyboard was not resigned yet. What I need is to find a way to fire the textFieldDidBeginEditing afterwards. I have tried some many different ways, finally, with the debug message help, I realized this point and I found iPhone Notification framework is the way to resolve the issue.

I have created a helper class to encapsulate all the logic. Here is the way I use the helper class:

- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
[[mLogger debug:@"textFieldShouldReturn"] indent:YES];
// No more inputs?
if (![mEntryFieldHelper findNextEntryFieldAsResponder:textField])
{
...
}
[[mLogger indent:NO] debug:@"textFieldShouldReturn DONE"];
return YES;
}

- (void)viewDidLoad {
[[mLogger debug:@"viewDidLoad"] indent:YES];
[super viewDidLoad];
...
// Get all entry fields
if (!mEntryFieldHelper)
{
mEntryFieldHelper = [[EntryFieldHelper alloc] initWithView:self.view];
[mEntryFieldHelper registerForKeyboardNotifications];
// the first two fields (tax and rate) are OK, but the note
// field's keyboard needs to be reset
mEntryFieldHelper.inputFieldsWithKeyboardToBeReset =
[NSArray arrayWithObjects: [NSNumber numberWithInt:3], nil];
}

[[mLogger indent:NO] debug:@"viewDidLoad DONE"];
}
...
// In .h file:
@class EntryFieldHelper;

@interface EditTaxViewController : UIViewController <UINavigationBarDelegate,
UITextFieldDelegate, UITextViewDelegate>
{
...
@private
MyLogger* mLogger;
EntryFieldHelper* mEntryFieldHelper;
}


In the viewDidLoad event, the helper class instance is created with view, next register keyboard notifications and last set an array of integers for input fields which need reset keyboard.

In the event of textFieldShouldReturn:, the method of findNextEntryFieldAsResponder: is called to set the next responder. Within the helper class, all the logic auto-jump to next fields is implemented. The only requirement for the view class or parent is that all the input fields should have tag value set in sequence in xib file. As in this example, three fields in the view: name, rate and note.

The header file defines the variables, properties and methods:

@class MyLogger;
@interface EntryFieldHelper : NSObject {
@private
MyLogger* mLogger;
NSArray* mEntryFields;
NSArray* mInputFieldsWithKeyboardToBeReset;
}

@property (nonatomic, retain) NSArray* inputFieldsWithKeyboardToBeReset;

-(id) initWithView:(UIView*) aView;
-(BOOL) findNextEntryFieldAsResponder:(UIControl*)field;
@end

And here are implementations, first the initialization and dealloc:

import "EntryFieldHelper.h"
#import "MyLogger.h"

#define kInputFieldKey @"MyInputFields"

@implementation EntryFieldHelper

@synthesize inputFieldsWithKeyboardToBeReset = mInputFieldsWithKeyboardToBeReset;

-(id)initWithView:(UIView *)theView {

if (self = [super init]) {
mLogger = [[MyLogger alloc] initWithContext:@"EntryFieldHelper"];
[[mLogger debug:@"InitWithView:"] indent:YES];
NSMutableArray* entryFields = [[NSMutableArray alloc] init];
NSInteger tag = 1;
UIView* aView;
while (aView = [theView viewWithTag:tag] ) {
if (aView && [[aView class] isSubclassOfClass:[UIResponder class]]) {
[entryFields addObject:aView];
}
tag++;
}
mEntryFields = entryFields;
[mEntryFields retain];
[entryFields release];

[[mLogger indent:NO] debug:@"initWithView: DONE"];
}

return self;
}

-(void) dealloc {
[[mLogger debug:@"dealloc"] indent:YES];
// Remove the subscription of all notifications from the notification center.
[[NSNotificationCenter defaultCenter] removeObserver:self];
if ([mEntryFields retainCount] > 0) {
[mEntryFields release];
}
[mInputFieldsWithKeyboardToBeReset release];
[[mLogger indent:NO] debug:@"dealloc"];
[mLogger release];
[super dealloc];
}
@end

The class level NSArray mEntryFields is populated based on the input view's UIResponder type controls. The following is the method of findNextEntryFieldAsResponder:. A text field or text view is an input. Within the method, the field next to the input is searched within mEntryFields. If the found one does not require reset keyboard, it will become the next responder; if keyboard reset is required, then an notification is created and registered.

- (BOOL)findNextEntryFieldAsResponder:(UIControl *)field {
[[mLogger debug:@"findNextEntryFieldAsResponder:"] indent:YES];
BOOL retVal = NO;
for (UIView* aView in mEntryFields) {
// Find the entry field
if (aView.tag == (field.tag + 1)) {
NSNumber* tag = [NSNumber numberWithInt:aView.tag];
if ([self.inputFieldsWithKeyboardToBeReset containsObject:tag]) {
NSNotification* notification = [NSNotification notificationWithName:kInputFieldKey
object:aView];
[[NSNotificationQueue defaultQueue]
enqueueNotification:notification postingStyle:NSPostWhenIdle
coalesceMask:NSNotificationCoalescingOnName forModes:nil];

[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardShowNotification:)
name:kInputFieldKey object:nil];
}
else {
[aView becomeFirstResponder];
}
retVal = YES;
break;
}
}
[[mLogger indent:NO] debug:@"findNextEntryFieldAsResponder: return: %@",
retVal ? @"true" : @"false"];
return retVal;
}

Last the async notification is received to this method:

- (void) keyboardShowNotification:(NSNotification*) notification {
[[mLogger debug:@"keyboardShowNotification:"] indent:YES];

UIResponder* responder = [notification object];
if (responder) {
[responder becomeFirstResponder];
}
// Notification received. Remove the notification.
[[NSNotificationCenter defaultCenter] removeObserver:self name:kInputFieldKey object:nil];

[[mLogger indent:NO] debug:@"keyboardShowNotification: DONE"];
}

As expected here is the snap-shot of screen from rate to note:



hand here is the log:

[DEBUG] EditTaxViewController - textFieldDidBeginEditing
[DEBUG] EditTaxViewController - textFieldDidBeginEditing DONE
[DEBUG] EditTaxViewController - textFieldShouldReturn
--[DEBUG] EntryFieldHelper - findNextEntryFieldAsResponder:
--[DEBUG] EntryFieldHelper - findNextEntryFieldAsResponder: return: true
[DEBUG] EditTaxViewController - textFieldShouldReturn DONE
[DEBUG] EntryFieldHelper - keyboardShowNotification:
--[DEBUG] EditTaxViewController - textViewDidBeginEditing
--[DEBUG] EditTaxViewController - textViewDidBeginEditing DONE
--[DEBUG] EntryFieldHelper - <UITextView: 0x3d09bd0; frame = (20 143; 287 77); text = 'Test'; clipsToBounds = YES; opaque = NO; autoresize = W; tag = 3; layer = <CALayer: 0x3d09db0>> is responder
[DEBUG] EntryFieldHelper - keyboardShowNotification: DONE

0 comments: