Comprendre la gestion des vues ! Puis RootViewController

Sur l »iPhone, contrairement aux ordinateurs, vous n’avez qu’une seule fenêtre de disponible pour votre application. Ainsi, l’affichage se fera selon un empilement des vues. Étant débutant, il est assez difficile de le visualiser. Faisons-le ensemble !

http://www.ipup.fr/forum/userimages/Capture-d-e-769-cran-2011-03-30-a-768-14.14.06.png

Dans cette fiche, nous allons très simplement manipuler deux vues, que l’on enlèvera/affichera grâce à un bouton. Ensuite, nous verrons comment implémenter une gestion plus compliquée, en utilisant un RootViewController.

Commençons simplement !

Tout d’abord créez un nouveau projet de type Window-based Application que vous nommerez GestionVues. Ajoutez ensuite un fichier de type UIViewController que vous nommerez RootViewController. Enfin, ajoutez deux fichiers de type UIView (Objective-C class fleche Subclass of UIView), que vous nommerez respectivement Vue1 et Vue2.

Dans un premier temps, nous allons ajouter la vue du RootViewController à la fenêtre, dans l’application delegate.
Dans GestionVuesAppDelegate.h

#import <UIKit/UIKit.h>
 
@class RootViewController; // 1
 
@interface GestionVuesAppDelegate : NSObject <UIApplicationDelegate> {
 
RootViewController *rootViewController;
 
}
 
@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) RootViewController *rootViewController; // 2
 
@end

 

Dans GestionVuesAppDelegate.m

#import "GestionVuesAppDelegate.h"
#import "RootViewController.h"
 
@implementation GestionVuesAppDelegate
 
@synthesize window=_window;
@synthesize rootViewController;
 
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
RootViewController *viewController = [[RootViewController alloc] init];
self.rootViewController = viewController;
[viewController release];
 
[self.window addSubview:rootViewController.view]; // 3
[self.window makeKeyAndVisible];
 
return YES;
}
 
- (void)dealloc
{
[rootViewController release];
[_window release];
[super dealloc];
}

 

Rien de nouveau dans ce code, si ce n’est une petite différence avec l’objet rootViewController instancié. En effet, ligne 2, on se sert du mécanisme @property et @synthesize qui, rappelez-vous, remplace les getters/setters.
Ligne 1, nous utilisons le mot-clé @class. Cela permet de spécifier au compilateur que tout usage de RootViewController en tant que nom de classe, dans la déclaration de variables à l’intérieur d’une autre classe (ici GestionVuesAppDelegate) est autorisé.
Ligne 3, on ajoute la vue du RootViewController à la pile de vues une fois que tout s’affichera. Il y aura donc la window, et par dessus, la vue du RootViewController.

Ensuite, dans le RootViewController, nous allons ajouter la vue1.

#import <UIKit/UIKit.h>
#import "Vue1.h"
 
@interface RootViewController : UIViewController {
Vue1 *vue1;
}
 
@end

 

Dans le .m :

- (void)loadView
{
[super loadView]; // 1
vue1 = [[Vue1 alloc] initWithFrame:self.view.bounds];
[self.view addSubview:vue1];
vue1.backgroundColor = [UIColor redColor];
}
 
- (void)dealloc
{
[super dealloc];
[vue1 release];
}

 

Il est important de noter que lorsque l’on n’utilise pas de fichier .xib, la vue du view controller n’est pas allouée par défaut. Il faut donc le faire manuellement ligne 1 en appelant le loadView de super. Cette méthode sera appelée automatiquement si view pointe vers nil. Si vous lancez un fichier .xib mais que vous créez tout de même votre interface à la main, il faudra implémenter initWithNibName: bundle: voire viewDidLoad.
Vous remarquerez que le [super loadView] n’est pas proposé par défaut. En fait, c’est parce que vous pouvez choisir vous même quel serait la vue  (une scroll view, une table view, … ) sans pour autant l’empiler par dessus une vue inutile

Ensuite, ajoutons dans la vue 1 un bouton qui permettra de changer de vue, ainsi qu’un label pour afficher le nom de la vue.
Dans Vue1.h

#import <UIKit/UIKit.h>
#import "GestionVuesAppDelegate.h"
 
@interface Vue1 : UIView {
UILabel *labelStatut;
}
 
@end

 

Vue1.m

#import "Vue1.h"
#import "RootViewController.h"
 
@implementation Vue1
 
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[self addSubview:button];
 
labelStatut = [[UILabel alloc] initWithFrame:CGRectMake(30, 30, 250.0, 30.0)];
labelStatut.textColor = [UIColor blackColor];
labelStatut.backgroundColor = [UIColor clearColor];
labelStatut.text = @"On est sur la vue 1";
[self addSubview:labelStatut];
}
return self;
}
 
- (void) allerALaVue2:(id)sender {
GestionVuesAppDelegate *appDelegate = (GestionVuesAppDelegate*)[[UIApplication sharedApplication] delegate]; // 1
[appDelegate.rootViewController changePourLaVue2]; // 2
}
 
- (void)dealloc
{
[labelStatut release];
[super dealloc];
}
 
@end

 

Nous allons nous servir du rootViewController pour échanger nos vues, vue1 et vue2. Pour accéder à l’objet rootViewController instancié par l’application delegate, on fait tout d’abord une référence vers le delegate de l’application ligne 1. Ensuite, on appelle la méthode changePourLaVue2 du rootViewController ligne 2.
Faisons le point sur notre pile de vues : par-dessus la vue du rootViewController, nous affichons la vue1, qui contiendra un bouton et un label. On aura donc la window, puis par dessus, la vue du rootViewController. Ensuite, vient s’ajouter la vue1, puis un bouton et un label !

Implémentez Vue2.h et Vue2.m de la même manière. Seules ces lignes changeront :

// et
 
labelStatut.text = @"On est sur la vue 2";
// puis
- (void) allerALaVue1:(id)sender {
GestionVuesAppDelegate *appDelegate = (GestionVuesAppDelegate*)[[UIApplication sharedApplication] delegate];
[appDelegate.rootViewController changePourLaVue1];
}

 

Il ne reste plus qu’à ajouter ou enlever les bonnes vues. Pour commencer, modifier le RootViewController.h :

#import <UIKit/UIKit.h>
#import "Vue1.h"
#import "Vue2.h"
 
@interface RootViewController : UIViewController {
Vue1 *vue1;
Vue2 *vue2;
}
 
- (void) changePourLaVue1;
- (void) changePourLaVue2;
 
@end

 

Puis ajouter dans le .m

- (void) changePourLaVue1 {
[vue2 removeFromSuperview]; // 1
[self.view addSubview:vue1]; // 2
}
- (void) changePourLaVue2 {
if(!vue2)
vue2 = [[Vue2 alloc] initWithFrame:self.view.bounds]; // 3
[vue1 removeFromSuperview];
[self.view addSubview:vue2];
}

 

C’est l’appel à removeFromSuperview (enlever vue2 de la vue sur laquelle elle repose : rootViewController.view) qui va enlever vue2 ligne 1. Ensuite, on ajoute ligne 2 la vue1 à la vue du rootViewController grâce à la méthode addSubview:.

C’est ici qu’il peut y avoir une confusion. En effet, en enlevant la vue2, on enlève également le bouton et le label. Pourquoi ? Tout simplement parce que le bouton et le label sont ajoutés à la vue2. Ainsi, il faut préciser que la notion de pile de vues comprend des vues avec des sous-vues potentielles.
Finalement, le label et le bouton appartiennent à la vue2. En fait, pour chaque vue, on peut retrouver ses sous-vues grâce à NSArray *sousVues = [maVue subviews];. De fait, un removeFromSuperview enlève la vue et toutes ses sous-vues de la pile.
De même, [maVue superview] renvoi à la vue sur laquelle est posée maVue.

Notez que l’on peut également ajouter une vue dans la pile grâce à insertSubview: atIndex:. Ainsi, on peut choisir à quelle position insérer la vue dans la pile. En effet, à chaque vue dans la pile correspond un index. Par exemple, dans vue1, le bouton est à l’index 0, le label à l’index 1 dans la sous-pile de vue1.
Vous pouvez également déplacer une vue dans la pile, avec les méthodes suivantes :
• bringSubviewToFront: Met la vue passée en paramètre au-dessus de la pile de vues.
• sendSubviewToBack: Met la vue passée en paramètre en dessous de la pile de vues.
• insertSubview:aboveSubview: Insère la vue passée en paramètre 1 au-dessus de la vue passée en paramètre 2.
• insertSubview:belowSubview: Insère la vue passée en paramètre 1 en dessous de la vue passée en paramètre 2.
• exchangeSubviewAtIndex:withSubviewAtIndex: Échange les vues en fonction de leur index.

Enfin, notez que ligne 3, on teste si la vue2 a déjà été allouée ou non. Si ce n’est pas le cas, on le fait. La raison en est très simple : par défaut, on affiche la vue1. Mais rien ne garantit que l’utilisateur va afficher la vue2. Dans ce cas, pourquoi allouer de la mémoire pour un objet qui ne sera pas utilisé ? C’est une règle d’or que vous devrez respecter par la suite : éviter tout chargement mémoire inutile !


Nous avons vu une manière très simple de faire de la gestion de vues. Mais ce n’est pas vraiment terrible pour une architecture plus complexe ! CVous serez probablement amené par la suite à recourir à un rootViewController qui sera un objet de gestion des contrôleurs au-dessus. Pour cela, imaginez ce cas concret : une application avec un menu (MenuViewController), une vue de jeu (JeuViewController) et une vue de paramètres (ParametresViewController). Pour naviguer facilement entre ses vues, une solution consiste à utiliser un RootViewController, qui servira à changer les vues entre elles. Prenons ce cas et réalisons-le.

Utiliser un RootViewController

Vous allez donc construire cette interface. Commencez par créer un projet Window-based Application que vous nommerez UtiliserUnRoot. Ajoutez ensuite quatre view controllers : RootViewController, JeuViewController, MenuViewController et ParametresViewController.

Ajoutez la vue du rootViewController à la window dans l’application delegate, comme au début de ce tutoriel. Essayez maintenant de construire le schéma suivant :

1. Lorsque le rootViewController apparaît, ajoutez la vue du menu.
2. Ajoutez dans la vue du menuviewcontroller deux boutons qui serviront à se rendre sur la vue du jeu ou la vue des paramètres, ainsi qu’un label affichant le nom de la vue courante.
3. Dans les vues des deux contrôleurs de Jeu et Parametres, ajoutez un bouton Retour au menu ainsi qu’un label indiquant le nom de la vue courante.

Notez que ce sera le RootViewController qui possédera les trois contrôleurs Jeu, Menu, et Parametres.
C’est un très bon exercice, qui vous permettra de vérifier vos acquis !

Voici ce que vous avez dû construire (je vous laisse construire ParametresViewController sur le modèle de JeuViewController) :
RootViewController.h

#import <UIKit/UIKit.h>
 
@class MenuViewController;
@class JeuViewController;
@class ParametresViewController;
 
@interface RootViewController : UIViewController {
 
MenuViewController *menuViewController;
JeuViewController *jeuViewController;
ParametresViewController *parametresViewController;
 
}
 
- (void) changePourJeu;
- (void) changePourParametres;
 
@end

 

RootViewController.m

#import "RootViewController.h"
#import "MenuViewController.h"
#import "JeuViewController.h"
#import "ParametresViewController.h"
 
 
@implementation RootViewController
// ....
- (void)dealloc
{
[menuViewController release];
[jeuViewController release];
[parametresViewController release];
[super dealloc];
}
// .....
- (void)loadView
{
[super loadView];
menuViewController = [[MenuViewController alloc] init];
[self.view addSubview:menuViewController.view];
}
 
- (void) changePourJeu {
 
}
 
- (void) changePourParametres {
 
}

 

MenuViewController.h

#import <UIKit/UIKit.h>
#import "UtiliserUnRootAppDelegate.h"
 
@interface MenuViewController : UIViewController {
UILabel *labelStatut;
}
 
@end

 

MenuViewController.m

- (void) loadView {
[super loadView];
UIButton *boutonJeu = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[boutonJeu setFrame:CGRectMake(20, 20, 160, 30)];
[boutonJeu addTarget:self action:@selector(changePourJeu:) forControlEvents:UIControlEventTouchUpInside];
[boutonJeu setTitle:@"Aller dans jeu" forState:UIControlStateNormal];
[self.view addSubview:boutonJeu];
 
UIButton *boutonParametres = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[boutonParametres setFrame:CGRectMake(20, 60, 200, 30)];
[boutonParametres addTarget:self action:@selector(changePourParametres:) forControlEvents:UIControlEventTouchUpInside];
[boutonParametres setTitle:@"Aller dans les paramètres" forState:UIControlStateNormal];
[self.view addSubview:boutonParametres];
 
labelStatut = [[UILabel alloc] initWithFrame:CGRectMake(30, 30, 260, 30)];
[labelStatut setCenter:self.view.center];
labelStatut.text = @"On est sur le menu";
labelStatut.textColor = [UIColor blueColor];
[self.view addSubview:labelStatut];
}
 
- (void) changePourJeu : (id) sender {
UtiliserUnRootAppDelegate *appDelegate = (UtiliserUnRootAppDelegate*)[[UIApplication sharedApplication] delegate];
[appDelegate.rootViewController changePourJeu];
}
 
- (void) changePourParametres : (id) sender {
UtiliserUnRootAppDelegate *appDelegate = (UtiliserUnRootAppDelegate*)[[UIApplication sharedApplication] delegate];
[appDelegate.rootViewController changePourParametres];
}

 

JeuViewController.h

#import <UIKit/UIKit.h>
#import "UtiliserUnRootAppDelegate.h"
 
@interface JeuViewController : UIViewController {
UILabel *labelStatut;
}
 
@end

 

JeuViewController.m

- (void)loadView
{
[super loadView];
UIButton *boutonMenu = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[boutonMenu setFrame:CGRectMake(20, 20, 160, 30)];
[boutonMenu addTarget:self action:@selector(changePourMenu:) forControlEvents:UIControlEventTouchUpInside];
[boutonMenu setTitle:@"Retour au menu" forState:UIControlStateNormal];
[self.view addSubview:boutonMenu];
 
labelStatut = [[UILabel alloc] initWithFrame:CGRectMake(30, 30, 260, 30)];
[labelStatut setCenter:self.view.center];
labelStatut.text = @"On est sur le jeu";
labelStatut.textColor = [UIColor redColor];
[self.view addSubview:labelStatut];
 
 
}
- (void) changePourMenu : (id) sender {
UtiliserUnRootAppDelegate *appDelegate = (UtiliserUnRootAppDelegate*)[[UIApplication sharedApplication] delegate];
// on appel changePourJeu qui va nous permettre de revenir au menu
[appDelegate.rootViewController changePourJeu];
}

 

Il ne vous aura pas échappé que nous n’avons écrit aucun code enlevant une vue pour en remettre une autre. Faisons-le pour JeuViewController en implémentant la méthode changePourJeu dans RootViewController.m. Dans cette méthode, nous allons regarder quelle vue du menu ou du jeu s’affiche. Ensuite, nous enlèverons la vue affichée pour mettre l’autre à la place.Vous allez voir, c’est un peu subtil…

- (void) changePourJeu {
if (!jeuViewController) { // rappelez-vous, on n'alloue que si l'on a besoin
jeuViewController = [[JeuViewController alloc] init];
}
 
// on fait référence sur les vues de menu et jeu
UIView *menuView = menuViewController.view;
UIView *jeuView = jeuViewController.view;
 
if([menuView superview] != nil) // si menuView repose sur une vue, alors on l'enlève // 1
{
[jeuViewController viewWillAppear:NO]; // 2
[menuViewController viewWillDisappear:NO];
[menuView removeFromSuperview];
[self.view addSubview:jeuView];
[jeuViewController viewDidAppear:NO];
[menuViewController viewDidDisappear:NO];
}
else // sinon, c'est jeuView que l'on enlève
{
[menuViewController viewWillAppear:NO];
[jeuViewController viewWillDisappear:NO];
[jeuView removeFromSuperview];
[self.view addSubview:menuView];
[menuViewController viewDidAppear:NO];
[jeuViewController viewDidDisappear:NO];
}   
}

 

Pour savoir quelle vue enlever, et quelle vue remettre, on teste ligne 1 si la superview de menu ne pointe pas vers nil (pointeur nul). En fait, cela revient à tester si la vue du menu est dans la pile de vue.
Ligne 2, on appelle explicitement les méthodes viewWillAppear:(BOOL)animated, viewWillDisappear:(BOOL)animated… On spécifie non pour l’animation car la transition se fera sans animations. Compilez et lancez votre application pour tester.

Faites de même pour afficher la vue de paramètres.

Pour finir, nous allons ajouter une animation de type flipside (retournement de 180°) à l’aide de méthodes de haut niveau de Core Animation. Modifiez la méthode ci-dessus pour obtenir :

- (void) changePourJeu {
if (!jeuViewController) { // rappelez-vous, on n'alloue que si l'on a besoin
jeuViewController = [[JeuViewController alloc] init];
}
 
// on fait référence sur les vues de menu et jeu
UIView *menuView = menuViewController.view;
UIView *jeuView = jeuViewController.view;
 
[UIView beginAnimations:@"flipAnimation" context:NULL]; // réinitialiser le layer
[UIView setAnimationDuration:0.5]; // durée de l'animation
if([menuView superview] != nil) // si menuView repose sur une vue, alors on l'enlève
{
[jeuViewController viewWillAppear:YES];
[menuViewController viewWillDisappear:YES];
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromLeft forView:self.view cache:YES]; // type de l'animation
[menuView removeFromSuperview];
[self.view addSubview:jeuView];
[jeuViewController viewDidAppear:YES];
[menuViewController viewDidDisappear:YES];
}
else // sinon, c'est jeuView que l'on enlève
{
[menuViewController viewWillAppear:YES];
[jeuViewController viewWillDisappear:YES];
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromRight forView:self.view cache:YES];
[jeuView removeFromSuperview];
[self.view addSubview:menuView];
[menuViewController viewDidAppear:YES];
[jeuViewController viewDidDisappear:YES];
}
[UIView commitAnimations]; // lancer l'animation
 
}

 

L’autre animation disponible est UIViewAnimationTransitionCurlUp ou UIViewAnimationTransitionCurlDown. Cette animation permet de simuler un retournement de page.

Voilà, ce tutoriel est maintenant terminé !!

Les sources GestionVues !
Les sources du Root !

À bientôt