Application mère et modules sur iOS

Contexte

Dans les grosses entreprises, il y a souvent beaucoup d’idées d’applications mobiles et il est également tentant de faire une application mobile à chaque gros coup de pub. Au final, on peut vite se retrouver avec le cas de Orange, par exemple, qui a déployé un grand nombre d’applications sur les markets mobile (à l’heure où j’écris : 12 pour Orange France + 10 pour Orange Group sur Android market) [1]. Cette situation, loin de donner une bonne image de l’entreprise, embrouille l’utilisateur qui ne sait plus laquelle télécharger.

Une solution à ce problème est de mettre en place une seule et unique application mère, déployée sur le market, qui se chargera de gérer la connexion (si besoin) et de télécharger les contenus disponibles en fonction des droits de l’utilisateur. Pour que ce système fonctionne, il faut toutefois trouver un moyen pour charger du code dynamiquement. Or, le principe de sandboxing des applications mobiles rend cette tâche compliquée. Cette mesure est nécessaire pour éviter le chargement de code source nuisible à l’insu de l’utilisateur. Mais malheureusement, c’est ce système que l’on voudrait utiliser (pour la bonne cause bien sûr :) ).

Faisabilité

Chargement dynamique

Avec le framework Cocoa, il existe une notion de Bundle qui peut être chargé au runtime. Il s’agit tout simplement d’un dossier contenant principalement les ressources suivantes : images, fichiers de texte ou de configuration. Mais il est également possible d’y ajouter du code source sous forme d’une librairie dynamique. Sur Mac OS il est possible de créer un projet de type Bundle. C’est parfait, c’est exactement ce qu’il nous faut. Mais…. (et oui il y’a un mais), par défaut il n’est pas possible de créer un projet de ce type pour iOS. Pour contourner ce problème, on peut toutefois utiliser une petite astuce détaillée sur le wiki de Sumgroup.

Chargement d’un Bundle

On sait désormais qu’il est possible de créer un Bundle sur iOS. Il reste à vérifier que l’on peut en charger un au runtime. Après une rapide recherche, on remarque la classe NSBundle qui fournit tout ce dont on a besoin. Le code suivant devrait donc faire l’affaire.

1
2
3
4
5
6
7
8
9
10
11
12
- (id)loadLibrary:(NSString *)path {
NSError *error;
NSBundle *bundle = [NSBundle bundleWithPath:path];

if (![bundle loadAndReturnError:&error]) {
NSLog(@"loading failed : %@", error.description);
return nil;
}

Class mainClass = [bundle classNamed:@"moduleMainClass"];
return [[[mainClass alloc] init] autorelease];
}

Mise en pratique

Bon, ça avance. Il ne nous reste plus qu’à compiler notre projet sous forme de Bundle, ce qui s’est révélé être un peu plus compliqué que prévu lors de mes recherches.

AttentionIl va donc falloir retrousser ses manches et mettre ses mains dans la config d’un projet objective-C.

InformationDans la suite de ce document, je partirai du principe que vous avez déjà un projet de type iOS existant.

InformationEt enfin, avant d’attaquer la suite, je rappelle qu’un projet iOS est différent d’un projet Mac OS, puisque les restrictions sont plus importantes pour les terminaux mobiles. De plus, l’API est légèrement différente.

Création d’une cible

Notre projet possède déjà une cible, utilisée pour compiler et tester l’application de manière habituelle, sur l’émulateur ou un iPhone/iPad. Maintenant, on va en rajouter une autre qui servira à générer le Bundle à envoyer sur un serveur. Bundle qui sera ensuite téléchargé par l’application mère.

Pour cela, direction la configuration du projet pour y ajouter une nouvelle cible.

AttentionAttention à ne pas faire une copie d’une cible existante puisque votre projet iOS ne permet pas la création d’une cible Bundle.

Bien qu’il s’agisse d’un projet iOS, sélectionnons le template “Framework & Library / Bundle” dans la partie Mac OS X. Nous modifierons ensuite la configuration de cette cible pour la rendre compatible iOS.

Entrez comme nom de cible le nom du module à générer en respectant une norme de nommage que vous aurez préalablement définie, bien sûr. :)

Configuration de la cible

En l’état actuel, nous avons une cible de type Bundle mais configurée pour Mac OS. Il va falloir faire en sorte qu’elle soit compatible iOS. Pour cela, rendez-vous dans la vue de configuration de la cible pour modifier la valeur de “Base SKD” à “Latest iOS“. Cela devrait modifier automatiquement une bonne partie de la configuration.

Dans cette même liste recherchez les clés suivantes et modifiez leur valeurs comme suit.

AttentionPensez à faire vos modification dans la colonne correspondant à votre cible, puisqu’on ne veut pas modifier la configuration générale qui est aussi utilisée par vos autres cibles.

  • Architectures
    • Architectures = “Standard (armv6 armv7)”
    • Build Active Architecture Only = “No”
    • Supported Platforms = “iphoneos iphonesimulator”
    • Valid Architectures = “armv6”, “armv7”
  • Deployment
    • Max OS X Deployment target = “Compiler default”

Notre cible est désormais configurée pour générer un Bundle pour iOS. Il ne reste plus qu’à aller dans le troisième onglet pour lui ajouter les classes à ajouter au fichier final.

Cible universelle & Automatisation

“C’est bien beau tout ça, mais pour faire mes tests, je dois régénérer le bundle et le redéployer sur un serveur pour que l’application mère puisse essayer de le charger. Et si je change de plateforme (émulateur ou iPhone/iPad), je dois recommencer.”

Et c’est tout à fait normal. Sur xCode on doit choisir la plateforme de destination lors de la compilation puisque l’architecture du processeur n’est pas la même :

  • armv6 (armv7 pour les plus récents) pour un iPhone ou un iPad ;
  • i386 pour l’émulateur.

Toutefois, il existe un outil — Lipo — distribué avec xCode qui permet de fusionner deux librairies, générées pour des plateformes différentes, en un seul fichier. On va donc pouvoir générer notre bundle pour les deux plateformes puis les fusionner en un seul Bundle compatible iPhone/iPad ET émulateur.

Comme on ne veut pas faire cette opération à la main à chaque fois, on veut trouver un moyen permettant de lancer un script bash qui s’occupera de générer les bundle et les fusionner. Pour cela, on va donc créer une nouvelle cible (et oui encore :)) de type “Other / Aggregate” de la partie iOS cette fois.

Son seul intérêt sera de pouvoir lancer notre script que l’on va rentrer dans la configuration de cette cible.

Le script ci-dessous va effectuer plusieurs actions :

  1. Générer le bundle pour iPhone/iPad ;
  2. Générer le bundle pour émulateur ;
  3. Copier les librairies et leurs ressources associées ;
  4. Générer le bundle universel contenant également les ressources ;
  5. Générer un zip du résultat puisqu’il s’agit du dossier (comme indiqué au début de l’article) ;
  6. L’envoyer sur un serveur FTP utilisé pour les tests avec l’application mère.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# Sets the target folders and the final bundle product.
# Need to be the same as the bundle’s target name
BUNDLE_NAME=test-module
PRODUCTION="Release"

# Install dir will be the final output to the bundle.
# The following line create it in the root folder of the current project.
INSTALL_DIR=${SRCROOT}/${BUNDLE_NAME}.bundle

# Working dir will be deleted after the framework creation.
WRK_DIR=build
DEVICE_DIR=${WRK_DIR}/${PRODUCTION}-iphoneos/${BUNDLE_NAME}.bundle
SIMULATOR_DIR=${WRK_DIR}/${PRODUCTION}-iphonesimulator/${BUNDLE_NAME}.bundle

# Building both architectures.
xcodebuild -configuration "${PRODUCTION}" -target "${BUNDLE_NAME}" -sdk iphoneos
xcodebuild -configuration "${PRODUCTION}" -target "${BUNDLE_NAME}" -sdk iphonesimulator

# Cleaning the oldest.
if [ -d "${INSTALL_DIR}" ]
then
rm -rf "${INSTALL_DIR}"
fi

# Cleaning the oldest.
if [ -f "${BUNDLE_NAME}.zip" ]
then
rm -f "${BUNDLE_NAME}.zip"
fi

# Creates and renews the final product folder.
mkdir -p "${INSTALL_DIR}"

# Copy all contents with the generated bundle except the bundle file
cp -rf "${DEVICE_DIR}/" "${INSTALL_DIR}"
rm -f "${INSTALL_DIR}/${BUNDLE_NAME}"

echo 'Creating universal final bundle...'
# Uses the Lipo Tool to merge both binary files (i386 + armv6/armv7) into one Universal finalproduct.
lipo -create "${DEVICE_DIR}/${BUNDLE_NAME}" "${SIMULATOR_DIR}/${BUNDLE_NAME}" -output "${INSTALL_DIR}/${BUNDLE_NAME}"

echo 'DONE.'
echo 'Archiving...'

# make an archive
# tar -cf "${BUNDLE_NAME}.tar.gz" "${BUNDLE_NAME}.bundle"
zip -r "${BUNDLE_NAME}.zip" "${BUNDLE_NAME}.bundle"

echo 'DONE.'
echo 'Sending to FTP server...'

# send archive to a local ftp server
ftp -A -u "ftp://mon_serveur_distant_de_test/" "${BUNDLE_NAME}.zip"

echo 'DONE.'

# clean working directory
rm -r "${WRK_DIR}"
rm -r "${INSTALL_DIR}"

Contraintes

Convention de nommage

Avec ce système, les modules (ou Bundle) sont tous chargés dans l’espace mémoire de l’application mère. Etant donné qu’il n’y a pas de namespace en Objective-C, il est donc important de respecter certaines règles d’écriture pour éviter les conflits de noms entre les fichiers.

On utilisera donc un préfixe du type “<CodeDeLEntreprise><CodeDuModule><NomDuFichier>” pour chaque classe, protocole, catégorie, nom de fichier.

Structure

Chaque bundle contient les ressources nécessaires (medias, configuration, textes) au fonctionnement du module. Or plusieurs bundles cohabitent dans le même espace mémoire (celui de l’application mère). Les modules doivent donc savoir quel dans quel bundle aller chercher les ressources dont ils ont besoin. Pour cela, on va devoir surcharger les méthodes initWithNibName de nos ViewController de la manière suivante :

1
2
3
4
5
6
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
if (self = [super initWithNibName:nibNameOrNil bundle:[NSBundle bundleForClass:self.class]]) {
// Custom initialization
}
return self;
}

Ainsi, on s’assure que les vues utiliserons le bon bundle et trouverons les ressources.

Conclusion

Finalement, il est possible de mettre en place un système d’application mère sur iOS pouvant télécharger du contenu additionnel et le charger en mémoire au runtime. Cela oblige en revanche à plus de rigueur dans l’écriture du code, mais en soit… Ce n’est pas forcément un mal. ;)

Le seul vrai bémol de cette structure reste du côté de l’entreprise à la pomme puisqu’il n’est théoriquement pas possible qu’ils acceptent ce genre d’application sur l’App Store. Théoriquement… Puisque certaines entreprise auraient déjà[2] un système similaire en ligne. :)


[1] : et surement autant dans chaque grand pays où Orange est implanté
[2] : pas encore de sources sûres

VN:R_U [1.9.22_1171]
Rating: 0 (from 0 votes)
Share
Ce contenu a été publié dans iPhone, avec comme mot(s)-clef(s) , , , . Vous pouvez le mettre en favoris avec ce permalien.

2 réponses à Application mère et modules sur iOS

  1. Joan Zapata dit :

    Nice l’article ! Bon j’ai un peu décroché dans la mise en pratique (quand les crochets arrivent…), mais le concept est intéressant. Je crois pas avoir déjà vu ça, en général j’ai l’impression que c’est soit plein d’app comme tu le dis, soit une grosse app avec des fonctionnalités cachées / payantes. (cf. l’in-app billing par exemple, sur Android, qui permet de faire ça bien.)

    Je me pose une question d’ailleurs : puisqu’on a rarement une app iOS sans une app #A4C639, et que je crois que ta boite ne fait pas exception, où en sont tes collègues Androidiens dans leur réflexion / prise de tête ? :-D

    Je reste quand même sceptique sur le choix de la solution. Plusieurs apps ça donne plus de visibilité et une meilleure indexation, ça augmente les chances que l’user trouve ce qu’il cherche, sans avoir l’impression de tomber sur une usine à gaz. Ça permet aussi d’avoir une meilleure précision dans la notation / le nombre de téléchargements sur le market / store.

    Après, tu ne fais peut-être pas une application grand public, mais dans ce cas quels étaient les arguments contre les “markets privés”, ou les solutions de déploiement comme celle que tu avais testée quand on était sur Capico ?

    VN:R_U [1.9.22_1171]
    Rating: 0 (from 0 votes)
    • Damien V. dit :

      Alors du côté Android, les recherches n’ont pas commencées. On sait juste qu’il est possible de charger un JAR au runtime, mais pour l’utiliser en tant que sous-projet (module) Android, c’est encore assez flou. Je vous tiendrais au courant de l’avancée de l’étude de faisabilité du côté Android :)

      En fait, l’idée n’est pas forcément d’avoir une seule application (c’était pour simplifier l’article :)). Il s’agit plus de regrouper certaines type d’application. Par exemple on aurait une application pour le grand public, une autre pour les commerciaux, et une autre pour les clients si on est en B2B. Pour chacune d’entre elles, les modules à utiliser peuvent varier selon les droits de l’utilisateur. On peut donc contrôler finement les parties auxquelles ils ont accès et éventuellement ajouter ou retirer l’accès à un module pour une personne (suite à un changement de poste par exemple). C’est dans ce sens que l’idée a été proposée.

      L’idée d’un déploiement via un “market privé” pourrait être envisageable…. Si on connaissait à l’avance tout les appareils devant télécharger l’application et si le nombre n’excède pas un certain nombre, puisque pour chaque application on a un fichier qui regroupe les certificats : du développeur, de l’application et des appareils autorisés. L’outil TestFlight que j’avais testé pour Capico ne déroge pas à cette règle, sauf qu’il automatise légèrement le processus. Mais cela ne reste pas envisageable.

      VN:R_U [1.9.22_1171]
      Rating: 0 (from 0 votes)

Laisser un commentaire