Une app normale connaît ses écrans par cœur, et c'est plutôt rassurant : le développeur les dessine dans Xcode, les relit trois fois, les publie, et tout le monde rentre dîner. Puis un client appelle pour ajouter une question à sa checklist de sécurité, un deuxième veut la retirer, et un troisième la veut en arabe, avec une photo obligatoire, mais seulement le mardi (oui, ce client existe, il existe toujours). Si chaque case à cocher demande une nouvelle version, vous allez passer votre carrière dans la file de relecture d'Apple, qui n'est pas franchement réputée pour son sens de l'urgence. C'est exactement le problème que règle un moteur de formulaires : l'app arrête de connaître ses écrans, et c'est le serveur qui les lui envoie.
J'ai porté un moteur de ce genre côté iOS pendant plus de cinq ans chez Enablon, de 2019 à 2024, sous les apps d'inspection, d'audit et de reporting de clients grands comptes (l'histoire complète est dans l'étude de cas du Dynamic Forms SDK). Cinq ans, c'est assez long pour savoir ce qui compte vraiment, et surtout ce qui fait mal quand on l'oublie.
L'app qui ne sait pas à quoi elle ressemble (et c'est voulu)
Le principe tient en une phrase : au lieu d'un écran écrit en Swift, l'app reçoit une description du formulaire, en JSON la plupart du temps, et la transforme en écran au moment de l'ouvrir. Un champ ressemble à peu près à ceci, et oui, c'est moins glamour qu'une maquette Figma :
{ "id": "anomaly", "kind": "text", "label": "Décrire l'anomalie",
"required": true, "visibleIf": { "field": "extinguisher", "equals": "no" } }
Côté app, le moteur décode ces définitions, choisit un composant pour chaque type de champ (un choix, une photo, un texte, une date, une signature, et la liste s'allonge plus vite qu'on ne le voudrait) puis assemble l'écran. Voici une version volontairement simplifiée, écrite pour cet article et pas extraite du vrai SDK, histoire que personne ne m'envoie d'avocat :
struct FieldDefinition: Decodable {
enum Kind: String, Decodable { case choice, photo, text }
struct Condition: Decodable { let field: String; let equals: String }
let id: String
let kind: Kind
let label: String
let required: Bool
let visibleIf: Condition?
}
/// The fields to draw right now, given what the user has already answered.
func visibleFields(_ fields: [FieldDefinition], answers: [String: String]) -> [FieldDefinition] {
fields.filter { field in
guard let rule = field.visibleIf else { return true }
return answers[rule.field] == rule.equals
}
}
L'animation ci-dessous raconte la suite en quatre épisodes, et je ne spoile pas le dernier : l'écran qui se construit tout seul, une règle qui se réveille, une usine sans réseau, et une nouvelle version qui doit passer dans trois apps sans en casser aucune.
Les règles, ou l'art de faire apparaître un champ
Afficher des champs, c'est la partie facile, n'importe quel stagiaire motivé vous fait ça en un après-midi, café compris. Le vrai travail commence avec les règles : un champ qui n'apparaît que si on a répondu « non » plus haut, une validation qui refuse un nombre hors limites, un champ qui dépend d'un autre, une valeur pré-remplie à partir du site ou de l'utilisateur. Les clients adorent les règles, et ils ont raison, c'est ce qui transforme un formulaire bête en vrai outil de travail.
Le piège, c'est de laisser ces règles s'échapper dans le code des écrans. Un petit if ici pour un client pressé, un autre là pour la démo de jeudi, et six mois plus tard plus personne ne sait pourquoi le champ « commentaire » disparaît quand on change de langue (spoiler : c'était la démo de jeudi). Toute la logique doit vivre dans le moteur, au même endroit, et ce moteur mérite sa propre suite de tests, parce qu'une règle qui se trompe ne plante pas l'app, ce qui serait presque poli : elle laisse passer en silence un rapport faux, ce qui est bien pire.
Hors ligne, parce que les usines n'ont pas la fibre
Les inspections ont une fâcheuse tendance à se dérouler au sous-sol, dans une chaufferie ou au milieu d'un site industriel où le réseau relève de la légende urbaine. Un formulaire qui refuse de s'envoyer sans connexion est donc un formulaire décoratif. Dans notre cas, les checklists se remplissaient sans réseau, restaient stockées sur l'appareil et se synchronisaient toutes seules au retour de la connexion, sans que le technicien ait à y penser. C'est le genre de fonctionnalité que personne ne remarque quand elle marche, et que tout le monde remarque le jour où une demi-journée de rapport s'évapore.
Petit conseil au passage : les définitions de formulaire, elles aussi, doivent être disponibles hors ligne. Un moteur brillant qui attend le serveur pour savoir à quoi ressemble l'écran fait une très jolie page blanche au fond d'un parking souterrain.
Un moteur, trois apps, zéro droit à l'erreur
Le Dynamic Forms SDK tournait sous trois produits différents. Sur le papier, c'est le rêve : on corrige un bug une fois, et trois apps en profitent. Dans la vraie vie, c'est aussi une responsabilité assez vertigineuse, puisqu'une régression voyage exactement de la même manière, trois fois, et de préférence un vendredi.
Ce qui nous a gardés à flot tient en peu de choses : des versions numérotées du SDK sur un calendrier de sortie commun aux trois apps, une suite de tests sur le moteur de règles, des releases menées de la release candidate jusqu'à la production, et une seule personne qui relisait ce dont chaque app dépendait vraiment. Ajoutez ce qu'un moteur « grands comptes » doit encaisser sans broncher, l'authentification OAuth 2.0 avec PKCE, des APIs OData, l'arabe qui se lit de droite à gauche et la refonte d'une des apps en 12 langues, et vous comprenez pourquoi un libellé ne doit jamais être codé en dur, même « juste pour tester ». Surtout « juste pour tester ».
Faut-il vraiment un moteur ? (spoiler : pas toujours)
C'est probablement le conseil le plus utile de cet article, et le moins vendeur. Un moteur piloté par le serveur se rentabilise quand ce sont vos clients qui configurent leurs propres écrans, ou quand vos formulaires changent plus vite que vos versions sur les stores. Si vous avez cinq formulaires qui bougent deux fois par an, écrivez-les en SwiftUI comme tout le monde et dormez sur vos deux oreilles : sinon, vous payez la complexité deux fois, une fois pour construire le moteur et une fois pour le garder en vie.
Et si vous avez aussi une app Android, le moteur de règles est le candidat idéal à partager entre les deux plateformes, pour que « visible si » veuille dire exactement la même chose des deux côtés, et pas presque la même chose. C'est typiquement le genre de code que Kotlin Multiplatform permet d'écrire une seule fois.
Concrètement, je ferais quoi ?
Je commencerais par compter, honnêtement, combien de formulaires changent vraiment et qui les change, parce que la réponse décide de tout le reste. Si le moteur se justifie, je garderais son vocabulaire minuscule au départ (quelques types de champs, une poignée de règles), quitte à l'agrandir plus tard, plutôt que d'inventer un langage de programmation déguisé en JSON que personne n'aura envie de maintenir. J'écrirais les tests du moteur de règles avant le premier bel écran, je prévoirais le hors ligne dès le premier jour plutôt qu'après la première plainte, et je numéroterais le format des définitions, parce qu'un jour le serveur enverra un champ que la vieille version de l'app installée chez un client ne connaît pas, et ce jour-là il vaut mieux qu'elle le sache.
Si vous voulez de l'aide
Si votre app croule sous des formulaires qui changent tout le temps, ou si vous hésitez à vous lancer dans un moteur, je peux vous aider à trancher, puis à le construire si le jeu en vaut la chandelle. L'histoire complète est dans l'étude de cas Dynamic Forms SDK, et si vous préférez d'abord un regard extérieur sur l'app que vous avez déjà, l'audit en cinq jours commence exactement par là.