Categories
Non classé

PortraiMatic iPad (2): Reprendre le code de la version Mac ?

C’est décidé, je vais convertir PortraiMatic du Mac à l’iPad. Mais par où commencer ?

Faut-il reprendre le code de la version Mac ?

Il s’agit d’une question économique, à savoir: sachant que les bibliothèques de développement Mac et iOS possèdent de nombreuses similitudes, peut-on partager les fichiers sources entre les deux plateformes ? Après tout, le langage de programmation est le même, et Cocoa Touch est une adaptation de Cocoa Mac. Et si la réponse est positive, est-ce que la lourdeur structurelle (mise en place sous Xcode) n’anéantis pas le bénéfice de cette réutilisation?

Le gros du code concerne l’IHM

L’Interface Homme-Machine représente une grosse proportion dans PortraiMatic. Or, il s’agit probablement d’une des plus grosses différences entre Mac OS et iOS. D’un côté, nous utilisons App Kit, de l’autre UIKit. Il ne s’agit pas d’une simple adaptation: les ingés d’Apple l’ont repensé pour l’utilisation avec un écran tactile et l’ont grandement modernisé.

PM_ReframeView

Par exemple, je sais d’avance que je vais devoir reprogrammer entièrement la vue qui sert à recadrer les portraits. Or, il s’agit de la classe la plus complexe de PortraiMatic Mac.

Pas de bindings

Les Bindings sont une des technologies les plus utiles, mais également les plus complexes qui existe sur Cocoa Mac. Cette technologie synchronise l’état des vues (ce qui apparaît à l’écran) et l’état interne (couche ‘métier’). PortraiMatic en fait grand usage, entre autres parce qu’elle réduit les dépendances entre classes, et donc la complexité de l’architecture. Pour des raisons de performances, je pense, cette technologie n’a pas (encore) trouvé son chemin dans Cocoa Touch. Il existe des alternatives, mais cela signifie à nouveau que de grand pans du code ne peuvent pas être réutilisés.

Pas de Core Image

Core Image est ma technologie préférée dans Mac OS X. Elle permet d’appliquer des effets sur les images voire de générer des images. C’est une technologie à la fois performante, facile à utiliser et extensible. Malheureusement, elle n’a pas encore été portée sous iOS, même si je ne doute pas que cela sera fait prochainement. En conséquence, j’ai décidé, dans un premier temps, de ne pas inclure de fonctions de corrections de couleurs dans la version iPad. Non, pas que ceci me poserait de problème particulier (la programmation graphique est ma spécialité), mais je veux publier l’application rapidement.

Gestion des fichiers

La gestion des fichiers dans PortraiMatic est à la fois un motif de satisfaction — parce qu’elle est maintenant bien au point — et une inquiétude à chaque nouvelle version pour s’assurer de la compatibilité avec les versions précédentes. Elle présente quelques détails malins. Tout d’abord, la “galerie” est un bundle, c’est à dire un dossier qui apparaît comme un fichier sous Finder. On peut l’ouvrir sous Finder, par un clic droit > Afficher le contenu du paquet, et constater que chaque portrait possède son dossier:

PM_BundleGalerie

Chaque dossier comporte l’image utilisée par le portrait et un fichier Portrait.plist qui décrit les paramètres du portrait (recadrage, correction des couleurs). Cette organisation s’est révélée pratique en cas de problème (on peut mettre un dossier à la corbeille), plus simple à déboguer (on voit immédiatement si les données ont été enregistrées). Elle a aussi permis de mettre en œuvre facilement la fusion des galeries de deux utilisateurs. Cependant, je vais passer à Core Data pour enregistrer les données.

Core Data permet de décrire le modèle (la partie “métier”) de l’application de façon graphique et de stocker les informations dans une base de données SQLite de façon transparente. J’en attends des simplifications de l’architecture et une baisse de volume du code source. Par ailleurs, elle devrait régler mes soucis de migration d’une version à l’autre, en m’obligeant à décrire précisément les évolutions de chaque version.

À l’heure du choix

Finalement, je constate que peu de code peut être repris; si nécessaire, je dispose toujours de la possibilité de copier-coller du code. Je n’ai franchement pas envie de m’engager dans des problèmes d’inclusions de fichier d’un projet Xcode à l’autre. Il me semble donc plus opportun de ne pas partager de fichiers entre les versions Mac et iPad, et de créer un projet Xcode tout neuf.

Categories
Non classé

Mac App Store: vérifier le reçu

Dans l‘article précédent, je vous expliquai comment maintenir à la fois une version de votre application pour le Mac App Store et pour votre site web. Dans le présent article, nous verrons comment se prémunir du piratage de l’application en validant le reçu fourni par Apple.

Le reçu

Un répertoire _MASReceipt est incorporé au bundle de l’application téléchargée sur le Mac App Store:

.../SampleApp.app/Contents/_MASReceipt/receipt

Ce reçu constitue la pierre angulaire de la protection anti-pirate. Il s’agit de vérifier qu’il a bien été émis par Apple et que son contenu correspond bien à notre application. Malheureusement, Apple ne fournit pas d’API pour effectuer ces vérifications. Devant la difficulté de concevoir un service invulnérable, Apple a baissé les bras et s’est déchargée du travail sur nos épaules de développeurs. C’est donc à chaque application de faire les vérifications, qui n’ont rien de trivial!

Code d’exemple de Roddi

Apple a rédigé un document (réservé aux développeurs inscrits) qui décrit le contenu d’un reçu et les étapes de la validation. Comme vous le verrez, les indications pour l’implémentation concrète sont laconiques. Le but avoué est d’éviter que tous les développeurs utilisent le même code de validation. Le problème, c’est que peu de développeurs possèdent les connaissances nécessaires pour implémenter ces vérifications. Face à cette complexité, des développeurs ont décidé de créer une solution open-source: Roddi-ValidateStoreReceipt. On trouve aussi cet exemple d’Alan Quatermain, qui fut précurseur mais est moins abouti. Attention toutefois, il ne faut en aucun cas réutiliser ce code tel quel: il serait trop facile à repérer et modifier.

Les étapes de la validation

Comparons un peu la solution de Roddi et les préconisations d’Apple.

Vérifier la signature du reçu

Apple donne quelques indications pour vérifier la signature du reçu en utilisant OpenSSL, et indique qu’il faut utiliser le certificat Apple Root CA, mais pas comment l’obtenir. Le code de Roddi va chercher le certificat dans le Trousseau. Celui de Quatermain le récupère sur le site d’Apple, ce qui présente le défaut d’exiger un accès à Internet au lancement de l’appli.

Extraire les informations de la “Payload”

La partie “Payload” du reçu comprend les informations que nous allons vérifier par la suite: Bundle Identifier, version de l’appli, une valeur opaque (=aléatoire) et un hâchage. Apple préconise l’utilisation d’un obscur outil en ligne de commande, asn1c, pour décomposer la Payload. J’ai installé macports, installé asn1c et généré grâce à lui un fichier C permettant la décomposition.

Voici mon constat:

  • ne vous ennuyez pas à installer asn1c. Le code C qu’il a génèré est intégré au projet de Quatermain.
  • le code produit est dégueulasse. À vous les warnings de GCC dans tous les sens. Roddi a adopté une autre solution: la Payload est décomposée grâce à OpenSSL, ce qui dispense d’embarquer le code d’asn1c dans votre projet.

Vérifier le Bundle Identifier et la version

Comparez le Bundle Identifier de votre appli (pour moi, ”com.ceroce.PortraiMatic”) à celui du reçu. Ceci garantit que le reçu correspond bien à votre appli, et qu’on n’a pas remplacé le reçu de votre appli par celui d’une autre appli… (Hein, Angry Birds!) Faites de même pour la version: vous ne voudriez pas que l’utilisateur utilise le reçu de la version 1.5.3 pour valider la version 2.1.0… Il peut être tentant de récupérer ces deux informations depuis Info.plist: mauvaise idée. Ce fichier peut être modifié sans impact sur le reçu. Stockez le Bundle Identifier et la version ”en dur” dans votre code. De même, évitez — en les obscurcissant — que ces deux chaînes apparaissent quand on ouvre l’application avec un éditeur hexa. Notez que le code de Roddi compare les chaînes en dur avec celles d’Info.plist pour éviter les étourderies.

Vérifier le GUID

Le GUID est en fait l’adresse mac de l’ordinateur autorisé. Pour une fois, Apple fournit du code pour la récupérer (adopté tel quel chez Roddi). Il faut concaténer le GUID + la valeur opaque + Bundle Identifier et appliquer au tout un hâchage SHA-1. Ce hâchage doit correspondre au hâchage stocké dans la payload. !!! En conclusion Nous l’avons vu, la validation n’est pas facile. Heureusement, d’autres ont déjà essuyé les plâtres! Peut-être qu’Apple nous proposera une solution plus simple dans un avenir proche.

Categories
Non classé

Traduction d’une appli Cocoa

Lorsque l’on propose son application dans plusieurs langues, comme c’est mon cas pour PortraiMatic, la traduction (terme que je préfère au vilain anglicisme “localisation”) est plus complexe que ce qu’Apple laisse entendre. En gros, trois aspects doivent être gérés:

  • Ne pas utiliser de chaînes de caractères ”en dur”.
  • Il faut les écrire dans un fichier .strings et les charger avec une fonction ou méthode adéquate (NSLocalizedString() ou -NSBundle localizedStringForKey:value:table).
  • Adapter la saisie et la présentation des nombres, monnaies et dates (NSNumberFormatters et compagnie)
  • Traduire les fenêtres, vues et boites de dialogue, habituellement contenus dans des .xib.

Comme nous allons le voir, ce dernier aspect est délicat.

Solution 1: Traduire les XIB à la mano

Apple propose un concept simple: le bundle de l’application contient un dossier par langue (french.lproj, english.lproj, deutsch.lproj, etc.) qui contient les éléments d’interface utilisateur spécifiques à cette langue. Pour traduire un XIB anglais en français, il suffit donc de copier le .xib de english.lproj dans french.lproj et de le traduire. Cette manière fonctionne bien… la première fois.

Imaginons que vous vouliez ajouter une case à cocher dans votre logiciel:

  • vous ajoutez la case dans le xib anglais
  • vous créez ses connexions (outlets, actions et bindings)
  • et vous refaites le même boulot pour le xib français !

En dehors des risques d’erreur sur les connexions, qui ne peuvent être détectées qu’à l’exécution, c’est un travail rébarbatif, à tel point qu’il vaut mieux repartir des xib anglais et tout retraduire. C’est ce que je faisais jusqu’à récemment.

Un autre problème est que si vous voulez faire traduire le logiciel, vous ne pouvez pas demander au traducteur d’utiliser Interface Builder pour modifier le xib lui-même. Non seulement, c’est compliqué, mais il risque de casser quelque chose. Vous devez lui fournir un simple fichier texte, ce qui nous amène à la…

Solution 2: Extraire les chaînes

C’est la solution que j’utilise actuellement pour PortraiMatic. Je me suis inspiré de la technique exposée par Philippe Casgrain:

1) un outil en ligne de commande, ibtool, extrait les chaînes de caractères du xib anglais et les écrit dans un fichier .strings.

ibtool --export-strings-file English/MainMenu.strings ../English.lproj/MainMenu.xib

Voici un extrait du fichier .strings produit:

/* Class = "NSMenuItem"; title = "About PortraiMatic"; ObjectID = "58"; */ "58.title" = "About PortraiMatic"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "File";

2) le traducteur traduit le fichier .strings

/* Class = "NSMenuItem"; title = "About PortraiMatic"; ObjectID = "58"; */ "58.title" = "À propos de PortraiMatic"; /* Class = "NSMenu"; title = "File"; ObjectID = "81"; */ "81.title" = "Fichier";

3) ibtool recompose le xib français en remplaçant les chaînes traduites:

ibtool --import-strings-file French/MainMenu.strings --write ../French.lproj/MainMenu.xib ../English.lproj/MainMenu.xib

Philippe Casgrain propose d’extraire les chaînes à chaque build (en ajoutant une ”Script Build Phase” à la ”Target” sous XCode). C’est une bonne idée car votre logiciel de gestion de version va vous alerter d’une modification des fichiers .strings, qu’il faudra renvoyer au traducteur.

Par contre, il suggère aussi de recomposer les XIB traduits à chaque build: je ne le fais pas, car le build s’en trouve allongé, alors que ça n’agit que quand le fichier .strings vient d’être traduit.

Cette technique a l’avantage d’être simple à mettre en œuvre, mais pose encore deux problèmes:

  • les objets de l’IHM ne sont pas redimensionnés; or on sait que l’Anglais est souvent plus concis que le Français ou l’Allemand. Il faut donc prévoir de la marge dans les dimensions lors de l’élaboration des XIB anglais. Ce n’est pas toujours esthétique: il reste deux ou trois boutons que je redimensionne à chaque livraison.
  • les XIB sont volumineux, et l’application prend de l’embonpoint à chaque langue ajoutée. C’est particulièrement pénalisant pour une appli iPhone.

Solution 3: Traduire au chargement du NIB

Cette solution a été exposée par Axel Péju, l’auteur de Squirrel (et accessoirement gagnant d’un Apple Design Awards pour la version Mac), lors de sa présentation au dernier Cocoa Heads parisien. L’idée est simple: il suffit de mettre toutes les chaînes de caractères qui apparaissent à l’écran dans des fichiers .strings. Quand on charge un NIB, on remplace alors le texte de chaque objet par le code.Pour redimensionner les objets, on appelle leur méthode -sizeToFit pour que leur taille s’adapte au texte affiché.

Cette technique résout tous les problèmes exposés précedemment: il n’y a plus qu’un seul XIB, et les chaînes peuvent être fournies directement aux traducteurs. Elle possède aussi un avantage: le traducteur peut essayer lui-même l’application traduite: il n’a qu’à glisser les fichiers .strings traduits dans le dossier .lproj qui correspond à sa langue.

Ceci dit, le travail est important, puisqu’un objet contrôleur doit posséder des outlets vers tous les objets qui affichent du texte, même si ce sont de simple libellés, et faire les appels correspondant.

Solution 4: Traduire (automatiquement) au chargement du NIB

Voici une solution à laquelle j’avais pensé; Guillaume Cerquant m’a dit qu’il l’utilisait. L’idée est que chaque élément d’IHM du XIB à traduire a son titre qui commence par un astérisque (par exemple). Lors du chargement du XIB, on parcourt la hiérarchie des vues en entier: si le titre de la vue commence par “*”, on va chercher le titre dans un fichier .strings et on remplace le titre. Il me semble que cette technique est un bon compromis: à la fois assez légère à mettre en œuvre, tout en conservant les qualités de la solution 3.