Navigation

‎Introduction à la programmation sur iPhone - Partie 1

Un article de ODcWiki.

Jump to: navigation, search
Image:Auteur.png Auteur : yamsellem.


Sommaire

La mobilité à porté de main

Avec l'App Store, première boutique d'applications tierces sur mobile, l'iPhone d'Apple a donné un joli coup de pied dans la fourmilière de la mobilité. Prenons un moment afin dʼexplorer les possibilités offertes à tout développeur - Objective-C et Cocoa Touch - via la réalisation d'une machine à sous. Cette application sera composée de 5 disques quʼun bouton “Insérer une pièce” fera tourner. Aligner 3 fruits ou plus sur ces disques affichera le prix remporté (ici, une peluche).

Un Mac Intel sous Léopard et un Apple ID (mail/password) sont obligatoires afin d'avoir accès à l'IDE xCode et au SDK 3.x, sur http://developer.apple.com/iphone. Proposés gratuitement, ces outils permettent le développement, le test sur simulateur et lʼoptimisation de performances. La distribution - sur son propre iPhone/iPod Touch et sur l'App Store - devant faire l'objet de l'acquisition d'une licence annuelle.

Objective-C, notions essentielles

En Objective-C, chaque classe se compose de deux fichiers aux noms identiques, un .h et un .m. Le .h déclare les propriétés et méthodes de la classe ainsi que sa classe mère et les différents protocoles quʼelle implémente. Le .m implémente les déclarations du .h. Les .h jouent deux rôles en Objective-C, interface ou protocole. Une @interface est la partie déclarative dʼune classe et doit être accompagnée dʼune unique implémentation du même nom (comme vu ci-dessus). Un @protocol est une liste de méthodes et peut être implémenté par plusieurs classes.

Les protocoles, en plus de permettre lʼutilisation du polymorphisme - quand plusieurs classes les implémentent -, sont utilisés abondamment par les composants standards afin de déléguer leur comportement aux classes qui les utilisent. Dans lʼexemple qui va suivre, la roue de fruits ne sera pas une classe dérivée de la classe standard idoine - UIPickerView - dont les méthodes auraient été surchargées, il sʼagira dʼun UIPickerView classique dont les protocoles - UIPickerViewDataSource, UIPickerViewDelegate - seront implémentés par le contrôleur lʼutilisant. La délégation est ainsi préférée à lʼhéritage.

Les éléments standards de ce type (qui nécessitent lʼimplémentation de protocoles particuliers afin de fonctionner) déclarent cette nécessité dans leur interface. Sans implémentation dans lʼappelant, rien nʼarrive.

Image:IPhonePartie1Figure1.jpg

Figure 1. Lʼécran de lʼapplication que nous nous apprêtons à créer

Création du contrôleur principal

Une fois installé, xCode est accessible dans /Developer/Applications. Commençons par créer un nouveau projet via le menu File > New Project.... Dans la colonne iPhone OS > Application sélectionnons Windowbased Application et nommons-la SlotMachine. Bien que des modèles prédéfinis soient proposés, partons de zéro à des fins didactiques.

Le répertoire Classes contient par défaut deux éléments : SlotMachineAppDelegate.h, une interface, et SlotMachineAppDelegate.m, son implémentation. Le raccourcis ⌘⌥↑ permet de basculer de lʼun à lʼautre.

#import <UIKit/UIKit.h>
@interface SlotMachineAppDelegate : NSObject <UIApplicationDelegate> {
   UIWindow *window;
   GameViewController *gameViewController; // seul ajout de notre part, le reste est généré
}
@end

La classe créée par défaut par xCode, SlotMachineAppDelegate, est le point dʼentrée de lʼapplication. Elle hérite de NSObject et implémente le protocole UIApplicationDelegate. Lorsque la main est donnée à lʼapplication, la méthode applicationDidFinishLaunching de ce protocole est appelée. Une instance de GameViewController, le contrôleur principal, sera créée lors de cet appel et sa vue, composée de 5 disques de fruits et dʼun bouton “Insérer une pièce”, initialisée automatiquement.

La création des 2 onglets, des 5 disques, du bouton et du label seront fait graphiquement à lʼaide dʼun autre outil, InterfaceBuilder. Pour le reste, lʼimplémentation de ces 2 classes suffira à faire fonctionner le jeu.

Image:IPhonePartie1Figure2.jpg

Figure 2. Les deux classes de notre application, SlotMachineAppDelegate et GameViewController (chacune dispose dʼun fichier .xib décrivant sa vue au format xml, MainWindow.xib et GameViewController.xib).

Passons à lʼimplémentation de cette interface dans SlotMachineAppDelegate.m (la syntaxe du langage est détaillée juste après) :

#import "SlotMachineAppDelegate.h"
@implementation SlotMachineAppDelegate
- (void)applicationDidFinishLaunching:(UIApplication *)application {
   gameViewController = [GameViewController alloc];
   [window addSubview: gameViewController.view];
   [window makeKeyAndVisible];
   // ajouter la vue de gameViewController à window
   // la fenêtre principale de l’application
}  // suffit à lui passer la main
 
- (void)dealloc {
   [gameViewController release];
   [window release];
   [super dealloc];
}
@end

Ajouter la vue du contrôleur du jeu (gameViewController) à la fenêtre de lʼapplication (via addSubview:) suffit à lui passer la main. Il en résultera lʼaffichage de la vue de ce contrôleur : GameViewController.xib, dotée de ses 5 disques de fruits.

Il est temps de créer ce contrôleur. Pour ce faire, cliquons droit sur le répertoire Classes puis Add > New File. Dans la liste iPhone > Cocoa Touch Class sélectionnons UIViewControllerSubclass et cochons With XIB for user interface. Nommons-le GameViewController. Déplaçons le xib, fichier décrivant la vue du contrôleur ainsi créé, dans le dossier Ressources.


Point technique : syntaxe des appels de méthodes

En Objective-C, les appels de méthodes se font sous la forme [object add:(int)value modulo:(int)mod].
Tous les arguments de la méthode sont nommés et typés. Lʼappel à cette méthode sʼeffectue via la syntaxe
[object add:9 modulo:4]. Lʼordre des arguments est fixe.

Création de la vue du jeu

Le fichier décrivant graphiquement notre application, GameViewController.xib, est situé dans le répertoire Ressources. Double-cliquer dessus ouvre InterfaceBuilder, le pendant graphique à xCode. Interface Builder est composé de quatre fenêtres, la librairie, liste de tous les éléments dʼinterface, lʼinspecteur, liste des propriétés de lʼobjet sélectionné, la fenêtre xib liste de tous les éléments du xib actuel et la vue courante affichée à la résolution de lʼiPhone, 320 * 480.

Image:IPhonePartie1Figure3.jpg

Figure 3. Interface Builder, lʼoutil permettant de construire lʼinterface

Nous allons instancier graphiquement le picker de fruits utilisé dans notre code, lui indiquer que le contrôleur actuel sera en charge de ses protocoles et le lier au code que nous avons précédemment écrit.

La vue de chaque contrôleur est matérialisée par un xib quʼInterface Builder peut éditer. Chacun des éléments (bouton, label...) ajoutés par ce biais devra être connecté à un objet du même type dans le code afin dʼy être manipulé (cela permet, par exemple, de modifier le texte dʼun label créé dans InterfaceBuilder).


Point technique : lier les objets de lʼinterface et du code

Lier les objets du code écrit dans xCode avec ceux positionnés dans Interface Builder est simple :
sélectionner le Fileʼs Owner - classe liée par lʼonglet 4 de lʼinspecteur au xib édité actuellement - tirer
lʼanneau en face de ses objets sur les objets déposés dans la vue dʼInterfaceBuilder.
Lier les actions marche dans le sens inverse, il faut commencer par sélectionner dans Interface Builder
lʼobjet qui sera responsable de lʼappel de la méthode, puis, via lʼonglet 2 de lʼinspecteur, sélectionner un
évènement et relier son anneau à une méthode de Fileʼs Owner.

Les manipulations suivantes requièrent, au préalable, la création de lʼinterface GameViewController.h :

#import <UIKit/UIKit.h>
#define nbFruits 4
#define nbPickers 5
@interface GameViewController : UIViewController <UIPickerViewDataSource, UIPickerViewDelegate> {
   UIPickerView *picker; // l’élément principal du jeu : la roue à 5 disques
   UILabel *winLabel; // le label indiquant le prix remporté
   UIButton *button; // le bouton “insérer une pièce”
   NSArray *column1, *column2, *column3, *column4, *column5; // 1 tableau de fruits par disque
}
 
@property(nonatomic, retain) IBOutlet UIPickerView *picker;
@property(nonatomic, retain) IBOutlet UILabel *winLabel;
@property(nonatomic, retain) IBOutlet UIButton *button;
 
-(IBAction)spin:(id)sender; 
// id, le type de sender, est un pointeur vers tous type d’objet
// Tous les objets n’héritent pas de NSObject, id est plus large
@end

Ajoutons un UIPickerView de la librairie en haut de notre vue. Le positionnement est facilité par des pointillés de pose et lʼaimant des bords. Posons également un UILabel et un UIButton. Renommons le bouton “Insérer une pièce” et décochons User Intercation Enabled du UIPickerView via lʼinspecteur afin dʼempêcher le joueur de sélectionner les fruits individuellement.

En sélectionnant Fileʼs Owner dans la fenêtre xib, lʼonglet 2 de lʼinspecteur laisse apparaître nos picker, button et winLabel - déclarés dans le code - suivis dʼun anneau. Il suffit de tirer lʼanneau - qui tendra alors un fil bleu - à partir du contrôleur et de le relier à lʼicône des trois éléments graphiques créés à lʼinstant. Lions également le bouton à son action, en le sélectionnant puis, via lʼonglet 2 de lʼinspecteur, en tirant lʼanneau de Touch Up Inside (qui signifie appuyer puis relâcher) et le déposant sur le Fileʼs Owner pour faire apparaître la méthode spin:.


Point technique : les outlets et les propriétés

Les IBOutlet et IBAction déclarés dans notre code peuvent être manipulés par InterfaceBuilder.
Indiquer IBOutlet sur la propriété dʼun UILabel dans GameViewController.h permet de lʼassocier à un
élément dʼinterface de GameViewController.h dans InterfaceBuilder. Ainsi il pourra être modifié (taille,
couleur...) et positionné dans InterfaceBuilder à lʼinitialisation de la vue et modifié dans le code par la suite.
Indiquer IBAction lors de la déclaration dʼune méthode dans GameViewController.h permet dʼindiquer quʼelle
pourra être déclenchée à lʼappui dʼun bouton de GameViewController.xib.
Redéfinir une variable avec @property permet de la manipuler à lʼextérieure de la classe. Cette annotation
déclare lʼaccès possible : nonatomic, par exemple, indique non thread safe. Elle fonctionne de paire avec
@synthesize qui génère getters et/ou setters associés à lʼexécution. Lʼaccès aux variables qui se ferrait
normalement [variable setProperty:9] peut désormais sʼeffectuer variable.property = 9.

Comme cʼest couramment le cas, lʼUIPickerView délègue au ViewController sa gestion et son alimentation en données. Après lʼavoir sélectionné, nous devons tirer les anneaux delegate et dataSource de lʼonglet 2 de lʼinspecteur sur le Fileʼs Owner (GameViewController, le Fileʼs Owner, doit déclarer, comme cʼest le cas ici, les protocoles liés afin que son implémentation des méthodes déléguées soient appelées automatiquement).

Programmation du jeu

Les éléments dʼinterface sont connectés au code : attaquons nous à la méthode spin:, la méthode appelée lors de lʼappui sur “Insérer une pièce” dans GameViewController.h :

-(void)showButton {
   button.hidden = NO;
}
-(void)win:(NSNumber *)points {
   switch ([points intValue]) {
      case 3: winLabel.text = @"une peluche !";break;
      case 4: winLabel.text = @"une montre !"; break;
      case 5: winLabel.text = @"une voiture !";break;
      default:winLabel.text = @"perdu";
   }
   [self performSelector:@selector(showButton) withObject:nil afterDelay:.5];
}
-(IBAction)spin:(id)sender {
   int values[nbFruits] = {0}; // Initialisation du tableau
   for (int i = 0; i < nbPickers; i++)
   {   !
      int value = random() % nbFruits;
      values[value]++;
      [picker selectRow:value inComponent:i animated:YES]; // Déplacement du disque
      [picker reloadComponent:i];
   }
   int bestValue = -1;
   for (int i = 0; i < nbFruits; i++) {
      if (values[i] > bestValue)
         bestValue = values[i];
   }
   button.hidden = YES;
   NSNumber *number = [[NSNumber alloc] initWithInt:bestValue];
   [self performSelector:@selector(win:) withObject:number afterDelay:.5];
   [number release]; // Le selector prend un objet : attention à bien le libérer après
}
-(void)viewDidLoad {
UIImage *grape = [UIImage imageNamed:@"raisin.png"];
   UIImage *pineapple = [UIImage imageNamed:@"ananas.png"];
   for (int i = 1; i <= nbPickers; i++)
   {
      // Afin d’économiser de la place et faciliter la lecture, seulement 2 des 4
      // UIImageView sont initialisées ici
      UIImageView *grapeView = [[UIImageView alloc] initWithImage:grape];
      UIImageView *pinView = [[UIImageView alloc] initWithImage:pineapple];
         !
      NSArray *imageViewArray = [[NSArray alloc] initWithObjects: grapeView, pinView, nil];
   !
      NSString *fieldName = [[NSString alloc] initWithFormat:@"column%d", i];
      [self setValue:imageViewArray forKey:fieldName];
      [fieldName release];
      [imageViewArray release];
   !
      [grapeView release];
      [pinView release];
   }
   srandom(time(NULL));!
}

La méthode spin: va itérer sur chaque colonne du UIPickerView et déterminer un fruit parmi les 4 présents. Via la méthode random() on détermine un entier de 0 à 3, on stocke son nombre dʼoccurrences dans un tableau et lʼon met la colonne du UIPickerView à jour via lʼappel à reloadComponent: en précisant que le changement nʼest pas abrupte, mais animé. Ensuite, on détermine si une valeur est sortie 3 fois ou plus et on affiche le prix remporté par le joueur.

La méthode viewDidLoad est héritée de UIViewController. Elle est exécutée dès que le xib de lʼinterface est chargé. Elle charge les images que nous avons préalablement copiées dans le répertoire Ressources, les ajoute dans une UIImageView dont elle fait un tableau. Elle initialise finalement les tableaux column1, column2... avec.


Point technique : les selectors

Nous avons recours à de lʼintrospection via la méthode [self performSelector:@selector(showButton)
withObject:nil afterDelay:.5] afin de temporiser lʼappel à une méthode. Ainsi, le résultat ne sʼaffiche
quʼaprès une demie seconde, laissant aux roues du UIPickerView le temps de tourner. Attention à la syntaxe,
si la méthode à des arguments le caractère “ : ” lui est ajouté et lʼargument précisé via withObject:.
Lʼappel à [self setValue:imageViewArray forKey:fieldName] dans la méthode viewDidLoad permet,
également via de lʼintrospection, de positionner chacun des 5 NSArray (column1...) à la valeur passée en
paramètre, un tableau dʼUIImageView. Une de celles-ci est sélectionnée lors de lʼappel [picker selectRow:].

Implémenter le delegate et la dataSource du UIPickerView

Ajoutons à GameViewController.m lʼimplémentation des protocoles de UIPickerView :

#pragma mark Picker Data Source Methods
// #pragma facilite la navigation dans xCode
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
{
   return nbPickers;
}
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
{
   return nbFruits;
}
#pragma mark Picker Delegate Methods
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row
      forComponent:(NSInteger)component reusingView:(UIView *)view
{
   if (component == 0)
      return [self.column1 objectAtIndex:row];
   else if (component == 1)
      return [self.column2 objectAtIndex:row];
   else if (component == 2)
      return [self.column3 objectAtIndex:row];
   else if (component == 3)
      return [self.column4 objectAtIndex:row];
   return [self.column5 objectAtIndex:row];
   // Chaque colonne du UIPickerView à son propre tableau d’UIImageView
}

Ces deux protocoles UIPickerViewDelegate et UIPickerViewDataSource définissent plusieurs méthodes. La dataSource détermine les données gérées par chaque disque du PickerView. Nous fixons le nombre de composant à nbPickers et le nombre de lignes à nbFruits. Dans notre exemple, tous les éléments affichent la même chose, mais il arrive couramment que la dataSource fasse appel au modèle pour alimenter ses colonnes. Lʼimplémentation du delegate indique quelle valeur choisir pour chaque ligne. Ces valeurs sont stockées dans les tableaux column1, column2...


Point technique : les différents objets du SDK

Nous avons utilisés de nombreuses chaines de caractères - NSString - au fil de nos exemples. Celles-ci se
déclarent via lʼutilisation de @"texte" et sont libérées automatiquement à l''exécution.
Il en va différemment des autres objets, NSNumber, NSArray, UIImageView... Contrairement au Mac, lʼiPhone ne
dispose pas de garbage collection. La gestion de la mémoire y est donc cruciale.
Par convention, toutes les méthodes laissent la charge de la gestion mémoire à lʼappelant. Nous verrons
dans la partie suivante comment cela se traduit sur le code.
Attention, NSInteger et NSUInteger (non signé) ne sont pas des objets. Ils permettent simplement de laisser
le choix du 32 bits - 64 bits au compilateur. Lʼinstanciation dʼun objet sera donc nécessaire pour utiliser leur
valeur dans une méthode nécessitant un objet (lʼajout à un NSMutableArray, par exemple).

Sauvons, compilons, la première partie du jeu fonctionne. Les roues tournent et indiquent le prix gagné. Dans la partie suivante nous verrons comment, toujours à lʼaide de la délégation et lʼutilisation de protocoles, historiser les parties gagnantes dans une UITableViewController.

Boîte à outils
Dernière modification de cette page le 11 mars 2010 à 13:33