Des actualités personnelles sous un style impersonnel, et inversement.
Follow @Thomas_Jannaud
Cocoa est le framework phare d'Apple pour la programmation. Il n'est pas aussi simple à prendre en main que le C++ ou le Java mais on peut faire de très beaux programmes avec. Les difficultés viennent de la gestion de la mémoire, et des tas de petites bidouilles qu'il faut faire constamment pour avoir un programme qui respecte le modèle 3-tiers que nous impose Apple. Et aussi du peu de ressources sur internet, en comparaison avec Java ou C++. Il ne m'aura vraiment pas été rare de passer 3 ou 4 heures à chercher comment faire quelque chose qui ne prend en fait qu'une ligne (mais laquelle !), comme la détection de l'événement mouseMove.
Les avantages de Cocoa sont : l'accès à une interface graphique qui permet de faire de très jolis programmes, dans la plus pure tradition Apple. Et aussi de nombreux frameworks qui rendent la vie très simple au programmeur ; citons entre autres la sérialisation (pour ouvrir/sauvegarder simplement des choses au sein des logiciels), l'opération Annuler/Rétablir, ...
Vous trouverez dans cet ouvrage de quoi apprendre Cocoa et Objective-C par l'exemple... sans doute le meilleur moyen pour appréhender un langage.
Vous aurez dans ce livre à construire des petites applications simples, pas à pas, que vous allez faire grossir et ce faisant vous
découvrirez d'innombrables techniques et principes pour coder proprement en Cocoa / Obj-C.
D'autre part, par expérience, vous verrez qu'un site internet ne remplacera jamais un livre papier.
Dans XCode, si vous appuyez sur Alt et que vous double cliquez sur un mot, alors la fenêtre avec la documentation s'ouvre.
Cocoa est un drôle de mix entre le C et le C++, et ne ressemble finalement à aucun d'eux, à part dans leurs grandes idées. Il est néanmoins possible d'écrire du C (tout le temps) et du C++ si vous donnez à votre fichier source l'extension .mm. Si vous déclarez une fonction C ou C++ cela doit se faire à l'extérieur d'une balise @implementation
Cocoa semble aussi plus récent que le C / C++ au sens où l'on peut créer des classes ou appeler des fonctions à partir de chaînes de caractères, ou savoir si un objet est de la classe A ou de la classe B ou si c'est un descendant de C, ... On peut plus ou moins faire ça en C / C++ mais de manière très "sale", et je pense qu'avec la puissance des ordinateurs d'aujourd'hui avoir un code qui tourne 5% moins vite ça ne change rien, par contre avoir du code plus lisible, plus propre, et qui permet de coder 2 fois plus vite c'est plus important.
this
mais self
.Les objets en Cocoa dérivent de la classe NSObject. si l'objet toto a une fonction f, il faut faire [toto f]
pour appeler cette fonction, au lieu du traditionnel toto.f()
. Pour les fonctions prenant des arguments, c'est
[toto f:x:y:autreObjet]
. Il y a aussi des fonctions qui exigent que les arguments soient nommés.
Ainsi si on a une fonction définie comme
-(void) f:(int) hauteur largeur:(int) y pere:(A*)p {...}
...
[toto f:x largeur:y pere:autreObjet];
NSPoint pt = NSMakePoint(2.2, 0.5);
double a = pt.x + pt.y;
ou aussi (puisque les structures sont des éléments assez simples :
NSPoint pt;
pt.x = 2.2;
pt.y = 0.5;
A ma connaissance, on ne se sert surtout que de NSPoint, NSSize (construire avec NSMakeSize(largeur, hauteur), attributs width et height), et NSRect (construire avec NSMakeRect(x, y, largeur, hauteur), attributs NSPoint origin, et NSSize size).
L'opérateur '.' s'applique enfin aussi aux membres des objets, et il ne s'applique à rien d'autre. Dans l'exemple ci-dessous,
C* c = ...; c.zPublique = 3;
.
En Cocoa, tous les objets sont des pointeurs.
Déclaration typique d'une classe. Ici C est une classe qui dérive de B. B est soit NSObject soit une classe qui dérive de qqchose qui dérive
de qqchose... qui dérive de NSObject.
Code de C.h
#import <Cocoa/Cocoa.h>
@class A, B;
@interface C : B {
int xPrive, ySemiPublique, zPublique
A* monA;
}
@property (readonly) A* monA;
@property (readonly) int ySemiPublique;
@property int zPublique;
-(id) init:(A*) aa;
-(void) fonctionPublique1;
-(NSString*) fonctionPublique2;
@end
Code de C.m
#import "C.h"
#import "A.h"
#import "B.h"
@interface C (PrivateMethods)
-(double) fonctionPrivee1;
-(A*) fonctionPrivee2:(int) a:(B*) b;
+(int) fonctionStatiquePublique;
@end
@implementation C
@synthesize monA, ySemiPublique, zPublique;
-(double) fonctionPrivee1 {
...
}
-(A*) fonctionPrivee2:(int) a:(B*) b {
return [[A alloc] init:[b uneFonction]];
}
-(void) fonctionPublique1{
double a = [self fonctionPrivee1];
...
}
-(NSString*) fonctionPublique2 {
...
}
+(int) fonctionStatiquePublique {
...
}
-(id) init:(A*) aa {
if (self = [super init]) {
monA = aa;
}
return self;
}
id
property
et synthesize
readonly
n'est pas obligatoire,
et on peut aussi mettre des choses comme nonatomic
, ...Note historique : pendant longtemps la gestion de la mémoire était une plaie. Il fallait utiliser des retain
ou release
. En interne chaque objet possède un compteur, indiquant le nombre d'objets qui le référencent. Si ce compteur tombe à 0, l'objet est désalloué. C'était cependant très compliqué à gérer. Depuis quelques années il y a heureusement l'ARC (automatic reference counting) qui a énormément simplifié la vie ! Le compilateur détermine automatiquement quand les objets doivent être garbage collectés.
Pour créer un objet, on le fait toujours par le biais de [[... alloc] init]
.
La fonction init peut prendre des arguments. alloc
met automatiquement le compteur à 1 pour l'objet.
@"toto"
(ne pas oublier le signe '@' sinon tout
plante), soit en créer avec [NSString stringWithFormat:@"bonjour %@, ça va ?", nom];
; regarder la doc de la classe
NSString pour voir toutes les possibilités.NSNumber * n = [NSNumber numberWithInt:6], m = [NSNumber numberWithDouble:0.14 + [n doubleValue]];
NSLog(@"x vaut %d", x);
Un NSTimer est un compte à rebourd. Il va sonner toutes les x secondes. On lui passe un sélecteur en paramètre (un pointeur sur une fonction) et à chaque "bip" du timer, le système appelle notre fonction. On peut décider de ne faire biper le timer qu'une fois puis il s'éteint, ou alors une infinité de fois.
NSTimer* chrono = [NSTimer timerWithTimeInterval:nbSecondes target:self selector:@selector(bip:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:chrono forMode:NSDefaultRunLoopMode];
[chrono fire];
-(void) die {
[chrono invalidate];
}
-(void) bip:(NSTimer*)theTimer {
...
}
On peut énumérer rapidement les conteneurs avec des commandes telles que :
for(MonObjet* i in tableau) ...
Choses à savoir.
Exemple de classe Controleur :
#import <Cocoa/Cocoa.h>
@interface Controleur : NSObject {
IBOutlet NSTextField* zoneTexte;
IBOutlet NSButton* boutonOk;
}
-(IBAction) clic:(id) sender;
@end
Quand vous faites une action sur l'interface (clic sur un bouton, bouger un curseur, ...), vous voulez qu'un message soit transmis au
Controleur. En quelque sorte le Controleur est le delegate de l'interface, c'est à dire que quand un message est transmis, c'est lui qui reçoit.
Mais la notion de delegate est beaucoup plus générale me semble t'il. Ainsi le Application Delegate est le délégué de l'application. Quand
l'application vient de finir de loader la fenêtre et qu'elle est prête, elle envoie le message
- (void) applicationWillFinishLaunching: (NSNotification *) aNotification;
à son delegate. Si aucun delegate n'a été
spécifié, personne ne récupère ce message et il n'y a pas mort d'homme.
On spécifie qui est un delegate de qui dans Interface Builder, de la même manière qu'on définit les actions et les outlets.
Le mieux est de regarder en même temps sur un exemple (cf codes sources) pour suivre.
D'après ce que j'ai compris, l'architecture "3-tiers" signifie : une classe qui gère les données, une autre qui gère l'interface graphique, et une autre qui gère le passage entre les deux, les événements (souris, clic sur un bouton, ...). Ça c'est un petit peu la définition Wikipédia mais ça ne dit pas comment faire en pratique : qui est créée en premier ? la classe données ? la classe graphique ? l'autre ? Qu'est ce que c'est que ce "delegate" dont tout le monde parle ? Comment affecter une référence de Controleur à MaView si Controleur est créé après MaView ? ou l'inverse ?
Voilà comment je vois les choses, et comment je procède à chaque fois. Je ne pense pas que ça soit la meilleure solution, mais ça ne marche pas trop mal. J'utilise 4 classes : Application Delegate, MaView, Controleur et Cœur.
Comme je vous le disais, ça a l'air propre et structuré, mais en pratique il y a des "conflits" avec ce modèle. Par exemple, MaView est la classe qui s'occupe de dessiner les serpents. Donc elle demande à Cœur de lui fournir la liste des cases à colorier (le corps des serpents). Mais alors si elle fait ça Controleur ne sert plus à rien alors qu'il est sensé être là pour faire le dialogue entre les deux. On pourrait bien sûr faire passer ce message via le Controleur mais c'est du code en plus pour rien, non ?
D'autre part, quand on clique quelque part sur la fenêtre et qu'on cherche à connaître les coordonnées de la souris à l'endroit du clic, est-ce un événement à gérer par le Controleur ou par MaView ? Bon, vous voyez qu'il y a pleins de petites questions, mais ça n'empêche pas de faire des logiciels qui tournent quand même.
Vous pouvez télécharger le projet minimal que je propose. Regardez bien les outlets (rappel : fenêtre des propriétés d'un objet) pour voir où les liens ont été mis.
Si vous pensez que le projet est un petit peu alambiqué pour un simple projet, vous avez raison. Mais il y a aussi une raison :
Quid du controleur ou de MaView va être créé en premier quand le logiciel va être lancé ? Ils sont tous les deux instanciés automatiquement
dès l'exécution du programme et on ne peut pas vraiment savoir ce qui va se passer. Il faut donc attendre que tout soit bien fait et créé,
puis, quand l'Application envoit le message
-(void) applicationWillFinishLaunching: (NSNotification *) aNotification;
à son delegate, celui-ci sait que tout a été
créé et il peut donc demander aux gens de se référencer les uns les autres.
Il y a aussi un IBOutlet du AppDelegate vers le Controleur, et un IBOutlet du Controleur vers MaView pour aider !
Pour info, j'ai eu un bug difficile à détecter, qui disait qu'un pointeur était nul alors que pourtant MaView et Controleur semblaient tous deux en état de marche. C'est effectivement que l'un était créé avant l'autre.
Dans une classe MaView héritant de NSView, mettez les fonctions suivantes (dans l'interface et dans l'implémentation) :
-(void) mouseDown:(NSEvent*) theEvent {
NSPoint pt = [theEvent locationInWindow];
pt.x -= [self frame].origin.x; pt.y -= [self frame].origin.y;
NSLog(@"mouseDown à x=%f y=%f", pt.x, pt.y);
}
-(void) mouseUp:(NSEvent*) theEvent {
}
-(void) mouseMoved:(NSEvent*) theEvent {
}
-(void) mouseDragged:(NSEvent*) theEvent {
}
-(void) keyDown:(NSEvent *)theEvent {
// bas:125, haut:126, gauche:123, droite:124
NSLog(@"la touche numéro %d est appuyée", [theEvent keyCode]);
NSString *characters = [theEvent charactersIgnoringModifiers];
for (int i = 0; i < [characters length]; i++)
NSLog(@"le caractère %c est appuyé",[characters characterAtIndex:i]);
}
-(BOOL) acceptsFirstResponder {return YES;}
-(BOOL) becomeFirstResponder {return YES;}
-(BOOL) resignFirstResponder {return YES;}
Pour pouvoir avoir accès à mouseMoved, il faut de plus ajouter [appFenetre setAcceptsMouseMovedEvents:YES];
dans
la fonction applicationWillFinishLaunching
de l'AppDelegate. appFenetre est ici un IBOutlet sur la NSWindow* de
l'application (c'est à dire la fenêtre contenant la view).
L'Undo Manager est l'objet Cocoa qui permet de gérer tout seul ce qui se passe quand on appuie sur Annuler ou Répéter (Redo). Le seul problème est qu'il n'y a pas d'Undo Manager pour toute l'application, il y en a un par fenêtre seulement (c'est ce que j'ai l'impression). Il faut donc que dans toutes les classes qui aient besoin d'appeler l'Undo Manager, il y ait une référence vers la fenêtre NSWindow de l'application.
Principe de fonctionnement : quand la classe va faire une action que l'on considère comme digne d'intérêt pour le Undo, elle spécifie à l'Undo Manager quelles fonctions appeler pour faire Undo et Redo sur cette action. L'Undo Manager place simplement ces appels de fonction sur des piles et les appelera au moment voulu.
Exemple : vous gérez une liste de personnes, avec une petite zone de texte et un bouton OK pour ajouter une personne à votre liste
avec son nom. Au lieu de faire [liste addObject:[[Personne alloc] init:nom]];
, vous allez faire
[self ajouter:[[Personne alloc] init:nom]];
.
-(void) ajouter:(id) qqchose {
[[fenetre undoManager] registerUndoWithTarget:self selector:@selector(retirer:) object:qqchose];
[liste addObject:qqchose];
}
-(void) retirer:(id) qqchose {
[[fenetre undoManager] registerUndoWithTarget:self selector:@selector(ajouter:) object:qqchose];
[liste removeLastObject];
}
Ici, fenetre est une NSWindow*.
La sérialisation est le procédé qui consiste à sauvegarder les objets dans un fichier. Par exemple, vous avez un tableau avec des objets "Personne" (attributs : nom et age) et vous voulez sauvegarder vos données dans un fichier. Comment faire ? Vous pouvez soit ouvrir un fichier, écrire le nombre d'éléments dans le tableau, puis pour chaque élément écrire les attributs, mais c'est un petit peu moche.
Cocoa rend le procédé plus formel et plus simple. Un objet que vous souhaitez sérialisable doit conformer au protocole
<NSCoding>
, c'est à dire qu'il doit implémenter les fonctions suivantes :
-(void) encodeWithCoder: (NSCoder *)coder
et -(id) initWithCoder:(NSCoder *) coder
.
Pour sauvegarder quelque chose, on a juste à dire "sauvegarde ceci". Et récursivement, toutes les "dépendances" de l'objet vont être sauvegardées.
Dans le cas de l'objet personne (int age, NSString* nom), on aurait par exemple :
-(void) encodeWithCoder: (NSCoder *)coder {
[coder encodeObject: [NSNumber numberWithInt:age] forKey:@"age"];
[coder encodeObject: nom forKey:@"nom"];
}
-(id) initWithCoder:(NSCoder *) coder {
if (self = [super init]) {
age = [[coder decodeObjectForKey:@"age"] intValue];
nom = [coder decodeObjectForKey:@"nom"];
}
return self;
}
Et dans Personne.h : class Personne : NSObject<NSCoding> {...
. Si une super classe est déclarée
comme conforme à NSCoding, alors les classes filles le sont automatiquement.
Pour ouvrir une boite de dialogue et sauvegarder un tableau contenant des objets 'Personne' :
NSSavePanel *sp = [NSSavePanel savePanel];
[sp setRequiredFileType:@"txt"];
int runResult = [sp runModalForDirectory:NSHomeDirectory() file:@""];
if (runResult == NSOKButton) {
NSString *fichier = [[sp filename] stringByExpandingTildeInPath];
NSMutableDictionary *rootObject = [NSMutableDictionary dictionary];
[rootObject setValue:tableau forKey:@"tableauPersonnes"];
[NSKeyedArchiver archiveRootObject:rootObject toFile:fichier];
}
Pour ouvrir une boite de dialogue et ouvrir le fichier avec le tableau contenant des objets 'Personne' :
NSOpenPanel* openDlg = [NSOpenPanel openPanel];
NSArray *fileTypes = [NSArray arrayWithObject:@"txt"];
if ([openDlg runModalForDirectory:nil file:nil types:fileTypes] == NSOKButton ) {
NSString* fichier = [openDlg filename];
NSDictionary *rootObject = [NSKeyedUnarchiver unarchiveObjectWithFile:fichier];
tableauPersonnes = [rootObject valueForKey:@"listeFigures"];
}
Rien de plus simple, il vous suffit d'une view, ou plutôt d'une classe qui en dérive. Dans Interface Builder, déposez une view sur la fenêtre, allez dans ses propriétés, et changez la classe NSView en MaView, après bien sûr avoir créer un classe MaView dans votre projet ! Redéfinissez simplement la méthode drawRect dans l'interface (le .h) et dans l'implémentation (le .m).
Pour demander à une NSView de se redessiner, on lui envoie le message setNeedsDisplay:YES
, et elle transmet tout ce
qu'il faut à drawRect
.
Dans MaView.h :
@interface MaView : NSView {
}
-(void) drawRect: (NSRect) bounds;
@end
Dans MaView.m :
#import "MaView.h"
@implementation MaView
- (void)drawRect:(NSRect)rect {
// dessiner un rectangle
[[NSColor blackColor] set];
NSRect cadre = NSMakeRect(0, 0, parametres.width, parametres.height);
[NSBezierPath strokeRect:cadre];
// dessiner une ligne
NSBezierPath* aPath = [NSBezierPath bezierPath];
[aPath moveToPoint:NSMakePoint(100, 0)];
[aPath lineToPoint:NSMakePoint(150, 200)];
[aPath stroke];
// dessiner une chaine de caractères dans une certaine couleur
NSDictionary* atts = [NSDictionary dictionaryWithObjectsAndKeys:[NSColor redColor], NSForegroundColorAttributeName, nil];
[@"toto" drawAtPoint:NSMakePoint(50, 50) withAttributes:atts];
// dessiner un oval
NSRect r = NSMakeRect(10, 10, 40, 70);
aPath = [NSBezierPath bezierPathWithOvalInRect:r];
[aPath stroke];
}
Class aa = [A class]; A* toto = [[aa alloc] init];
[toto isMemberOfClass:[A class]]
[toto isKindOfClass:[A class]]
Pour ce faire, inclure le fichier son dans le bundle (cliquer sur le petit dossier Resources et faire AddFile). Il ne suffit pas d'avoir le fichier son dans le dossier de l'application. Dans le code ci-dessous, le fichier est "supermusique.wav"
NSString* monFichSon = [bundle pathForResource:@"supermusique" ofType:@"wav"];
NSSound* monSon= [[NSSound alloc] initWithContentsOfFile:monFichSon byReference:YES];
[monSon play];
Pour ce faire, inclure l'image dans le bundle (cliquer sur le petit dossier Resources et faire AddFile). Il ne suffit pas d'avoir l'image dans le dossier de l'application. Dans le code ci-dessous, le fichier est "monimage.png"
NSURL *imURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"monimage" ofType:@"png"] isDirectory:NO];
NSImage* monImage = [[NSImage alloc] initWithContentsOfURL:imURL];
[bouton setImage:monImage];
Il existe un objet spécial sur toutes les applications : les préférences. Apple gère le fichier pour nous.
Quand on écrit dans le fichier, il faut faire synchronize
après. Mon expérience des préférences est que ça ne
fonctionne pas toujours bien. Quand on écrit dedans et qu'on ferme l'application et qu'on l'ouvre après, les préférences ont bien été
sauvegardées, mais si on ne ferme pas l'application avant et qu'on cherche dans un autre coin du code à savoir ce qu'il y a dans tel préférence,
il se peut très bien que le changement n'est pas été fait...
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
// lire des préférences
int a = [preferences integerForKey: @"Niveau"];
// écrire dans les préférences
[preferences setInteger:a+1 forKey:@"Niveau"];
[preferences synchronize];
Pour information, il y a aussi un autre truc, ça s'appelle les "bindings". C'est dans les propriétés d'un objet dans Interface Builder. Si par exemple il y a une case à cocher quelque part, l'application va sauvegarder toute seule l'état de cette case (cochée ou non), pour ne pas qu'on ait à le faire à chaque fois. Je n'ai jamais vraiment essayé ça, mais il faut savoir que ça existe.
Petit bout de code pour imprimer une NSView* window
.
NSPrintInfo *printInfo = [NSPrintInfo sharedPrintInfo];
[printInfo setHorizontalPagination: NSFitPagination];
[printInfo setVerticalPagination: NSFitPagination];
[printInfo setVerticallyCentered:NO];
NSLog(@"Printing with parameters %@", [printInfo dictionary]);
NSPrintOperation *op = [NSPrintOperation printOperationWithView:window printInfo:printInfo];
[op setShowPanels:YES];
[op runOperation];
Si vous décidez d'avoir une OpenGL View au lieu d'avoir une NSView, c'est comme vous décidez. Au lieu d'avoir une MaView : NSView, faites MaView : NSOpenGLView. Incluez aussi le framework OpenGL dans le petit dossier Frameworks de votre projet XCode.
#include <OpenGL/gl.h>
-(void) drawRect:(NSRect) bounds {
glClearColor(0, 0, 0, 0);
glClear(GL_COLOR_BUFFER_BIT);
glColor3f(.8, .6, .4);
glBegin(GL_POLYGON);
{
glVertex2f( x, y); // haut à gauche
glVertex2f( x + widt, y); // haut à droite
glVertex2f( x + widt, y - heigh); // bas à droite
glVertex2f( x, y - heigh); // bas à gauche
}
glEnd();
glFlush();
}
Il n'y a que très peu de différences entre Cocoa et iPhone. En fait j'en ai surtout noté pour ce qui est du dessin. Au lieu de faire des choses comme NS.... on fait CG... (pour les structures). Par exemple :
-(void) drawRect: (CGRect) bounds {
CGContextRef myContext = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor (myContext, [[NSColor redColor] CGColor]);
CGContextFillRect (myContext, CGRectMake (x, y, widt, heigh ));
}
Sinon il n'y a pas de Garbage Collector (à l'heure où je le dis) sur iPhone, donc vous devez gérer la mémoire vous même !
Laissez un commentaire !
Pas besoin de vous connecter, commencez à taper votre nom et une case "invité" apparaîtra.