Problem
Das Keyboard für die Texteingabe soll bei einem Touch-Event außerhalb eines Textfeldes automatisch verschwinden, egal welches Textfeld gerade den Eingabefokus besitzt. Um das Keyboard auszublenden wird das aktive Textfeld benötigt, der sogenannte “firstResponder”.
[myActiveTextField resignFirstResponder];
In einer größeren Anwendung können eine ganze Reihe von Textfeldern existieren die in verschiedenen Views liegen. Somit stehen wir vor dem Problem, wer ist der aktuelle firstResponder und wie kommen wir an diesen heran?
Der erste Ansatz zur Lösung des Problems war, dass jeder View, der Textfelder enthält, selbst für das ausblenden des Keyboards sorgt. Diese Lösung stellte sich aber schnell als recht unflexible heraus, da der Code zum Ausblenden des Keyboards in jedem dieser Views hinterlegt werden muss. Erschwerend kommt hinzu, dass diese Views keine Touch-Events außerhalb ihres Frames empfangen. Wir benötigen also einen View (Hauptview-Controller), der auf alle Touch-Events innerhalb der Anwendung hört.
Die nächste Idee beinhaltete eine zentrale Verwaltungsstelle für alle Textfelder. Der Touch-Event würde dann vom Hauptview-Controller ausgewertet werden. Dieser prüft jedes registrierte Textfeld, ob dies unter der Position des Touchs liegt. Somit hätten wir zumindest einen Teil des Codes an einer zentralen Stelle, aber müssten jedesmal die Textfelder der aktuellen Subviews an- und abmelden. Dies wiederum bedeutet einen Mehraufwand für den Programmierer. Somit war diese Lösung auch nicht akzeptabel.
Glücklicherweise gibt es die Eigenschaft “isFirstResponder” der Klasse UIView.
Dies brachte mich auf die Idee eine Methode im Hauptview-Controller zu implementieren, welche rekursiv über alle Subviews läuft auf der Suche nach dem firstResponder. Der komplette Code, der sich um das Ausblenden des Keyboards kümmert, kann nun direkt im Hauptview-Controller hinterlegt werden.
Lösung
Wir benötigen eine Benachrichtigung wann das Keyboard angezeigt und ausgeblendet wird. Zusätzlich wollen wir über Touchs informiert werden.
- (void)viewDidLoad
{
[super viewDidLoad];
// add listener for keyboard events
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWasShown:) name:UIKeyboardDidShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillBeHidden:) name:UIKeyboardWillHideNotification object:nil];
// register tap listener to detect taps
_hideKeyboardTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action: @selector(onTapGestureDetected:)];
_hideKeyboardTapRecognizer.enabled = FALSE;
_hideKeyboardTapRecognizer.cancelsTouchesInView = NO; // this prevents the gesture recognizers to 'block' touches
[self.view addGestureRecognizer:_hideKeyboardTapRecognizer];
}
- (void)viewDidUnload
{
[super viewDidUnload];
// remove the keyboard listener
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardDidShowNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil];
// remove tap listener
[self.view removeGestureRecognizer:_hideKeyboardTapRecognizer];
[_hideKeyboardTapRecognizer release];
_hideKeyboardTapRecognizer = nil;
}
Der Touch-Listener ist nur aktiv wenn das Keyboard eingeblendet ist, ansonsten benötigen wir keine Benachrichtigung.
- (void)keyboardWasShown:(NSNotification*)aNotification{
// enable the tap listener
_hideKeyboardTapRecognizer.enabled = TRUE;
}
- (void)keyboardWillBeHidden:(NSNotification*)aNotification{
// disable the tap listener
_hideKeyboardTapRecognizer.enabled = FALSE;
}
Sobald wir ein Touch-Event erhalten, prüfen wir ob dieser außerhalb eines Textfeldes stattfand und blenden in diesem Fall das Keyboard aus.
- (void)onTapGestureDetected:(UITapGestureRecognizer *)recognizer {
// get the location of the tap
CGPoint location = [_hideKeyboardTapRecognizer locationInView:self.view];
// get the view which is under the location
UIView *hittedView = [self.view hitTest:location withEvent:nil];
// check if the view contains the first responder
UIView *firstResponder = [self findFirstResponder:hittedView];
if(firstResponder){
location = [_hideKeyboardTapRecognizer locationInView:firstResponder];
// check if the tap was inside the first responder
if(CGRectContainsPoint(firstResponder.bounds, location)){
return;
}
}else{
// if not, we start the search from the main view
firstResponder = [self findFirstResponder:self.view];
}
// hide the keyboard
[firstResponder resignFirstResponder];
}
- (UIView *)findFirstResponder:(UIView*)startView{
if ([startView isFirstResponder]){
return startView;
}
UIView * firstResponder;
for(UIView * subView in startView.subviews) {
firstResponder = [self findFirstResponder:subView];
if (firstResponder != nil){
return firstResponder;
}
}
return nil;
}
Ich habe zum Testen ein einfaches Beispiel als Xcode-Projekt aufgesetzt, welches den Code in Aktion zeigt. Dies funktioniert auch mit verschachtelten Views. Wichtig ist, dass der Code im Hauptview-Controller der Anwendung hinterlegt ist. Ansonsten kann der firstResponder nicht in Views gefunden werden, welche außerhalb des Hauptviews liegen.






