CocoaTouch #1 – UITextField dans UITableView, UIScrollView et animations ergonomiques

J’ai parcouru de nombreux articles explicatifs et de nombreux forums sans trouver une solution suffisamment efficace pour intégrer de façon clair des UITextView dans un UICell au sein d’un UITableView, mais surtout sans trouver de solution a une problématique qui est de conserver le UITextField visible lors de la frappe au clavier. En effet la majeure partie des solutions décalent la vue sans se soucier de la possibilité de scroll, ce qui provoque des décalages impressionnants et une impossibilité de revenir voir les champs précédents. Ce tutorial propose une solution, tout en expliquant pas à pas depuis la création de la UITableView, en passant par l’intégration des UITextField, et en finissant par les différents calculs permettant d’arriver à un résultat satisfaisant (avec utilisation de Core Animation). Nous verrons également comment mettre des informations avant notre UITableView dans une même UIScrollView sans utiliser la ScrollView par défaut du TableView.

Arborescence virtuelle

Préliminaire

Lancez XCode et créez un nouveau projet de type iOS>View-Based Application pour iPhone (vous pouvez décocher Include Unit Tests dans l’assistant de création, nous ne nous servirons pas des tests unitaires). J’ai nommé le mien « JT_Blog_CocoaTouch_1″, si vous retrouvez cette chaine dans le tuto, ce sera votre nom de projet qui la remplacera.

Par défaut XCode dispose tout les fichiers à la racine de l’espace de travail du projet, je ne trouve pas cette disposition très claire et préfère créer des groupes dans l’arborescence virtuelle tels que ci-contre

Vous remarquerez que mes Headers « .h » sont à un emplacement différent de son « .m », je fais ceci tout simplement pour éviter d’alourdir visuellement l’arborescence (mon dossier Headers est toujours fermé), pour accéder aux headers, il suffit de faire glisser trois doigts vers le haut ou vers le bas du Trackpad ou d’appuyer sur Command+Option+Haut ou Command+Option+Bas.

Cellule éditable

Delegates

Maintenant, créez, dans le dossier Delegates, une nouvelle classe (Cocoa Touch > Objective-C class) qui hérite de UITableViewCell nommée « UITableViewCellEditable« , n’oubliez pas de glisser le header dans le dossier Headers/Delegates de l’arborescence.

Complétez le header de la façon suivante :


#import

@class UITableViewCellEditable;

@protocol UITableViewCellEditableDelegate
- (void)editDidFinish:(NSMutableDictionary *)result; //Méthode du delegate à implémenter dans notre UITableViewController qui sera appelée lors de la fin de l'édition du UITextField

@optional
- (void)editDidBegin:(UITextField *)field; //Méthode appelée lorsque le focus passe sur le UITextField

@end

@interface UITableViewCellEditable : UITableViewCell  { //ne pas oublier d'implémenter le delegate du UITextField
    IBOutlet UITextField *textField; //le UITextField qu'on associera à notre vue dans IB
    id  delegate; //déclaration du delegate
    NSString *key; //clé associé à notre UITextField
}

@property (nonatomic, retain) IBOutlet UITextField *textField;
@property (nonatomic, assign) id  delegate;
@property (nonatomic, retain) NSString *key;

@end

Rien de bien compliqué pour le moment.

Passons maintenant à l’implémentation des différentes méthodes de notre classe dans le « UITableViewCellEditable.m »

Vous n’avez pas besoin des méthodes d’initialisation, ni des méthodes déjà implémentées dans UITableViewCell, nous allons nous contenter de rediriger le textFieldDidBeginEditing (méthode déléguée de UITextField) vers notre fonction editDidBegin. Bien entendu ne pas oublier le dealloc pour préserver la mémoire et éviter des fuites :

#import "UITableViewCellEditable.h"

@implementation UITableViewCellEditable

@synthesize textField, delegate, key;

- (void)textFieldDidBeginEditing:(UITextField *)txtField {
    [[self delegate] editDidBegin:txtField];
}

- (void)dealloc
{
    [textField release];
    [key release];
    [super dealloc];
}

@end

Désormais nous avons un nouveau type de cellule pouvant être insérée dans le UITableView, qui délègue les différentes méthodes nécessaires à l’utilisation d’un UITextField en son sein.

Et dans IB ?

Passons un peu du côté d’interface builder, afin de créer notre model de cellule. Pour ce faire, dans le dossier/groupe Views, créez un nouveau xib vierge (New File > iOS > User Interface > Empty) pour iPhone et nommez le UITVCellEditable (TV=TableView).

Ajoutez dans cette vue un nouvel Objet « Table View Cell » (UITableViewCell) et renseignez notre classe dans l’onglet Identity Inspector : Class= UITableViewCellEditable.

Enfin ajoutez un « Text Field » (UITextField) dans notre « Table View Cell Editable ». Puis configurez de la façon suivante :

* Agrandissez le de façon à ce qu’il touche presque les deux bords de notre Table View Cell (laissez une marge, ce n’est pas vraiment visuel sans marge, surtout si la table view cell est utilisée en mode Grouped, avec des bords arrondis).

* Dans l’onglet Size Inspector du « UITextField », cochez l’autosizing de façon à ce que le TextField puisse s’agrandir horizontalement (toutes les flèches horizontales), puis faites de même avec l’autosizing de la « Table View Cell Editable ».

* Dans l’onglet Attribute Inspector du « UITextField », il est possible de personnaliser le UITextField de sorte à l’intégrer parfaitement à notre Cellule, pour ce faire, mettez un Border Style invisible, niveau accessibilité, mettez le Clear Button (petite croix pour vider le champs) à « Appears while editing »

* Dans l’onglet Attributes Inspector de « Table View Cell Editable », passez « Selection » à « None », pour désactiver le clic de sélection sur la cellule.

N’oubliez pas de lier le TextField à notre TableViewCell, en faisant un clic droit (ou ctrl+clic) et un glissement de la tableview au textfield, et sélectionnez textfield (vous pouvez lier en faisant un clic droit et en glissant textField sur l’objet correspondant).

Interface de notre application

Dans IB

Maintenant, nous allons nous occuper d’ajouter une UITableView à notre application, pour ce faire, ouvrez votre xViewController.xib (x est à remplacer par le nom de votre application, c’est la vue depuis laquelle MainWindow.xib charge sa vue au lancement de l’application).

Nous allons faire une interface de saisie d’information pour un utilisateur.

Par défaut, seule une vue est présente. Dans cette vue, ajoutez une « Scroll View » (UIScrollView). Puis à l’intérieur de cette ScrollView, ajoutez deux « Label » (UILabel) (l’un représentera le prénom l’autre le nom, deux champs qui seront juste affichés sans être éditables). Enfin, au même niveau, ajoutez une « Table View » (UITableView).

Maintenant passons à la configuration de tout ce beau monde.

Comme une image vaut mieux que 1000 explications, voici le rendu final dans Interface Builder :

Vue sous IB

Pour arriver à ce résultat, il ne faut pas hésitez à utilisation la fonction « Arrange » de l’onglet Size Inspector, pour étendre le Scroll View et la Table View aux bonnes dimensions.

Pour les deux labels, je pense qu’il n’y a pas besoin d’explication.

Quant à la Table View, changez le style en « Grouped », changez « Separator » à « None » (c’est plus joli), puis désactivez « Show selection on Touch) et désactivez le « Scrolling », pour l’empêcher de prendre la main en cas de scroll, nous laisserons cela à la Scroll View. Dans cette dernière (la Scroll View), sélectionnez pour Background : « Group Table View Background Color », en effet la Table View avec son background particulier ne couvre pas toute la Scroll.

Au niveau de l’autosizing, qui permet d’étendre les vues automatiquement en fonction des modifications de taille de l’élément parent, allez dans l’onglet « Size Inspector » de la « Table View », et coller à droite, en haut, à gauche, étendre horizontalement doivent être cochés (décochez donc coller en bas et étendre verticalement). Faites de même pour la « Scroll View ».

Maintenant faites un bouton de droite sur la « Table View » et liez le « datasource » et le « delegate » au « File’s Owner » (la classe correspondante au « File’s Owner » est contenu dans le fichier « .m » de même nom).

Déclaration des IBOutlets et des liaisons IB

Puis ouvrez le header du ViewController correspondant à la vue que vous venez d’éditer dans Interface Builder, remplissez le comme suit :


#import

#import "UITableViewCellEditable.h" //ne pas oubliez d'importer le Headers du UITableViewCellEditable

@interface JT_Blog_CocoaTouch_1ViewController : UIViewController  {
    IBOutlet UITableView* tblView;
    IBOutlet UIScrollView* scrlView;
    IBOutlet UILabel* lblFirstName;
    IBOutlet UILabel* lblLastName;
    UITextField* curTextField; //le UITextField actuellement en cours d'édition (nous verrons tout à l'heure l'utilité)

    NSArray* tableViewDataSource; //le DataSource (contenu) de notre tableView
    BOOL dataLoaded; //YES lorsque toutes les données de la tableview sont chargées, nous verrons l'utilité plus tard
    NSMutableDictionary* userDataSource; //le DataSource contenant les informations de l'utilisateur (vous remarquerez le "Mutable" qui signifie que le dictionnaire peut-être modifié)

    CGRect viewNormalFrame; //la taille originale de la vue (avant nos redimensionnements, histoire d'éviter des calculs pour la repositionner)
    CGRect keyboardFrame; //dimension du clavier
    BOOL keyboardOut; //est-ce que le clavier est affiché ?
    float keyboardAnimationDuration; //durée de l'apparition du clavier
    int keyboardAnimationCurve; //comportement dans le temps de l'animation (exemple: lent au début, plus rapide au milieu, lent à la fin)
}

- (void)keyboardWillShow:(NSNotification *)notification; //méthode appelé par une notification que l'on capturera

@end

Liaisons

Retournez ensuite dans votre vue sur InterfaceBuilder et faites les liaisons suivantes :

  • File’s Owner : scrlView => Scroll View
  • File’s Owner : tblView => Table View
  • File’s Owner : lblFirstName => Label – Prénom
  • File’s Owner : lblLastName => Label – Nom

N’oubliez pas d’enregistrer le fichier.

Un peu de code maintenant ;)

Méthodes utilisées

Dirigez vous ensuite vers le « .m » correspondant :

#import "JT_Blog_CocoaTouch_1ViewController.h"

@implementation JT_Blog_CocoaTouch_1ViewController

- (void)dealloc
{
    /* ne pas oublier les release pour préserver la mémoire vive */
    [super dealloc];
}

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    return (interfaceOrientation == UIInterfaceOrientationPortrait);
}

#pragma mark - Keyboard Handling
- (void)keyboardWillShow:(NSNotification *)notification {
    /* lorsque le clavier apparait pour la première fois, nous allons initialiser nos variables concernant le clavier */
}

#pragma mark - Text Field delegate
-(void)textFieldDidBeginEditing:(UITextField *)textField{
    /* animation de la vue afin de la réduire et de la mettre au bon niveau, en positionnant le scroll au bon endroit */
}

-(BOOL)textFieldShouldReturn:(UITextField *)textField{
	/* appellée lors de la fin d'édition, juste avant la disparition du clavier, la vue originale doit être restaurée */
}

#pragma mark - View lifecycle
- (void)viewDidLoad
{
    /* Préparation de notre DataSource */
    [super viewDidLoad];
}

- (void)viewDidAppear:(BOOL)animated{
    /*
     * Préparation des tailles et positions des différentes parties
     * Ajout d'un Observer sur l'apparition du clavier
     */
    [super viewDidAppear:animated];
}

-(void)viewWillDisappear:(BOOL)animated{
    /* On enlève l'Observer sur l'apparition du clavier, pour éviter d'utiliser des ressources en envoyant des notifications qui n'aboutiront nul part */
    [super viewWillDisappear:animated];
}

#pragma mark - TableView
- (void)editDidFinish:(NSMutableDictionary *)result {
    /* Fin de l'édition d'un champs, enregistrement de la valeur dans un tableau (généralement on utilise Core Data, mais ce n'est pas l'objectif de ce tutorial) */
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    /* UITableViewCell à afficher pour la cellule à l'index indexPath */
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    /* nombre total de section (groupes) dans la tableView en mode "grouped" */
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    /* nombre total de ligne dans la tableView dans la section/groupe section */
}

@end

Passons directement à la programmation de toutes ces méthodes.

viewDidLoad

Pour créer les différentes informations qui seront demandées, nous allons créer une arborescence de notre invention, je vous propose la suivante : un « NSArray » contient les sections, chaque section est un « NSArray » qui contient des « NSDictionnary » représentants chacun la clé et le label d’un champs. Pour le « userDataSource », rien de bien compliqué, cela peut-être un objet d’un model de CoreData, mais nous allons pour l’exemple, nous contenter d’un simple « NSMutableDictionary », nous allons renseigner deux valeurs qui devront, au final, apparaitre dans les champs textes correspondants.

- (void)viewDidLoad
{
    /* Préparation de notre DataSource */
    NSArray* sectionAddress = [[NSArray alloc] initWithObjects:
                               [[NSDictionary alloc] initWithObjectsAndKeys:
                                @"address", @"key",
                                @"Addresse", @"label",
                                nil],
                               [[NSDictionary alloc] initWithObjectsAndKeys:
                                @"city", @"key",
                                @"Ville", @"label",
                                nil],
                               [[NSDictionary alloc] initWithObjectsAndKeys:
                                @"zipCode", @"key",
                                @"Code postal", @"label",
                                nil],
                               nil];

    NSArray* sectionContact = [[NSArray alloc] initWithObjects:
                               [[NSDictionary alloc] initWithObjectsAndKeys:
                                @"phone", @"key",
                                @"Téléphone", @"label",
                                nil],
                               [[NSDictionary alloc] initWithObjectsAndKeys:
                                @"mail", @"key",
                                @"e-mail", @"label",
                                nil],
                               nil];

    NSArray* sectionProfil = [[NSArray alloc] initWithObjects:
                              [[NSDictionary alloc] initWithObjectsAndKeys:
                               @"hobbies", @"key",
                               @"Loisirs", @"label",
                               nil],
                              [[NSDictionary alloc] initWithObjectsAndKeys:
                               @"like", @"key",
                               @"Intérêts", @"label",
                               nil],
                              [[NSDictionary alloc] initWithObjectsAndKeys:
                               @"hate", @"key",
                               @"Déteste", @"label",
                               nil],
                              [[NSDictionary alloc] initWithObjectsAndKeys:
                               @"sports", @"key",
                               @"Sports", @"label",
                               nil],
                              [[NSDictionary alloc] initWithObjectsAndKeys:
                               @"movies", @"key",
                               @"Films", @"label",
                               nil],
                              [[NSDictionary alloc] initWithObjectsAndKeys:
                               @"musics", @"key",
                               @"Musiques", @"label",
                               nil],
                              [[NSDictionary alloc] initWithObjectsAndKeys:
                               @"books", @"key",
                               @"Livres", @"label",
                               nil],
                              [[NSDictionary alloc] initWithObjectsAndKeys:
                               @"videogames", @"key",
                               @"Jeux vidéos", @"label",
                               nil],
                              nil];

    tableViewDataSource = [[NSArray alloc] initWithObjects:sectionAddress,sectionContact,sectionProfil,nil];

    [sectionAddress release];
    [sectionContact release];
    [sectionProfil release];

    userDataSource = [[NSMutableDictionary alloc] initWithObjectsAndKeys:
                      @"Bordeaux",@"city",
                      @"33000",@"zipCode",
                      @"basket, footing, ...",@"sports",
                      nil];

    dataLoaded=NO; /* les données ne sont pas encore dans la table view, nous verrons plus tard pourquoi nous faisons cela */

    [super viewDidLoad];
}

numberOfSectionsInTableView:

Doit retourner le nombre de sections contenues dans la Table View, ça correspond pour nous au nombre d’éléments dans « arrTblViewDataSource ».

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    /* nombre total de section (groupes) dans la tableView en mode "grouped" */
    return [tableViewDataSource count];
}

tableView: numberOfRowsInSection:

Ici non plus, rien de bien compliqué, il faut récupérer le nombre d’élément dans la section.

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    /* nombre total de ligne dans la tableView dans la section/groupe section */
    return [[tableViewDataSource objectAtIndex:section] count];
}

tableView: cellForRowAtIndexPath:

En entrée de cette méthode, on a le tableView qui est en train de s’afficher, et l’indexPath qui correspond à la cellule actuellement à afficher.

A partir de ces informations, nous allons construire une cellule et lui retourner.

Quelques explications pour le code qui va suivre :

  • Notez l’utilisation de « dequeueReusableCellWithIdentifier » qui permet de charger la cellule en copiant une cellule déjà existante, dans le cas où aucune cellule n’est écrite avant, alors nous en créons une nouvelle.
  • Il ne faut pas non plus oublier de paramétrer les « delegate » des deux éléments de notre cellule
  • Enfin, il ne reste plus qu’a mettre les bonnes valeurs aux bons endroits !
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    /* UITableViewCell à afficher pour la cellule à l'index indexPath */
    static NSString *CellIdentifier = @"Cell";
    UITableViewCellEditable *cell = (UITableViewCellEditable *) [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

    if (cell == nil){
        NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:@"UITVCellEditable" owner:nil options:nil];
        for(id currentObject in topLevelObjects) {
            if([currentObject isKindOfClass:[UITableViewCellEditable class]]) {
                cell = (UITableViewCellEditable *) currentObject;
                break;
            }
        }
    }

    [cell setDelegate:self];
    [cell.textField setDelegate:self];

    NSDictionary* cellInfos=[[tableViewDataSource objectAtIndex:indexPath.section] objectAtIndex:indexPath.row];
    cell.key = [cellInfos objectForKey:@"key"];
    cell.textField.text = [userDataSource objectForKey:[cellInfos objectForKey:@"key"]];
    cell.textField.placeholder = [cellInfos objectForKey:@"label"];

    return cell;

}

editDidFinish:

Rien de bien compliqué, result correspond à un NSMutableDictionary, contenant toute les infos du textField dont l’édition vient de finir, il n’y a plus qu’à enregistrer ces infos (dans l’idéal en core data, mais pour l’exemple : ) dans notre « userDataSource ».

- (void)editDidFinish:(NSMutableDictionary *)result {
    /* Fin de l'édition d'un champs, enregistrement de la valeur dans un tableau (généralement on utilise Core Data, mais ce n'est pas l'objectif de ce tutorial) */
    [userDataSource setValue:[result objectForKey:@"text"]
                      forKey:[result objectForKey:@"key"]
     ];
}

dealloc

Finalement on termine par notre dealloc :


- (void)dealloc
{
    /* ne pas oublier les release pour préserver la mémoire vive */
    [userDataSource release];
    [tableViewDataSource release];
    [super dealloc];
}

Si vous avez été attentif jusque là, si vous avez suivit à la lettre ce tutorial, alors, vous pouvez lancer la compilation et démarrer le simulateur (bouton Play). L’application devrait être quasiment fonctionnelle.

Mais il y a quelques petits soucis, notamment l’impossibilité de scroller, mais aussi quelques soucis lors de l’édition, les champs ne se positionnant pas correctement. Nous allons voir comment résoudre tout ces petits soucis.

Finitions

Correction du Scroll

Le composant UITableView est fait de tel manière que seulement ce qui est affiché est chargé, c’est à dire, que si seulement 10 lignes sont visibles sur 20, l’appel à « - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath » ne se fera que 10 fois, ceci afin d’économiser les ressources, lors d’appel à des dataSources très couteux ou de plusieurs centaines de lignes.

Le petit hic, c’est que nous avons besoin de redimensionner la taille du tableView, afin que toute la tableView soit affichée (même en dehors de l’écran), dans la scrollView, ceci dans le but de pouvoir tout scroller, grâce à une seule et unique scrollView. Mais pour arriver à faire cela, il nous faut récupérer la hauteur du contenu de la tableView, cela se fait généralement grâce à la propriété « contentSize », mais cette propriété n’est valable que pour les cellules déjà chargées ! C’est à dire qu’avec notre exemple précédent, nous n’aurions la taille que de seulement 10 lignes sur 20…

Pour résoudre ce problème, nous allons, à l’apparition de la Vue globale, rafraîchir le UITableView avec la méthode « reloadData ». Aussi bizarre que cela puisse être, cette méthode fonctionne différemment du premier chargement et charge toutes les données et cellules. Une fois chargées, nous avons accès à la propriété contentSize qui contient désormais la bonne valeur à exploiter !

Afin de préparer nos opérations sur le clavier plus tard, nous allons observer les notifications sur l’apparition du clavier. Nous sauvegardons également la frame de la vue globale, afin d’éviter de la recalculer lorsque nous la redimensionnerons.


- (void)viewDidAppear:(BOOL)animated{
    /*
     * Préparation des tailles et positions des différentes parties
     * Ajout d'un Observer sur l'apparition du clavier
     */
    dataToLoad=YES;
    [tblView reloadData];

    [tblView setFrame:CGRectMake(tblView.frame.origin.x, tblView.frame.origin.y, tblView.frame.size.width, tblView.contentSize.height)];
    scrlView.contentSize=CGSizeMake(scrlView.frame.size.width, tblView.frame.origin.y+tblView.frame.size.height);
    viewNormalFrame=self.view.frame;

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];

    [super viewDidAppear:animated];
}

-(void)viewWillDisappear:(BOOL)animated{
    /* On enlève l'Observer sur l'apparition du clavier, pour éviter d'utiliser des ressources en envoyant des notifications qui n'aboutiront nul part */
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];
	[super viewWillDisappear:animated];
}

Vous souvenez vous de dataToLoad ? Je vous avez dit que nous en aurions besoin plus tard, je vais donc vous expliquez à quoi il va servir. Comme au chargement de la vue, nous rafraichissons manuellement la tableView, alors nous n’avons pas besoin de charger des lignes lorsqu’elle s’affiche la premiere fois, nous pouvons faire une économie de mémoire et de processeur à ce niveau là. Comme précédemment, nous avons passé à YES, dataToLoad, nous savons désormais quand le reloadData est appelé, nous pouvons donc modifier le delegate qui retourne le nombre de section, pour lui demander de retourner 0 lorsque dataToLoad est à NO, si il n’y a pas de section, aucune cellule ne sera chargée.

Modifiez la fonction « numberOfSectionsInTableView » tel que :

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    /* nombre total de section (groupes) dans la tableView en mode "grouped" */
    return dataToLoad ? [tableViewDataSource count]:0;
}

Testez, le scroll marche, tout les champs s’affichent désormais !

Désormais il nous reste seulement que quelques petits problèmes à régler :

 

 

Animer et montrer les cellules lorsque le clavier s’active

Avant toute chose, nous allons initialiser quelques variables dont nous allons avoir besoin, pour cela, ajoutez au début de la méthode « - (void)viewDidLoad » les initialisations suivantes :

        keyboardOut=NO;
        keyboardAnimationCurve=0;
        keyboardFrame=CGRectMake(0, 0, 0, 0);
        keyboardAnimationDuration=-1;
        curTextField=nil;

Nous allons utiliser la notification d’apparition du clavier pour récupérer les différents réglages d’animation du clavier afin de les reproduire. Une fois fait, nous pouvons supprimer « l’observer » d’apparition du clavier, nous n’en nécessiterons plus ensuite.

Enfin, nous verrons tout de suite après pourquoi, nous appelons la méthode « textFieldDidBeginEditing:curTextField ». En effet, dans cette méthode si nous n’avons pas encore récupéré les réglages, nous devons le faire avant (enregistrer le textField cliqué dans « curTextField »), et donc rappeler la méthode une fois fait.

- (void)keyboardWillShow:(NSNotification *)notification {
    /* lorsque le clavier apparait pour la première fois, nous allons initialiser nos variables concernant le clavier */
    if(keyboardAnimationDuration==-1 && curTextField!=nil){
        keyboardAnimationCurve=[[[notification userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey] intValue];
        keyboardAnimationDuration=[[[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
        keyboardFrame=[[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
        [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];
        [self textFieldDidBeginEditing:curTextField];
    }

}

Les petits calculs ^^

C’est maintenant qu’il va falloir être attentif… Nous allons faire quelques petits calculs.

Dans tout les cas, on ajuste la vue en hauteur juste au dessus du clavier.

Lors du clic sur un textField, s’il n’est pas visible, on va positionner le scroll de façon à ce qu’il le devienne.

On utilise bien entendu Core Animation avec les même réglages que possède le clavier afin d’obtenir une animation semblable.

Suivez les commentaires du code ci-dessous. En réfléchissant 5 minutes tout est normalement compréhensible.


-(void)textFieldDidBeginEditing:(UITextField *)textField{
    /* animation de la vue afin de la réduire et de la mettre au bon niveau, en positionnant le scroll au bon endroit */
    curTextField=textField;
    if(keyboardAnimationDuration!=-1) { //sinon on attend que keyboardwillappear soit executé
        CGRect viewFrame = self.view.frame;
        CGPoint contentOffset=scrlView.contentOffset; //position actuelle du scroll
        CGRect cellFrame = textField.superview.superview.frame; //correspond à la cellule en cours (deux vues au dessus de notre textfield
        int top=tblView.frame.origin.y+cellFrame.origin.y; //origine verticale de la cellule depuis le haut de la scrollView
        int newFrameHeight=viewNormalFrame.size.height-keyboardFrame.size.height; //on réajuste la vue en hauteur, de façon à ce que le bas de la vue globale soit coincide avec le haut de la frame du clavier

        if((top+cellFrame.size.height)-contentOffset.y>newFrameHeight) //si le bas du textinput plus bas que l'affichage
            contentOffset.y=top+cellFrame.size.height-newFrameHeight;
        else if(top-contentOffset.y<0) //si le haut du textinput plus haut que l'affichage
            contentOffset.y=top;

        [UIView beginAnimations:nil context:NULL]; //à partir de là on utilise Core Animation
        [UIView setAnimationDuration:keyboardAnimationDuration];
        [UIView setAnimationCurve:keyboardAnimationCurve];
        [scrlView setContentOffset:contentOffset animated:YES]; //on positionne le scroll au bon endroit, calculé précédemment, attention, setContentOffset ne fonctionne pas avec UIView beginAnimation, il faut specifier animated:YES
        if(!keyboardOut){ //si le clavier est déjà sorti, pas besoin de redimensionner la vue
            viewFrame.size.height=newFrameHeight;
            [self.view setFrame:viewFrame];
        }
        [UIView commitAnimations]; //et enfin on lance les animations

        keyboardOut=YES; //désormais, on peut dire que le clavier est sorti
    }
}

Fin d’édition

Finalement, il ne nous reste plus qu’a définir les actions lorsque nous avons finit d’éditer un textField. Si le clavier était sorti, il va se fermer, nous allons donc repositionner notre vue globale à son état d’origine.


-(BOOL)textFieldShouldReturn:(UITextField *)textField{
	/* appellée lors de la fin d'édition, juste avant la disparition du clavier, la vue originale doit être restaurée */
    [textField resignFirstResponder];

    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:keyboardAnimationDuration];
    [UIView setAnimationCurve:keyboardAnimationCurve];

    if(keyboardOut)
        [self.view setFrame:viewNormalFrame];

    [UIView commitAnimations];

    keyboardOut=NO;
    return YES;

}

A bientôt

Et oui ! C’est désormais fini ! Normalement tout fonctionne parfaitement bien ! Merci de m’avoir suivit pendant ce cours tuto :P ! Ne ratez pas la suite ;) .

Share and Enjoy:
  • Google Bookmarks
  • Facebook
  • del.icio.us
  • Digg
  • Live

2 Responses to “CocoaTouch #1 – UITextField dans UITableView, UIScrollView et animations ergonomiques”

  • Moustache Says:

    Merci pour ce tuto
    Cordialement

  • Vinetro Says:

    Excellent tuto que j’ai suivi pour faire une application iPad !

    Par contre je n’ai pas mis de ScrollView sous la TableView, étant donné que la la classe UITableView hérite de ScrollView, je n’ai pas pensé nécessaire de procéder comme tu le fais (peut être ai-je tords ^^).
    Pour la gestion de la remontée de la vue en cas de TextField caché, je suis sur l’iPad et j’ai deux soucis:

    En mode portrait, lorsque je procède au return sur le clavier une fois mon texte saisi, pour restaurer la vue de manière normal, j’ai un bug graphique assez désagréable (ça lag…)
    Et aussi, lorsque je passe en mode paysage, là c’est la cata’, rien ne fonctionne, le comportement est totalement aléatoire.

    As-tu une idée ?

    Je te remercie d’avance !

Leave a Reply